[
  {
    "path": ".asf.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ngithub:\n  description: \"Apache OpenWhisk is an open source serverless cloud platform\"\n  homepage: https://openwhisk.apache.org/\n  labels:\n    - openwhisk\n    - apache\n    - serverless\n    - faas\n    - functions-as-a-service\n    - cloud\n    - serverless-architectures\n    - serverless-functions\n    - docker\n    - kubernetes\n    - functions\n  protected_branches:\n    master:\n      required_status_checks:\n        strict: true\n      required_pull_request_reviews:\n        required_approving_review_count: 1\n      required_signatures: false\n  enabled_merge_buttons:\n    merge: false\n    squash: true\n    rebase: true\n  features:\n    issues: true\n\nnotifications:\n  commits: commits@openwhisk.apache.org\n  issues_status: issues@openwhisk.apache.org\n  issues_comment: issues@openwhisk.apache.org\n  pullrequests_status: issues@openwhisk.apache.org\n  pullrequests_comment: issues@openwhisk.apache.org\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization.\n# Resources:\n#       - https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html\n#       - http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/\n#       - https://help.github.com/articles/dealing-with-line-endings/\n* text=auto\n\n*.go            text eol=lf\n*.java          text\n*.js            text\n*.md            text\n*.py            text eol=lf\n*.scala         text\n*.sh            text eol=lf\n*.gradle        text\n*.xml           text\n*.bat           text eol=crlf\n\n*.jar           binary\n*.png           binary\n\n# python files not having the .py extension\ntools/cli/wsk      text eol=lf\ntools/cli/wskadmin text eol=lf\n\n# bash files not having the .sh extension\ngradlew                         text eol=lf\ncore/javaAction/proxy/gradlew   text eol=lf\nsdk/docker/client/action        text eol=lf\n\n# auth files with default api keys\nansible/files/auth.guest         text eol=lf\nansible/files/auth.whisk.system  text eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/proposal.md",
    "content": "## Enhancement Description\nPlease summarize your proposal (e.g. the \"what\", the \"why\", and some of the \"how\").\n\n* Proposal: [a link to the corresponding pull request if it exists already]()\n\n## References\nPlease provide links to prior discussion (e.g., Apache `dev` list), wikis, or related issues.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "<!--\nWe use the issue tracker for bugs and feature requests. For general questions and discussion please use https://openwhisk.apache.org/slack.html or https://openwhisk.apache.org/community.html instead.\n\nDo NOT share passwords, credentials or other confidential information.\n\nBefore creating a new issue, please check if there is one already open that\nfits the defect you are reporting.\nIf you open an issue and realize later it is a duplicate of a pre-existing\nopen issue, please close yours and add a comment to the other.\n\nIssues can be created for either defects or enhancement requests. If you are a committer than please add the labels \"bug\" or \"feature\". If you are not a committer please make clear in the comments which one it is, so that committers can add these labels later.\n\nIf you are reporting a defect, please edit the issue description to include the\ninformation shown below.\n\nIf you are reporting an enhancement request, please include information on what you are trying to achieve and why that enhancement would help you.\n\nFor more information about reporting issues, see\nhttps://github.com/apache/openwhisk/blob/master/CONTRIBUTING.md#raising-issues\n\nUse the commands below to provide key information from your environment:\nYou do not have to include this information if this is a feature request.\n-->\n\n## Environment details:\n\n* local deployment, native ubuntu, Mac OS, Kubernetes, ...\n* version of docker, ubuntu, ...\n\n## Steps to reproduce the issue:\n\n1.   \n2.   \n3.   \n\n\n## Provide the expected results and outputs:\n\n```\noutput comes here\n```\n\n\n## Provide the actual results and outputs:\n\n```\noutput comes here\n```\n\n## Additional information you deem important:\n* issue happens only occasionally or under certain circumstances   \n* changes you did or observed in the environment\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "\n<!--- Provide a concise summary of your changes in the Title -->\n\n## Description\n<!--- Provide a detailed description of your changes. -->\n<!--- Include details of what problem you are solving and how your changes are tested. -->\n\n## Related issue and scope\n<!--- Please include a link to a related issue if there is one. -->\n- [ ] I opened an issue to propose and discuss this change (#????)\n\n## My changes affect the following components\n<!--- Select below all system components are affected by your change. -->\n<!--- Enter an `x` in all applicable boxes. -->\n- [ ] API\n- [ ] Controller\n- [ ] Message Bus (e.g., Kafka)\n- [ ] Loadbalancer\n- [ ] Scheduler\n- [ ] Invoker\n- [ ] Intrinsic actions (e.g., sequences, conductors)\n- [ ] Data stores (e.g., CouchDB)\n- [ ] Tests\n- [ ] Deployment\n- [ ] CLI\n- [ ] General tooling\n- [ ] Documentation\n\n## Types of changes\n<!--- What types of changes does your code introduce? Use `x` in all the boxes that apply: -->\n- [ ] Bug fix (generally a non-breaking change which closes an issue).\n- [ ] Enhancement or new feature (adds new functionality).\n- [ ] Breaking change (a bug fix or enhancement which changes existing behavior).\n\n## Checklist:\n<!--- Please review the points below which help you make sure you've covered all aspects of the change you're making. -->\n\n- [ ] I signed an [Apache CLA](https://github.com/apache/openwhisk/blob/master/CONTRIBUTING.md).\n- [ ] I reviewed the [style guides](https://github.com/apache/openwhisk/blob/master/CONTRIBUTING.md#coding-standards) and followed the recommendations (Travis CI will check :).\n- [ ] I added tests to cover my changes.\n- [ ] My changes require further changes to the documentation.\n- [ ] I updated the documentation where necessary.\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# Keep GitHub Actions up to date with GitHub's Dependabot...\n# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot\nversion: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    groups:\n      GitHub_Actions:\n        patterns:\n          - \"*\"  # Group all Actions updates into a single larger pull request\n    schedule:\n      interval: weekly\n"
  },
  {
    "path": ".github/workflows/0-on-demand.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n#\nname: On Demand Tests\n\non:\n  workflow_dispatch:\n    inputs:\n      enable_ngrok_debug:\n        description: \"Enable Ngrok Debugging\"\n        required: true\n        type: boolean\n        default: false\n      test_suite:\n        description: Select Test Suite to run\n        type: choice\n        options:\n        - Unit\n        - System\n        - MultiRuntime\n        - Standalone\n        - Scheduler\n        - Performance\n        - Dummy\n\nenv:\n  # openwhisk env\n  TEST_SUITE: ${{ inputs.test_suite }}\n  ANSIBLE_CMD: \"ansible-playbook -i environments/local -e docker_image_prefix=testing\"\n  GRADLE_PROJS_SKIP: \"\"\n\n  # (optional) slack incoming wehbook for notifications\n  SLACK_WEBHOOK: ${{secrets.SLACK_WEBHOOK}}\n\n  # (optional) s3 log upload\n  LOG_BUCKET: ${{ secrets.LOG_BUCKET }}\n  LOG_ACCESS_KEY_ID: ${{ secrets.LOG_ACCESS_KEY_ID }}\n  LOG_SECRET_ACCESS_KEY: ${{ secrets.LOG_SECRET_ACCESS_KEY }}\n  LOG_REGION: ${{ secrets.LOG_REGION }}\n\n  # github\n  GH_BUILD: ${{ github.event_name }}-${{ github.sha }}\n  GH_BRANCH: ${{ github.head_ref || github.ref_name }}\n\n  # https://develocity.apache.org\n  DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}\n\n  # (optional) you need to add as secrets an ngrok token and a password to debug a build on demand\n  NGROK_DEBUG: ${{ inputs.enable_ngrok_debug }}\n  NGROK_TOKEN: ${{ secrets.NGROK_TOKEN }}\n  NGROK_PASSWORD: ${{ secrets.NGROK_PASSWORD }}\n\njobs:\n  openwhisk:\n    runs-on: ubuntu-22.04\n    continue-on-error: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: \"Setup\"\n        run: ./tools/github/setup.sh\n      - name: Maximize free space\n        run: >\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /usr/local/.ghcup\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n      - name: Check free space\n        run: df -h\n      - id: tests\n        name: Run Tests\n        run: \"./tools/github/run${{ env.TEST_SUITE }}Tests.sh\"\n        continue-on-error: true\n      - id: logs\n        name: Show results and Upload logs\n        run: ./tools/github/checkAndUploadLogs.sh ${{ env.TEST_SUITE }}\n      - name: Slack Notification\n        run: >\n             ./tools/github/writeOnSlack.sh\n             \"[$TEST_SUITE]\" ${{ steps.tests.outcome }} on ${GH_BUILD}\n             $'\\nbranch:' $GH_BRANCH\n             $'\\nmessage:' \"$(git log -1 --oneline | cat)\"\n             $'\\nlogs:' ${{ steps.logs.outputs.logs }}\n             $'\\nreport:' ${{ steps.logs.outputs.report }}\n      - name: Debug Action (if requested)\n        run:  ./tools/github/debugAction.sh\n      - name: Wait for Debug (if requested)\n        run: ./tools/github/waitIfDebug.sh\n      - name: Results\n        run: test \"${{ steps.tests.outcome }}\" ==  \"success\"\n"
  },
  {
    "path": ".github/workflows/1-unit.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n#\nname: Unit Tests\n\non:\n  push:\n    branches: [ master, 2.0.0 ]\n  pull_request:\n    branches: [ master, 2.0.0 ]\n    types: [ opened, synchronize, reopened ]\n  schedule:\n    - cron: '30 1 * * 1,3,5'\n\nenv:\n  # openwhisk env\n  TEST_SUITE: Unit\n  ANSIBLE_CMD: \"ansible-playbook -i environments/local -e docker_image_prefix=testing\"\n  GRADLE_PROJS_SKIP: \"\"\n\n  ## secrets\n  # (optional) slack incoming wehbook for notifications\n  SLACK_WEBHOOK: ${{secrets.SLACK_WEBHOOK}}\n\n  # (optional) s3 log upload\n  LOG_BUCKET: ${{ secrets.LOG_BUCKET }}\n  LOG_ACCESS_KEY_ID: ${{ secrets.LOG_ACCESS_KEY_ID }}\n  LOG_SECRET_ACCESS_KEY: ${{ secrets.LOG_SECRET_ACCESS_KEY }}\n  LOG_REGION: ${{ secrets.LOG_REGION }}\n\n  # some tests need also this even if they are empty on pull_requests...\n  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n  AWS_REGION: ${{ secrets.AWS_REGION }}\n\n  # github\n  GH_BUILD: ${{ github.event_name }}-${{ github.sha }}\n  GH_BRANCH: ${{ github.head_ref || github.ref_name }}\n\n  # https://develocity.apache.org\n  DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}\n\njobs:\n  openwhisk:\n    runs-on: ubuntu-22.04\n    continue-on-error: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: \"Setup\"\n        run: ./tools/github/setup.sh\n      - name: Maximize free space\n        run: >\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /usr/local/.ghcup\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n      - name: Check free space\n        run: df -h\n      - id: tests\n        name: Run Tests\n        run: \"./tools/github/run${{ env.TEST_SUITE }}Tests.sh\"\n        continue-on-error: true\n      - id: logs\n        name: Show results and Upload logs\n        run: ./tools/github/checkAndUploadLogs.sh ${{ env.TEST_SUITE }}\n      - name: Slack Notification\n        run: > \n             ./tools/github/writeOnSlack.sh\n             \"[$TEST_SUITE]\" ${{ steps.tests.outcome }} on ${GH_BUILD}\n             $'\\nbranch:' $GH_BRANCH\n             $'\\nmessage:' \"$(git log -1 --oneline | cat)\"\n             $'\\nlogs:' ${{ steps.logs.outputs.logs }}\n             $'\\nreport:' ${{ steps.logs.outputs.report }}\n      - name: Results\n        run: test \"${{ steps.tests.outcome }}\" ==  \"success\"\n"
  },
  {
    "path": ".github/workflows/2-system.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n#\nname: System Tests\n\non:\n  push:\n    branches: [ master, 2.0.0 ]\n  pull_request:\n    branches: [ master, 2.0.0 ]\n    types: [ opened, synchronize, reopened ]\n  schedule:\n    - cron: '30 2 * * 1,3,5'\n\nenv:\n  # openwhisk env\n  TEST_SUITE: System\n  ANSIBLE_CMD: \"ansible-playbook -i environments/local -e docker_image_prefix=testing -e container_pool_pekko_client=false\"\n  GRADLE_PROJS_SKIP: \"\"\n\n  ## secrets\n  # (optional) slack incoming wehbook for notifications\n  SLACK_WEBHOOK: ${{secrets.SLACK_WEBHOOK}}\n\n  # (optional) s3 log upload\n  LOG_BUCKET: ${{ secrets.LOG_BUCKET }}\n  LOG_ACCESS_KEY_ID: ${{ secrets.LOG_ACCESS_KEY_ID }}\n  LOG_SECRET_ACCESS_KEY: ${{ secrets.LOG_SECRET_ACCESS_KEY }}\n  LOG_REGION: ${{ secrets.LOG_REGION }}\n\n  # github\n  GH_BUILD: ${{ github.event_name }}-${{ github.sha }}\n  GH_BRANCH: ${{ github.head_ref || github.ref_name }}\n\n  # https://develocity.apache.org\n  DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}\n\njobs:\n  openwhisk:\n    runs-on: ubuntu-22.04\n    continue-on-error: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: \"Setup\"\n        run: ./tools/github/setup.sh\n      - name: Maximize free space\n        run: >\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /usr/local/.ghcup\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n      - name: Check free space\n        run: df -h\n      - id: tests\n        name: Run Tests\n        run: \"./tools/github/run${{ env.TEST_SUITE }}Tests.sh\"\n        continue-on-error: true\n      - id: logs\n        name: Show results and Upload logs\n        run: ./tools/github/checkAndUploadLogs.sh ${{ env.TEST_SUITE }}\n      - name: Slack Notification\n        run: > \n             ./tools/github/writeOnSlack.sh\n             \"[$TEST_SUITE]\" ${{ steps.tests.outcome }} on ${GH_BUILD}\n             $'\\nbranch:' $GH_BRANCH\n             $'\\nmessage:' \"$(git log -1 --oneline | cat)\"\n             $'\\nlogs:' ${{ steps.logs.outputs.logs }}\n             $'\\nreport:' ${{ steps.logs.outputs.report }}\n      - name: Results\n        run: test \"${{ steps.tests.outcome }}\" ==  \"success\"\n"
  },
  {
    "path": ".github/workflows/3-multi-runtime.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n#\nname: MultiRuntime Tests\n\non:\n  push:\n    branches: [ master, 2.0.0 ]\n  pull_request:\n    branches: [ master, 2.0.0 ]\n    types: [ opened, synchronize, reopened ]\n  schedule:\n    - cron: '30 3 * * 1,3,5'\n\nenv:\n  # openwhisk env\n  TEST_SUITE: MultiRuntime\n  ANSIBLE_CMD: \"ansible-playbook -i environments/local -e docker_image_prefix=testing\"\n  GRADLE_PROJS_SKIP: \"\"\n\n  ## secrets\n  # (optional) slack incoming wehbook for notifications\n  SLACK_WEBHOOK: ${{secrets.SLACK_WEBHOOK}}\n\n  # (optional) s3 log upload\n  LOG_BUCKET: ${{ secrets.LOG_BUCKET }}\n  LOG_ACCESS_KEY_ID: ${{ secrets.LOG_ACCESS_KEY_ID }}\n  LOG_SECRET_ACCESS_KEY: ${{ secrets.LOG_SECRET_ACCESS_KEY }}\n  LOG_REGION: ${{ secrets.LOG_REGION }}\n\n  # github\n  GH_BUILD: ${{ github.event_name }}-${{ github.sha }}\n  GH_BRANCH: ${{ github.head_ref || github.ref_name }}\n\n  # https://develocity.apache.org\n  DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}\n\njobs:\n  openwhisk:\n    runs-on: ubuntu-22.04\n    continue-on-error: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: \"Setup\"\n        run: ./tools/github/setup.sh\n      - name: Maximize free space\n        run: >\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /usr/local/.ghcup\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n      - name: Check free space\n        run: df -h\n      - id: tests\n        name: Run Tests\n        run: \"./tools/github/run${{ env.TEST_SUITE }}Tests.sh\"\n        continue-on-error: true\n      - id: logs\n        name: Show results and Upload logs\n        run: ./tools/github/checkAndUploadLogs.sh ${{ env.TEST_SUITE }}\n      - name: Slack Notification\n        run: > \n             ./tools/github/writeOnSlack.sh\n             \"[$TEST_SUITE]\" ${{ steps.tests.outcome }} on ${GH_BUILD}\n             $'\\nbranch:' $GH_BRANCH\n             $'\\nmessage:' \"$(git log -1 --oneline | cat)\"\n             $'\\nlogs:' ${{ steps.logs.outputs.logs }}\n             $'\\nreport:' ${{ steps.logs.outputs.report }}\n      - name: Results\n        run: test \"${{ steps.tests.outcome }}\" ==  \"success\"\n"
  },
  {
    "path": ".github/workflows/4-standalone.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n#\nname: Standalone Tests\n\non:\n  push:\n    branches: [ master, 2.0.0 ]\n  pull_request:\n    branches: [ master, 2.0.0 ]\n    types: [ opened, synchronize, reopened ]\n  schedule:\n    - cron: '30 4 * * 1,3,5'\n\nenv:\n  # openwhisk env\n  TEST_SUITE: Standalone\n  ANSIBLE_CMD: \"ansible-playbook -i environments/local -e docker_image_prefix=testing\"\n  GRADLE_PROJS_SKIP: \"\"\n\n  ## secrets\n  # (optional) slack incoming wehbook for notifications\n  SLACK_WEBHOOK: ${{secrets.SLACK_WEBHOOK}}\n\n  # (optional) s3 log upload\n  LOG_BUCKET: ${{ secrets.LOG_BUCKET }}\n  LOG_ACCESS_KEY_ID: ${{ secrets.LOG_ACCESS_KEY_ID }}\n  LOG_SECRET_ACCESS_KEY: ${{ secrets.LOG_SECRET_ACCESS_KEY }}\n  LOG_REGION: ${{ secrets.LOG_REGION }}\n\n  # github\n  GH_BUILD: ${{ github.event_name }}-${{ github.sha }}\n  GH_BRANCH: ${{ github.head_ref || github.ref_name }}\n\n  # https://develocity.apache.org\n  DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}\n\njobs:\n  openwhisk:\n    runs-on: ubuntu-22.04\n    continue-on-error: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: \"Setup\"\n        run: ./tools/github/setup.sh\n      - name: Maximize free space\n        run: >\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /usr/local/.ghcup\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n      - name: Check free space\n        run: df -h\n      - id: tests\n        name: Run Tests\n        run: \"./tools/github/run${{ env.TEST_SUITE }}Tests.sh\"\n        continue-on-error: true\n      - id: logs\n        name: Show results and Upload logs\n        run: ./tools/github/checkAndUploadLogs.sh ${{ env.TEST_SUITE }}\n      - name: Slack Notification\n        run: > \n             ./tools/github/writeOnSlack.sh\n             \"[$TEST_SUITE]\" ${{ steps.tests.outcome }} on ${GH_BUILD}\n             $'\\nbranch:' $GH_BRANCH\n             $'\\nmessage:' \"$(git log -1 --oneline | cat)\"\n             $'\\nlogs:' ${{ steps.logs.outputs.logs }}\n             $'\\nreport:' ${{ steps.logs.outputs.report }}\n      - name: Results\n        run: test \"${{ steps.tests.outcome }}\" ==  \"success\"\n"
  },
  {
    "path": ".github/workflows/5-scheduler.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n#\nname: Scheduler Tests\n\non:\n  push:\n    branches: [ master, 2.0.0 ]\n  pull_request:\n    branches: [ master, 2.0.0 ]\n    types: [ opened, synchronize, reopened ]\n  schedule:\n    - cron: '30 5 * * 1,3,5'\n\nenv:\n  # openwhisk env\n  TEST_SUITE: Scheduler\n  ANSIBLE_CMD: \"ansible-playbook -i environments/local -e docker_image_prefix=testing\"\n  GRADLE_PROJS_SKIP: \"\"\n\n  ## secrets\n  # (optional) slack incoming wehbook for notifications\n  SLACK_WEBHOOK: ${{secrets.SLACK_WEBHOOK}}\n\n  # (optional) s3 log upload\n  LOG_BUCKET: ${{ secrets.LOG_BUCKET }}\n  LOG_ACCESS_KEY_ID: ${{ secrets.LOG_ACCESS_KEY_ID }}\n  LOG_SECRET_ACCESS_KEY: ${{ secrets.LOG_SECRET_ACCESS_KEY }}\n  LOG_REGION: ${{ secrets.LOG_REGION }}\n\n  # github\n  GH_BUILD: ${{ github.event_name }}-${{ github.sha }}\n  GH_BRANCH: ${{ github.head_ref || github.ref_name }}\n\n  # https://develocity.apache.org\n  DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}\n\njobs:\n  openwhisk:\n    runs-on: ubuntu-22.04\n    continue-on-error: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: \"Setup\"\n        run: ./tools/github/setup.sh\n      - name: Maximize free space\n        run: >\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /usr/local/.ghcup\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n      - name: Check free space\n        run: df -h\n      - id: tests\n        name: Run Tests\n        run: \"./tools/github/run${{ env.TEST_SUITE }}Tests.sh\"\n        continue-on-error: true\n      - id: logs\n        name: Show results and Upload logs\n        run: ./tools/github/checkAndUploadLogs.sh ${{ env.TEST_SUITE }}\n      - name: Slack Notification\n        run: > \n             ./tools/github/writeOnSlack.sh\n             \"[$TEST_SUITE]\" ${{ steps.tests.outcome }} on ${GH_BUILD}\n             $'\\nbranch:' $GH_BRANCH\n             $'\\nmessage:' \"$(git log -1 --oneline | cat)\"\n             $'\\nlogs:' ${{ steps.logs.outputs.logs }}\n             $'\\nreport:' ${{ steps.logs.outputs.report }}\n      - name: Results\n        run: test \"${{ steps.tests.outcome }}\" ==  \"success\"\n"
  },
  {
    "path": ".github/workflows/6-performance.yaml",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n#\nname: Performance Tests\n\non:\n  push:\n    branches: [ master, 2.0.0 ]\n  pull_request:\n    branches: [ master, 2.0.0 ]\n    types: [ opened, synchronize, reopened ]\n  schedule:\n    - cron: '30 6 * * 1,3,5'\n\nenv:\n  # openwhisk env\n  TEST_SUITE: Performance\n  ANSIBLE_CMD: \"ansible-playbook -i environments/local -e docker_image_prefix=testing\"\n  GRADLE_PROJS_SKIP: \"\"\n\n  ## secrets\n  # (optional) slack incoming wehbook for notifications\n  SLACK_WEBHOOK: ${{secrets.SLACK_WEBHOOK}}\n\n  # (optional) s3 log upload\n  LOG_BUCKET: ${{ secrets.LOG_BUCKET }}\n  LOG_ACCESS_KEY_ID: ${{ secrets.LOG_ACCESS_KEY_ID }}\n  LOG_SECRET_ACCESS_KEY: ${{ secrets.LOG_SECRET_ACCESS_KEY }}\n  LOG_REGION: ${{ secrets.LOG_REGION }}\n\n  # github\n  GH_BUILD: ${{ github.event_name }}-${{ github.sha }}\n  GH_BRANCH: ${{ github.head_ref || github.ref_name }}\n\n  # https://develocity.apache.org\n  DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}\n\njobs:\n  openwhisk:\n    runs-on: ubuntu-22.04\n    continue-on-error: false\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: \"Setup\"\n        run: ./tools/github/setup.sh\n      - name: Maximize free space\n        run: >\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /usr/local/.ghcup\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n      - name: Check free space\n        run: df -h\n      - run: ./tests/performance/preparation/deploy.sh\n      - run: TERM=dumb ./tests/performance/wrk_tests/latency.sh \"https://172.17.0.1:10001\" \"$(cat ansible/files/auth.guest)\" ./tests/performance/preparation/actions/noop.js 2m\n        continue-on-error: true\n      - run: TERM=dumb ./tests/performance/wrk_tests/latency.sh \"https://172.17.0.1:10001\" \"$(cat ansible/files/auth.guest)\" ./tests/performance/preparation/actions/async.js 2m\n        continue-on-error: true\n      - run: TERM=dumb ./tests/performance/wrk_tests/throughput.sh \"https://172.17.0.1:10001\" \"$(cat ansible/files/auth.guest)\" ./tests/performance/preparation/actions/noop.js 4 1 2 2m\n        continue-on-error: true\n      - run: TERM=dumb ./tests/performance/wrk_tests/throughput.sh \"https://172.17.0.1:10001\" \"$(cat ansible/files/auth.guest)\" ./tests/performance/preparation/actions/async.js 4 1 2 2m\n        continue-on-error: true\n      - run: TERM=dumb ./tests/performance/wrk_tests/throughput.sh \"https://172.17.0.1:10001\" \"$(cat ansible/files/auth.guest)\" ./tests/performance/preparation/actions/noop.js 100 110 2 2m\n        continue-on-error: true\n      - run: TERM=dumb ./tests/performance/wrk_tests/throughput.sh \"https://172.17.0.1:10001\" \"$(cat ansible/files/auth.guest)\" ./tests/performance/preparation/actions/async.js 100 110 2 2m\n        continue-on-error: true\n      - run: OPENWHISK_HOST=\"172.17.0.1\" CONNECTIONS=\"100\" REQUESTS_PER_SEC=\"1\" ./gradlew gatlingRun-org.apache.openwhisk.ApiV1Simulation\n        continue-on-error: true\n      - run: OPENWHISK_HOST=\"172.17.0.1\" MEAN_RESPONSE_TIME=\"1000\" API_KEY=\"$(cat ansible/files/auth.guest)\" EXCLUDED_KINDS=\"python:default,java:default,swift:default\" PAUSE_BETWEEN_INVOKES=\"100\" ./gradlew gatlingRun-org.apache.openwhisk.LatencySimulation\n        continue-on-error: true\n      - run: OPENWHISK_HOST=\"172.17.0.1\" API_KEY=\"$(cat ansible/files/auth.guest)\" CONNECTIONS=\"100\" REQUESTS_PER_SEC=\"1\" ./gradlew gatlingRun-org.apache.openwhisk.BlockingInvokeOneActionSimulation\n        continue-on-error: true\n      - run: OPENWHISK_HOST=\"172.17.0.1\" API_KEY=\"$(cat ansible/files/auth.guest)\" CONNECTIONS=\"100\" REQUESTS_PER_SEC=\"1\" ASYNC=\"true\" ./gradlew gatlingRun-org.apache.openwhisk.BlockingInvokeOneActionSimulation\n        continue-on-error: true\n      # The following configuration does not make much sense. But we do not have enough users. But it's good to verify, that the test is still working.\n      - run: OPENWHISK_HOST=\"172.17.0.1\" USERS=\"1\" REQUESTS_PER_SEC=\"1\" ./gradlew gatlingRun-org.apache.openwhisk.ColdBlockingInvokeSimulation\n        continue-on-error: true\n      - name: Slack Notification\n        run: >\n          ./tools/github/writeOnSlack.sh\n          \"[$TEST_SUITE]\" ${{ steps.tests.outcome }} on ${GH_BUILD}\n          $'\\nbranch:' $GH_BRANCH\n          $'\\nmessage:' \"$(git log -1 --oneline | cat)\"\n          $'\\nCheck GitHub logs for results'\n"
  },
  {
    "path": ".github/workflows/README.md",
    "content": "# How to use those workflows\n\nThere are a few [GitHub secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) to configure to fully leverage the build.\n\nYou can use and set the following secrets also in your fork.\n\n## Ngrok Debugging\n\nYou can debug a GitHub Action build using [NGROK](https://ngrok.com/).\n\nIt is disabled for automated build triggered by push and pull_requests.\n\nYou can trigger a workflow run manually enabling ngrok debugging.\n\nIt will open an ssh connection to the VM and keep it up and running for one hour.\nThe connection URL is shown in the log for debugAction.sh\n\nYou can then connect to the build VM, and debug it.\nYou need to use a password of your choice to access it.\n\nYou can continue the build with `touch /tmp/continue`.\nYou can abort the build with `touch /tmp/abort`.\n\nTo enable this option you have to register to Ngrok, using the fee account and get the NGROK Token.\n\nThen set the following secrets:\n\n- `NGROK_TOKEN` to the ngrok token.\n- `NGROK_PASSWORD` to a password of choice to access the build with the ssh command generated.\n\n## Log Upload\n\nThe build uploads the logs to a S3 bucket allowing to inspect them with a browser.\n\nYou need to create the bucket with the following commands:\n\n```\nLOG_BUCKET=<name-of-your-bucket>\nLOG_REGION=<the-region-you-use>\naws s3 mb s3://$LOG_BUCKET --region $LOG_REGION\naws s3 website s3://$LOG_BUCKET/ --index-document index.html\naws s3api put-bucket-acl --acl public-read --bucket $LOG_BUCKET\n```\n\nTo enable upload to the created bucket you need to set the following secrets:\n\n- `LOG_BUCKET`: name of your bucket in s3 (just the name, without `s3://`); create it before.\n- `LOG_ACCESS_KEY_ID`: your aws access key.\n- `LOG_SECRET_ACCESS_KEY`: your aws secret key.\n- `LOG_REGION`: important: the region where your bucket is.\n\n## Slack notification\n\nIf you want to get notified of what happens on slack, create an [Incoming Web Hook](https://api.slack.com/messaging/webhooks) and then set the following secret:\n\n- `SLACK_WEBHOOK`: the incoming webhook URL provided by slack.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Whisk\nnginx.conf\nwhisk.properties\nwhisk.conf\ndefault.props\n/tests/src/test/resources/application.conf\n\n.ant-targets-build.xml\n/results/\n/logs/\n/config/custom-config.xml\nresults\n*.retry\n\n# Environments\n/ansible/environments/*\n!/ansible/environments/distributed\n!/ansible/environments/docker-machine\n!/ansible/environments/local\n!/ansible/environments/mac\n\n# Eclipse\nbin/\n**/.project\n.settings/\n.classpath\n.cache-main\n.cache-tests\n\n# Linux\n*~\n*.swp\n\n# Mac\n.DS_Store\n\n# Gradle\n.gradle\nbuild/\n!/tools/build/\n\n# Python\n.ipynb_checkpoints/\n*.pyc\n\n# NodeJS\nnode_modules\n\n# IntelliJ\n.idea\n*.class\n*.iml\nout/\n\n# Ansible\nansible/environments/docker-machine/hosts\nansible/environments/local/hosts\nansible/db_local.ini*\nansible/tmp/*\nansible/roles/nginx/files/openwhisk-client*\nansible/roles/nginx/files/*.csr\nansible/roles/nginx/files/*.p12\nansible/roles/nginx/files/*cert.pem\nansible/roles/nginx/files/*p12\nansible/roles/kafka/files/*\nansible/roles/controller/files/*\nansible/roles/invoker/files/*\n\n# .zip files must be explicited whitelisted\n*.zip\ntests/dat/actions/sleep.jar\ntests/dat/actions/blackbox.zip\ntests/dat/actions/python.zip\ntests/dat/actions/zippedaction.zip\ntests/dat/actions/zippedaction/node_modules\ntests/dat/actions/python2_virtualenv.zip\ntests/dat/actions/python_virtualenv/virtualenv\ntests/dat/actions/.built\ntests/dat/actions/unicode.tests/java-8.bin\ntests/performance/gatling_tests/src/gatling/resources/data/javaAction.jar\ntests/performance/gatling_tests/.built\n\n# dev\nintellij-run-config.groovy\n\n# VSCode\n.vscode/\n\n# route management\ncore/routemgmt/createApi/apigw-utils.js\ncore/routemgmt/createApi/package-lock.json\ncore/routemgmt/createApi/utils.js\ncore/routemgmt/deleteApi/apigw-utils.js\ncore/routemgmt/deleteApi/package-lock.json\ncore/routemgmt/deleteApi/utils.js\ncore/routemgmt/getApi/apigw-utils.js\ncore/routemgmt/getApi/package-lock.json\ncore/routemgmt/getApi/utils.js\n\n# vscode metals\n.bloop/\n.metals/\n"
  },
  {
    "path": ".pydevproject",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<?eclipse-pydev version=\"1.0\"?><pydev_project>\n<pydev_property name=\"org.python.pydev.PYTHON_PROJECT_INTERPRETER\">Default</pydev_property>\n<pydev_property name=\"org.python.pydev.PYTHON_PROJECT_VERSION\">python 3.6</pydev_property>\n</pydev_project>\n"
  },
  {
    "path": ".scalafmt.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nversion = 1.5.1\nstyle = intellij\ndanglingParentheses = false\nmaxColumn = 120\ndocstrings = JavaDoc\nrewrite.rules = [SortImports]\nproject.git = true\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# OpenWhisk Core\n\n## Apache 2.0.0\n### Branch: [2.0.0](https://github.com/apache/openwhisk/tree/2.0.0)\n### Changes\n- Upgrade the docker version to 23.0.6 (https://github.com/apache/openwhisk/commit/8af209262c98e07b8cc3cb688d5d48f633f2d006, [@style95](https://github.com/style95))\n- Enable the scheduler by default (https://github.com/apache/openwhisk/commit/4fac03aa8b2df4d8074f2af5b1139225e02890ea, [@style95](https://github.com/style95))\n- Update the ngrok v3 (https://github.com/apache/openwhisk/commit/5529cc49d31f135dfdac4f2a2072ca46bfd754de, [@style95](https://github.com/style95))\n- Update FPC invoker health reporting logic (https://github.com/apache/openwhisk/commit/aea3a8814d4e9ce6f704374d124146be1693f123, [@bdoyle0182](https://github.com/bdoyle0182))\n- Change the required_status_checks to strict (https://github.com/apache/openwhisk/commit/e20ab1789ecea04da15dd34bddbc1143607e08cf, [@style95](https://github.com/style95))\n- Bump the github_actions group with 1 update (https://github.com/apache/openwhisk/commit/29168859aaab83b821a18f12243f63793cbfe588, [@dependabot[bot]](https://github.com/apps/dependabot))\n- Keep GitHub Actions up to date with GitHub's Dependabot (https://github.com/apache/openwhisk/commit/5c876f247982e2896c974f91114e1ce89b7b4c8e, [@cclauss](https://github.com/cclauss))\n- Fix typo (https://github.com/apache/openwhisk/commit/3f11fcc361e060326f4341e9e79d14c185eb5771, [@cclauss](https://github.com/cclauss))\n- Fix unit test cases (https://github.com/apache/openwhisk/commit/4b5c5d1c7aaebffd6e07db0c4b0d8e79b648bcbb, [@style95](https://github.com/style95))\n- Update README.md (https://github.com/apache/openwhisk/commit/6d99fd1c4ae92741994457a367df3384c052ef2d, [@moritzraho](https://github.com/moritzraho))\n- Add ability to scale Ephemeral storage along with memory, similar to CPU (https://github.com/apache/openwhisk/commit/b9f16dc68100e41f19a6a78f92c5e27dc5fab177, [@mcdan](https://github.com/mcdan))\n- Fixed an issue with missed config keys while iterating through them on Scala 2.13.x (https://github.com/apache/openwhisk/commit/597d61d6eef974b25e5cdc6e1c284f9867429170, [@joni-jones](https://github.com/joni-jones))\n- Bump lodash from 4.17.15 to 4.17.21 in /core/routemgmt/getApi (https://github.com/apache/openwhisk/commit/cdde5a79c663796da1af494296cc8afb20dfb9ac, [@dependabot[bot]](https://github.com/apps/dependabot))\n- Use max-connection-pool as queue size (https://github.com/apache/openwhisk/commit/ae0c4e0c37a77de4c23ca95dee763b165ce20d18, [@style95](https://github.com/style95))\n- fix: message skipping typo (https://github.com/apache/openwhisk/commit/a4f4b2da6f7fed4821775fb79a60b7a5e6f396b4, [@0o001](https://github.com/0o001))\n- Memory leak in `akka.actor.LocalActorRef` (https://github.com/apache/openwhisk/commit/6f11d48b216a01b7c6fa3342d2b8b972109106f7, [@joni-jones](https://github.com/joni-jones))\n- Add optional cpu limit to spawned action containers (https://github.com/apache/openwhisk/commit/0c27a650ab6073e131e5c74002465e93cf4d8621, [@quintenp01](https://github.com/quintenp01))\n- Add ZOOKEEPER_HOSTS as an optional property (https://github.com/apache/openwhisk/commit/6375c96066ca0e24cb81996a877bdd7caecdb72b, [@style95](https://github.com/style95))\n- Update the nodejs action kind (https://github.com/apache/openwhisk/commit/20f7d98fdfba275bc3e5c88be0b7cdec956df2aa, [@style95](https://github.com/style95))\n- Maximize build spaces of all workflows by manually removing unnecessary resources (https://github.com/apache/openwhisk/commit/951ce58be7f82122734523b338053c49662a6dad, [@style95](https://github.com/style95))\n- Compare invocation namespaces when handling a cycle and recovering a queue (https://github.com/apache/openwhisk/commit/9371d6290136b37a24bbb32a9ef50ff816e9211d, [@style95](https://github.com/style95))\n- Refresh runtimes list: add new versions; remove old/deprecated versions (https://github.com/apache/openwhisk/commit/54564cbc76771658c14a84dd413856d8b078eeb8, [@dgrove-oss](https://github.com/dgrove-oss))\n- Authenticate GitHub Actions builds to ge.apache.org (https://github.com/apache/openwhisk/commit/be3f6d63be9ff513f733f0b64bde7bca78e8c148, [@clayburn](https://github.com/clayburn))\n- Remove `fail-fast` matrix strategy from GitHub Actions (https://github.com/apache/openwhisk/commit/f73ec7f006ff278c9ff88a241d58c5c799d16115, [@clayburn](https://github.com/clayburn))\n- Finish setup on error (https://github.com/apache/openwhisk/commit/88156c3c382dff013dc9f636ac49f0a39886cbce, [@style95](https://github.com/style95))\n- patch groovy 3.0.17 (https://github.com/apache/openwhisk/commit/6c47e6024d9de18c7151e4f04f56a195e98b0cd3, [@bdoyle0182](https://github.com/bdoyle0182))\n- Capture build scans on ge.apache.org to benefit from deep build insights (https://github.com/apache/openwhisk/commit/37605b4da431d610660d97ae11e181f2aecff268, [@clayburn](https://github.com/clayburn))\n- Apply scalaFmt (https://github.com/apache/openwhisk/commit/ba871e59f7b77f02689a13e4e24e438645d67a47, [@style95](https://github.com/style95))\n- User Defined Action Instance Concurrency Limits (https://github.com/apache/openwhisk/commit/72bb2a1fc4783f29cb34d6ad1ffabf2b6676773b, [@bdoyle0182](https://github.com/bdoyle0182))\n- use compatibility serializer for future message forma… (https://github.com/apache/openwhisk/commit/be8ac20372fe44ed226c758b71dce510a4227cc8, [@bdoyle0182](https://github.com/bdoyle0182))\n- upgrade kafka client library to 2.8.2 (https://github.com/apache/openwhisk/commit/6bc559d41f74525e26c9bd03d01dc4a3e9b5c658, [@bdoyle0182](https://github.com/bdoyle0182))\n- update scheduler not processing metric to counter (https://github.com/apache/openwhisk/commit/a22e706b0f93cf4d673a2aa5ab6110918ce149c0, [@bdoyle0182](https://github.com/bdoyle0182))\n- add support for multi partition kafka topics (https://github.com/apache/openwhisk/commit/d84e4eefb764b36b63867967b1882d1226481f61, [@bdoyle0182](https://github.com/bdoyle0182))\n- fix bug in average ring buffer and add negative protection to scheduling decision maker (https://github.com/apache/openwhisk/commit/de3e0a8fdf4bf6341ccc423727628d554e53edee, [@bdoyle0182](https://github.com/bdoyle0182))\n- fix action not processing metric (https://github.com/apache/openwhisk/commit/4e2dea12c7b0cfe14dcb509685cc9097d6007e19, [@bdoyle0182](https://github.com/bdoyle0182))\n- Send a queue removed message to the queue manager (https://github.com/apache/openwhisk/commit/cb3b64f84f64305cf680faaa14f2cd13bdb56589, [@style95](https://github.com/style95))\n- fix memory queue stuck in removed state edge case (https://github.com/apache/openwhisk/commit/fedf0227803401f7ac6cf3a33d9e14fc6fac74e8, [@bdoyle0182](https://github.com/bdoyle0182))\n- fix missed etcd unregister data case for an existing container in container proxy (https://github.com/apache/openwhisk/commit/7c94e9ba05ec9dbd59303672cc81b963d53351ba, [@bdoyle0182](https://github.com/bdoyle0182))\n- Add Scheduler Queue Metric for Not Processing Any Activations (https://github.com/apache/openwhisk/commit/60ca6605bb081f99906cff1a21caf75d47e414fa, [@bdoyle0182](https://github.com/bdoyle0182))\n- remove action version from scheduler metrics without kamon (https://github.com/apache/openwhisk/commit/96ff327dcce25bdb91b59fd2746d1e3a2979143e, [@bdoyle0182](https://github.com/bdoyle0182))\n- add base dependency version to cve remediations for downstream runtime builds (https://github.com/apache/openwhisk/commit/949c513de55c06e8e244a05c57f5d2419eb00466, [@bdoyle0182](https://github.com/bdoyle0182))\n- attempt to fix downstream runtime builds (https://github.com/apache/openwhisk/commit/6dd737d628a9e852c2ff2db55ac510d7661b8528, [@bdoyle0182](https://github.com/bdoyle0182))\n- dependency updates for cve patches (https://github.com/apache/openwhisk/commit/65a0132e73b41528dcf5b2817a55a579f7900433, [@bdoyle0182](https://github.com/bdoyle0182))\n- Bump Newtonsoft.Json (https://github.com/apache/openwhisk/commit/8054f3b4cb53babdbc89911060258f36363afbc0, [@dependabot[bot]](https://github.com/apps/dependabot))\n- Docs golang broken markdown link (https://github.com/apache/openwhisk/commit/2358976a5d5f9f42c539a6436b35922c4f8898fa, [@jonasbn](https://github.com/jonasbn))\n- update language runtimes to use newer versions (https://github.com/apache/openwhisk/commit/b7c203b8b70f1d5ec39e95287a6cf572fed33f1b, [@dgrove-oss](https://github.com/dgrove-oss))\n- dependency updates for cve patches (https://github.com/apache/openwhisk/commit/f0e281e35f90930983fbbbd0da2da3f4b32b4d72, [@bdoyle0182](https://github.com/bdoyle0182))\n- fix flaky ActivationClientProxy unit test (https://github.com/apache/openwhisk/commit/ed43b4d1ce0e6ae27c9a4b714be123be5a82fae9, [@bdoyle0182](https://github.com/bdoyle0182))\n- rollback logback minor upgrade to latest patch (https://github.com/apache/openwhisk/commit/3ea756f2d9d42463212c192f6a008f7d24e05718, [@bdoyle0182](https://github.com/bdoyle0182))\n- upgrade some dependencies for CVE Patches (https://github.com/apache/openwhisk/commit/084c2ad9dbaf1c86b469eb4730b9eb9e7bc6e095, [@bdoyle0182](https://github.com/bdoyle0182))\n- make scheduler consider action concurrency >1 (https://github.com/apache/openwhisk/commit/415ae98fd9f1fd44f5ab2dccb8b5cbe2d20932bb, [@bdoyle0182](https://github.com/bdoyle0182))\n- remove zookeeper config requirement (https://github.com/apache/openwhisk/commit/9d96c6ded0bcc46076a8be2800fc061d08eb3704, [@bdoyle0182](https://github.com/bdoyle0182))\n- add GHA status badges; remove .travis.yml (https://github.com/apache/openwhisk/commit/096dba495dff5eddd95d98159739e155df628040, [@dgrove-oss](https://github.com/dgrove-oss))\n- Fix the bug that match does not exhaustive (https://github.com/apache/openwhisk/commit/104c1e8e6cca2194da9a46a721626f4584741f23, [@style95](https://github.com/style95))\n- Provide action limit configuration for each namespace (https://github.com/apache/openwhisk/commit/61ca4c8fe39f2b47b84c20a8114f261cd12820d7, [@upgle](https://github.com/upgle))\n- Fix typo (https://github.com/apache/openwhisk/commit/4bc4c3b0e1294479dc20fa19389917d0f630a6a6, [@style95](https://github.com/style95))\n- schedule actions to run at least 3 times a week (https://github.com/apache/openwhisk/commit/40944e6eef40c39b7e547cd9b7251b72f53fc1ed, [@dgrove-oss](https://github.com/dgrove-oss))\n- fixes to use different secrets, show the results in the log (https://github.com/apache/openwhisk/commit/21c9a6363fe14e581ec67da281e1762db02ae557, [@msciabarra](https://github.com/msciabarra))\n- GitHub action (https://github.com/apache/openwhisk/commit/f717619c8c25b54294cfa195732732e85c5d9431, [@msciabarra](https://github.com/msciabarra))\n- drop travis from required checks to merge a PR (https://github.com/apache/openwhisk/commit/a7ed8b5ef080036b3e7dc18b966f096effd20494, [@dgrove-oss](https://github.com/dgrove-oss))\n- Fix missing attachment stuck actions (https://github.com/apache/openwhisk/commit/daeadbf11fb46d0f4471fef8e56c1741e1249ab8, [@bdoyle0182](https://github.com/bdoyle0182))\n- Add cors headers to components' server admin routes (https://github.com/apache/openwhisk/commit/85788875d597f4225909479cefbdf403c0d19ff2, [@bdoyle0182](https://github.com/bdoyle0182))\n- Handle container cleanup from ActivationClient shutdown gracefully (https://github.com/apache/openwhisk/commit/44791f361d1492e985e9f1bcf3616253c77ed39d, [@style95](https://github.com/style95))\n- Add scheduler overprovision for new actions before namespace throttling (https://github.com/apache/openwhisk/commit/077fb6d24f0132e7755ea47d7ee9b35f0966daf3, [@bdoyle0182](https://github.com/bdoyle0182))\n- Make the test stable (https://github.com/apache/openwhisk/commit/0f4b0c220e050d408a650a8c5a90fbc54b52768a, [@style95](https://github.com/style95))\n- optimize scheduling decision when there are stale activations (https://github.com/apache/openwhisk/commit/07c920249d0a0db5fe3bc603add73e410f40dddd, [@bdoyle0182](https://github.com/bdoyle0182))\n- Add zero downtime deployment (https://github.com/apache/openwhisk/commit/651a2e95726f69fb8403c49ce909371521e8986f, [@style95](https://github.com/style95))\n- Openwhisk action invocation flow diagram (https://github.com/apache/openwhisk/commit/74ca61c8512f43981b6eaca158edc3749ba2513f, [@Rajiv2605](https://github.com/Rajiv2605))\n- Prevent cycle in the QueueManager (https://github.com/apache/openwhisk/commit/ef725a653ab112391f79c274d8e3dcfb915d59a3, [@style95](https://github.com/style95))\n- Delete ETCD data first when disabling the invoker (https://github.com/apache/openwhisk/commit/145971b8faedad71a7e07fd398528ff563bbaae5, [@style95](https://github.com/style95))\n- Clean Up Etcd Worker Actor (https://github.com/apache/openwhisk/commit/236ca5e4b894e4cc626f685c1d0eba5c3e6077ec, [@bdoyle0182](https://github.com/bdoyle0182))\n- Fix minor ansible typos (https://github.com/apache/openwhisk/commit/8d60463db6ee59b77d1f9034755cb1946ed49fdb, [@hunhoffe](https://github.com/hunhoffe))\n- change default warmed container keeping count (https://github.com/apache/openwhisk/commit/ff7d578bc6ffe0bb392ca9d741f585ac68a025db, [@bdoyle0182](https://github.com/bdoyle0182))\n- Add Function Cache Refresh If Invoker Is Running Container For Function (https://github.com/apache/openwhisk/commit/6effb15ce6f415f8424c0094d67e64078f5c64c1, [@bdoyle0182](https://github.com/bdoyle0182))\n- Fix Orphaned Container Edge Case In Paused State of Container Proxy (https://github.com/apache/openwhisk/commit/625c5f2ef64c9cc50da568998199ff623ad01f41, [@bdoyle0182](https://github.com/bdoyle0182))\n- add container and creation ids to logs when queue attempts to stop for better debugging (https://github.com/apache/openwhisk/commit/a1639f0e4d7270c9a230190ac26acb61413b6bbb, [@bdoyle0182](https://github.com/bdoyle0182))\n- Add note on concurrency limit config (https://github.com/apache/openwhisk/commit/c209a65322e89bdcd85c028192b905a5f2670211, [@davidfrickert](https://github.com/davidfrickert))\n- Prepare to integrate new scheduler into apache/openwhisk-deploy-kube (https://github.com/apache/openwhisk/commit/347ff1f3b737d00194ddeda2fb6fefe79c36ec48, [@hunhoffe](https://github.com/hunhoffe))\n- add config to fail async scheduler throttles as whisk errors (https://github.com/apache/openwhisk/commit/20a7b1c6ad3c57b583484c1fc12a069da42ad847, [@bdoyle0182](https://github.com/bdoyle0182))\n- add error handling to container manager when invoker query fails (https://github.com/apache/openwhisk/commit/138f3d9022610b9d00078db5226e3fd4ec67d64a, [@bdoyle0182](https://github.com/bdoyle0182))\n- Revert etcd client version (https://github.com/apache/openwhisk/commit/740c907b5e99b3210a46e936370d703911201862, [@style95](https://github.com/style95))\n- Skip scheduling for empty cold creation. (https://github.com/apache/openwhisk/commit/9be427c72680035e852496440a5a8ce4113424c4, [@style95](https://github.com/style95))\n- use openwhisk-client-js 3.21.7 (https://github.com/apache/openwhisk/commit/3354314ac8c6b9c6ab2d9f584c8916afc64360a4, [@dgrove-oss](https://github.com/dgrove-oss))\n- add config to mask docker run args when logging (https://github.com/apache/openwhisk/commit/6605b5f9187adc84a298a290bfdee914d6c2fe4c, [@bdoyle0182](https://github.com/bdoyle0182))\n- bump java etcd client to 0.0.21 (https://github.com/apache/openwhisk/commit/40077664ab3011e7a333b0197fa9a285502a1d81, [@bdoyle0182](https://github.com/bdoyle0182))\n- Exclude warmed containers in disabled invokers. (https://github.com/apache/openwhisk/commit/e36b2d8c0cd3ef362ea0f6220d9af0371acd10c2, [@style95](https://github.com/style95))\n- Add document for support array result (https://github.com/apache/openwhisk/commit/7de8bad25fbad49b2613c70b12a3abe9b24c857e, [@ningyougang](https://github.com/ningyougang))\n- Add fake clock for test code (https://github.com/apache/openwhisk/commit/9005a083cb58735031f36262bb93f580044cad9e, [@upgle](https://github.com/upgle))\n- Support backward compatibility for runtime's return type (https://github.com/apache/openwhisk/commit/1a0f1ce74446a30cdb8d06c7526d7405bfe55909, [@upgle](https://github.com/upgle))\n- Go to the NamespaceThrottled state rather than Flushing state. (https://github.com/apache/openwhisk/commit/8fd21565a293eb548e6aceddb925cec49bbb6b03, [@style95](https://github.com/style95))\n- Adjust the default configurations. (https://github.com/apache/openwhisk/commit/cf127f9cd3fab2102a4797faa6ab044a39d82bd2, [@style95](https://github.com/style95))\n- Revert cycle handling. (https://github.com/apache/openwhisk/commit/2683ed1e6f35da5d7f7c586d8f1a14ba11c7d408, [@style95](https://github.com/style95))\n- Support array result for common action and sequence action (https://github.com/apache/openwhisk/commit/62b8a50d447c066a4e579ed1d8c85eef02e37e41, [@ningyougang](https://github.com/ningyougang))\n- chore: remove duplicate entry from .gitignore (https://github.com/apache/openwhisk/commit/d92485ce7743931cae48ef8fa89da3c10e54fcf4, [@jbampton](https://github.com/jbampton))\n- poem for max action container concurrency (https://github.com/apache/openwhisk/commit/a3287924e87cc4cba3cdf878cefbbfbe9c408b50, [@bdoyle0182](https://github.com/bdoyle0182))\n- Add missing configurations. (https://github.com/apache/openwhisk/commit/3e5b8b2949bfca87cad417badb2e2a00124d4de8, [@style95](https://github.com/style95))\n- This is to make sure activations for a shared action run in an invocation namespace. (https://github.com/apache/openwhisk/commit/44379b87821e8dcec360fc9b465c3977d07b0807, [@style95](https://github.com/style95))\n- Bumping to gradle 6 (https://github.com/apache/openwhisk/commit/f29981a2f9961765067340f5ce15a6a616ef9b31, [@upgle](https://github.com/upgle))\n- [Proposal] POEM: provide array result for all runtime images (https://github.com/apache/openwhisk/commit/3014b870005a11902fdc70cbe88b10cab8ef67f8, [@ningyougang](https://github.com/ningyougang))\n- Do not put data to ETCD when no date is changed. (https://github.com/apache/openwhisk/commit/c507069d1b3983bff20c3d9e306cd3694e6c6a3c, [@style95](https://github.com/style95))\n- Adjust error for container creation. (https://github.com/apache/openwhisk/commit/491256530c980fda6457816049db0ec9f2007330, [@style95](https://github.com/style95))\n- Change the value of pause-grace for new scheduler (https://github.com/apache/openwhisk/commit/f915acc659b71029f0c74fd848e366d923dbb2d4, [@JesseStutler](https://github.com/JesseStutler))\n- Make ElasticSearch ports configurable. (https://github.com/apache/openwhisk/commit/8d0a9b2bb7bdbf01ef5f75c70d64cd2653a793cb, [@style95](https://github.com/style95))\n- Update wrong error message \"action does not exist\". (https://github.com/apache/openwhisk/commit/9bc5e043444d115cec0d79ac47d7002baa522924, [@style95](https://github.com/style95))\n- Dedicated Invokers (https://github.com/apache/openwhisk/commit/8c140e0d5baec8e760832ca98d8bcc66664a0146, [@style95](https://github.com/style95))\n- Add tags to invokers. (https://github.com/apache/openwhisk/commit/d55d8fe86332c9996ccbb421233f60da2c70192f, [@style95](https://github.com/style95))\n- Support graceful shutdown. (https://github.com/apache/openwhisk/commit/a03507ceeb2c4648c875ed68273de1448ec0d074, [@style95](https://github.com/style95))\n- Add a documentation for warmed containers configurations. (https://github.com/apache/openwhisk/commit/1fbfd6bcb3d65845a328382641c1db7182763ced, [@style95](https://github.com/style95))\n- Retry on any errors. (https://github.com/apache/openwhisk/commit/4a69be20cda806680d181666c8126333b31c90ca, [@style95](https://github.com/style95))\n- Increase the payload limit. (https://github.com/apache/openwhisk/commit/53b23524e7b8d6c44a2cf103cda1d577d8d2144a, [@style95](https://github.com/style95))\n- Add retry to store activations. (https://github.com/apache/openwhisk/commit/814b7fabafa21d5580271149407f91af432953aa, [@style95](https://github.com/style95))\n- Upgrade the Kubernetes client version. (https://github.com/apache/openwhisk/commit/1a8364dcb7e1b24cb79e463a9f54d82cefad832f, [@style95](https://github.com/style95))\n- Forward header from a trigger to actions. (https://github.com/apache/openwhisk/commit/7c5a5d6964a62deb931445684bab0b0524a9ad64, [@style95](https://github.com/style95))\n- Fail all activations when it fails to pull a blackbox image. (https://github.com/apache/openwhisk/commit/f03cbcc4ab6ff2d041b5e83b243b48afa4c81381, [@style95](https://github.com/style95))\n- Fix scheduler inProgressDuration parsing (https://github.com/apache/openwhisk/commit/4016aa803ee84b849db8a907cbd17a41cc374e7a, [@hunhoffe](https://github.com/hunhoffe))\n- [Scheduler Enhancement] Increase the retention timeout for the blackbox action. (https://github.com/apache/openwhisk/commit/8d31e96e1a321987f9f5c3b7289ba83bf4eebff6, [@style95](https://github.com/style95))\n- Make proxy timeouts configurable. (https://github.com/apache/openwhisk/commit/a7fecfb6939b1b32fec84ccac7291a838b2d61c7, [@style95](https://github.com/style95))\n- Add test cases to make sure an invoker properly boots up in terms of ETCD keys. (https://github.com/apache/openwhisk/commit/2363295909de5c872e61a6c9a721d80653d1f2df, [@style95](https://github.com/style95))\n- Consider binding action when creating or recovering queue. (https://github.com/apache/openwhisk/commit/c66486e7038e1d334b9b50aff31ff9d00d50b566, [@style95](https://github.com/style95))\n- add support for etcd client authentication (https://github.com/apache/openwhisk/commit/639c4a914bd0b7f31509349c3c8b3566e027dab3, [@bdoyle0182](https://github.com/bdoyle0182))\n- [Scheduler Enhancement] Remove deleted containers. (https://github.com/apache/openwhisk/commit/88435790cbed79db3f887c1918599cb9dead2590, [@style95](https://github.com/style95))\n- Update npm version for ow-utils docker (https://github.com/apache/openwhisk/commit/c5970a657a3070e9964cb14715b9df31819d3b75, [@hugy718](https://github.com/hugy718))\n- rework scheduler wait time metric (https://github.com/apache/openwhisk/commit/052d4d21889b38808f96958f553eeaa73947a553, [@bdoyle0182](https://github.com/bdoyle0182))\n- Remove containers gradually when disable invoker (https://github.com/apache/openwhisk/commit/80de54e1fce7c900d694ec4427ec8730a0432697, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- fix scheduling config loading wrong config (https://github.com/apache/openwhisk/commit/21b03a57019d9cc3ba80917534edbd4bb19b607a, [@bdoyle0182](https://github.com/bdoyle0182))\n- Prevent cycle sending (https://github.com/apache/openwhisk/commit/a75950a543b83ffbd7753f128855556198ee155d, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Introduce scheduling configurations. (https://github.com/apache/openwhisk/commit/4ef94d727cdc90a772946330de3837649936a5db, [@style95](https://github.com/style95))\n- Use pureconfig for invoker/scheduler's basic http auth (https://github.com/apache/openwhisk/commit/0c4aab1bd57bc4b8eb1e0e968b714e6447aeabea, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- refresh runtime versions for nodejs and golang (https://github.com/apache/openwhisk/commit/1e09049aaf32e595d13a67ead2cec583a3b1809d, [@dgrove-oss](https://github.com/dgrove-oss))\n- Fix missing keystroke in requirements (https://github.com/apache/openwhisk/commit/f71c41c04e240fdfb47e9534f367ad6c7b2c434c, [@Blucknote](https://github.com/Blucknote))\n- Add containerPool container histogram metric (https://github.com/apache/openwhisk/commit/1a6c99df42dd190d897b6806c008d5b2cdd5a4e5, [@ningyougang](https://github.com/ningyougang))\n- Add missing configuration for scheduler (https://github.com/apache/openwhisk/commit/6731f1c996b8ea5e913d8bc81ceb7de87e4e8c02, [@upgle](https://github.com/upgle))\n- fix perMin throttle config for fpc (https://github.com/apache/openwhisk/commit/4b595e00960200516023592e0135dac7550c3d7e, [@bdoyle0182](https://github.com/bdoyle0182))\n- update action container metrics subactions to action instead of namespace (https://github.com/apache/openwhisk/commit/57be429ed28d93167472ac3b1bc0f670498cddfb, [@bdoyle0182](https://github.com/bdoyle0182))\n- Add some testcases and missing ASF headers for new scheduler (https://github.com/apache/openwhisk/commit/c90e8ccbc6e49bebc4bf9d641bd8aee0085cc805, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- [Proposal] POEM: Providing action limits for each namespace (https://github.com/apache/openwhisk/commit/edc484b373f0c18a6555f50cbcff095cda09d3ca, [@upgle](https://github.com/upgle))\n- add per min throttling support to fpc (https://github.com/apache/openwhisk/commit/0912c73a4bf00caa448d6aea726fbf255b7a187e, [@bdoyle0182](https://github.com/bdoyle0182))\n- add fpc load balancer metrics (https://github.com/apache/openwhisk/commit/426aef484d31f863c3345e321f7f39ee4c23b88a, [@bdoyle0182](https://github.com/bdoyle0182))\n- Take revision into consideration when choose warm container (https://github.com/apache/openwhisk/commit/7fdc246137162e937e54435e1f27d8d95e8e89d5, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Fix wrong returned type when reschedule activation msg (https://github.com/apache/openwhisk/commit/23be3351d3db6d62d14ed69b85afdc71e7ffb4a4, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Use a template for swagger code generating (https://github.com/apache/openwhisk/commit/d88a7d2be8399bea7e55910ed225777d90d8fb9f, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Use testcontainers to test MongoDB stuff (https://github.com/apache/openwhisk/commit/674704eab8aa56d86b13b452e627511f22c3ca06, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Replace kafka.RecordMetadata with a common ResultMetadata (https://github.com/apache/openwhisk/commit/cbdcfe574984b6509335da64da86f80d592aaa1e, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- send old version memoryQueue's stale activation to queueManager when update action (https://github.com/apache/openwhisk/commit/37f1f9918906ee938ec30e4442f1b9910d5c5c5d, [@ningyougang](https://github.com/ningyougang))\n- Update Ansible scheduler instructions (https://github.com/apache/openwhisk/commit/33cfb36a326f4cdc621cdd33d4b4265e3cb090cb, [@hunhoffe](https://github.com/hunhoffe))\n- Fix path error (https://github.com/apache/openwhisk/commit/9a39c10cbf820265b99c3135193ee5709248bbfa, [@hunhoffe](https://github.com/hunhoffe))\n- Adjust the keeping duration. (https://github.com/apache/openwhisk/commit/3e3414ced7fb6e6d85b95f0f9cdd43a0a68da4fd, [@style95](https://github.com/style95))\n- Fix new scheduler error (https://github.com/apache/openwhisk/commit/b0a88b5b523b1867038b0d2615fc2cf7b4951972, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Update ansible with new scheduler instructions (https://github.com/apache/openwhisk/commit/829e734c759e8cf188e30c22d7f96f69ebfb1a97, [@hunhoffe](https://github.com/hunhoffe))\n- add enable/disable invoker support to old scheduler (https://github.com/apache/openwhisk/commit/3b6d07a18cec3e92ceb5c54558464d1bfdbc0f82, [@bdoyle0182](https://github.com/bdoyle0182))\n- [New Scheduler] Run scheduler (https://github.com/apache/openwhisk/commit/5332e6de625c4df9b717e067a5369ee446e2fda1, [@style95](https://github.com/style95))\n- add system config options for success / failure levels to write blocking / non-blocking activations to db (https://github.com/apache/openwhisk/commit/8be150577a12c54867d99f0dd82a74e9647863c0, [@bdoyle0182](https://github.com/bdoyle0182))\n- [New Scheduler] Implement FPCInvokerReactive (https://github.com/apache/openwhisk/commit/e172168fc5a55ba0c8443adbc91629291a1ca321, [@ningyougang](https://github.com/ningyougang))\n- Update main method of the scheduler. (https://github.com/apache/openwhisk/commit/d9394f4e04f68359985df5654f7e0f11686b6741, [@style95](https://github.com/style95))\n- Add FPC Load Balancer (https://github.com/apache/openwhisk/commit/b1ccbec1fd0b88d5277c8ca601f2b257b26c19da, [@style95](https://github.com/style95))\n- Fix links in Issue and PR Template (https://github.com/apache/openwhisk/commit/96330437d63b5291a020d8a2ba1b401f2cc84ab5, [@klcodanr](https://github.com/klcodanr))\n- docker creds in build-specific directory (https://github.com/apache/openwhisk/commit/af11418df942e8d2cdc4c4ce933645fa723e9e45, [@dgrove-oss](https://github.com/dgrove-oss))\n- Update README.md (https://github.com/apache/openwhisk/commit/285f9d4afcc429d63baa1943a69bb64097b059dd, [@dgrove-oss](https://github.com/dgrove-oss))\n- Reflect decision to drop support for runtime-ballerina (https://github.com/apache/openwhisk/commit/ac332ea7ce756fdb95284bc48b2b335c5ee92684, [@dgrove-oss](https://github.com/dgrove-oss))\n- finish/failed uncompleted transaction (https://github.com/apache/openwhisk/commit/3753daf0e0c3d237c05ff4668e2f4c5f45d7dcb9, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- remove swift:4.2 (https://github.com/apache/openwhisk/commit/eacbe1a3d63539bcc9aa3a6c57cdcabc302b92cd, [@dgrove-oss](https://github.com/dgrove-oss))\n- [New Scheduler] Add memory queue for the new scheduler (https://github.com/apache/openwhisk/commit/cf36299d5ee45aa014ec84326d3a69f5b2df446c, [@style95](https://github.com/style95))\n- Accept non-standard status codes. (https://github.com/apache/openwhisk/commit/7e1caaa42fb485cefea799ed6835adada1461d7f, [@rabbah](https://github.com/rabbah))\n- upgrade to nginx 1.21.1 (https://github.com/apache/openwhisk/commit/cbe53b199a36c5c2a288e249aa84690e2066abdc, [@dgrove-oss](https://github.com/dgrove-oss))\n- minor version bump of azure-storage-blob to fix builds (https://github.com/apache/openwhisk/commit/3e6138d088fbd502a69c31314ad7c0089c5f5283, [@dgrove-oss](https://github.com/dgrove-oss))\n- update name for Python 3 image (https://github.com/apache/openwhisk/commit/209dc44cc8d8fd46e5ac56a869ca7dd58ad9f616, [@dgrove-oss](https://github.com/dgrove-oss))\n- remove previously deprecated nodejs:10 and go:1.11 kinds (https://github.com/apache/openwhisk/commit/91e3f28cd010d5410a78e6514d630cb1f6b63cee, [@dgrove-oss](https://github.com/dgrove-oss))\n- cleanup: remove obsolete mesos configuration (https://github.com/apache/openwhisk/commit/a99b0a8acaeb03a7e65965549bbdd12a9c13e78f, [@dgrove-oss](https://github.com/dgrove-oss))\n- don't log a uuid of the username portion of basic auth info (https://github.com/apache/openwhisk/commit/4e25ada99c00875564c4e5aa51677b20d67afd66, [@dgrove-oss](https://github.com/dgrove-oss))\n- modules.md: travis-ci.org -> travis-ci.com migration (https://github.com/apache/openwhisk/commit/bf62f740057f5210ff05582d119fd692fb6c6341, [@dgrove-oss](https://github.com/dgrove-oss))\n- Disable StandaloneKCFTests (https://github.com/apache/openwhisk/commit/67a80c3378e53d9bf967e3f0c3dcf10d55aa6ee7, [@dgrove-oss](https://github.com/dgrove-oss))\n- [New Scheduler] Manage memory queues in scheduler (https://github.com/apache/openwhisk/commit/0cdfdb3ecb20fbff11e401c34143fe0e8ff61f83, [@KeonHee](https://github.com/KeonHee))\n- Remove Mesos container factory. (https://github.com/apache/openwhisk/commit/dc7c66678308f72d219464ca723a7a997c66568a, [@rabbah](https://github.com/rabbah))\n- Fix ansible README (https://github.com/apache/openwhisk/commit/11a1f33c4e5f2bc7c97584fc889c8dcaaf5e1a43, [@ryutoyasugi](https://github.com/ryutoyasugi))\n- Docs update for local dev (https://github.com/apache/openwhisk/commit/afe39860eb456b8d32bdd68ed724967aefca72c5, [@ddragosd](https://github.com/ddragosd))\n- update to openwhisk-client-js 3.21.4 (https://github.com/apache/openwhisk/commit/58b218340b1070e153ba6ed8cf7a51c2a1a9b1de, [@dgrove-oss](https://github.com/dgrove-oss))\n- update modules.md to move deploy-mesos and external-resources to inactive (https://github.com/apache/openwhisk/commit/184a926a93c340688558eb86c9c49d4edb1dbfdf, [@dgrove-oss](https://github.com/dgrove-oss))\n- Fix deprecated error (https://github.com/apache/openwhisk/commit/20417de0e73b8c15f162c20efb66d2df027dcfa4, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Implement MongoDBArtifactStore (https://github.com/apache/openwhisk/commit/927962692e45fc7276ccb7902fd3a111daa54885, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Fix build error (https://github.com/apache/openwhisk/commit/e65f899ca5ce95d1aadd8f1b244a2cb80bbd35f4, [@ningyougang](https://github.com/ningyougang))\n- Implement ActivationClientProxy (https://github.com/apache/openwhisk/commit/99fb2c59e264d820d6b95a6319b7882f19aead4d, [@ningyougang](https://github.com/ningyougang))\n- [New Scheduler]Add CreationJobManager (https://github.com/apache/openwhisk/commit/71585f130df31b74733ef6c4652a6779aaa679ab, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- #5060: Upgrade to Akka 2.6.12 (https://github.com/apache/openwhisk/commit/7791e4b97c0918205bc957f59f859af5ad51edcd, [@vrann](https://github.com/vrann))\n- Update the notice year (https://github.com/apache/openwhisk/commit/ecb2a980659f28d0adbd9ef837afaf4cb2b695bf, [@style95](https://github.com/style95))\n- [New Scheduler] Implement FunctionPullingContainerProxy (https://github.com/apache/openwhisk/commit/0b2d2ab0103b92b57bda64a37277788fa89171f0, [@ningyougang](https://github.com/ningyougang))\n- #5120: Unit Tests failing due to testcontainers (https://github.com/apache/openwhisk/commit/4ec5d966e5b80babdce35effe0ca13729fa056a8, [@vrann](https://github.com/vrann))\n- Document for prewarmed container (https://github.com/apache/openwhisk/commit/661ddc5e3657fe9b9be21f18e5ef9bafe7ba7aec, [@ningyougang](https://github.com/ningyougang))\n- [New Scheduler] Add container message consumer (https://github.com/apache/openwhisk/commit/b818c3b3e8bd3fa9ac7742d1b8c051ec09b76ae2, [@upgle](https://github.com/upgle))\n- [New Scheduler] Manage container creation (https://github.com/apache/openwhisk/commit/f1829e1160d0aca51fc3ff93a77621c488b148ad, [@KeonHee](https://github.com/KeonHee))\n- refresh module list (https://github.com/apache/openwhisk/commit/9c445f372f4e504acdd97fa20d046692fe61294d, [@dgrove-oss](https://github.com/dgrove-oss))\n- increase default totalWait when getting an activation (https://github.com/apache/openwhisk/commit/1a0bf9fe027665b839d11f81b9ee7afe11a67ad3, [@dgrove-oss](https://github.com/dgrove-oss))\n- prefetch openwhisk/example to avoid timeout during test execution (https://github.com/apache/openwhisk/commit/bb7192e90a046c5aaad9785fdd4ec6591ab78909, [@dgrove-oss](https://github.com/dgrove-oss))\n- deprecate nodejs:10 kind (https://github.com/apache/openwhisk/commit/64cd96438374a5ad0e71582aeaae26490f8cdd22, [@dgrove-oss](https://github.com/dgrove-oss))\n- [New Scheduler] Implement FunctionPullingContainerPool (https://github.com/apache/openwhisk/commit/3802374d58d87fc6a95477929fc67269d6dcfe2c, [@ningyougang](https://github.com/ningyougang))\n- Add prefix for topics (https://github.com/apache/openwhisk/commit/aa7e6e2af196ac017ae4b9ea36656bec868a9931, [@ningyougang](https://github.com/ningyougang))\n- [New Scheduler]Implement PFCInvokerServer (https://github.com/apache/openwhisk/commit/e036fc9823b8e015a88e1a44d3f282698ae62fe7, [@ningyougang](https://github.com/ningyougang))\n- attempt to hack around docker rate limiting during tests (https://github.com/apache/openwhisk/commit/87c8a98869399fb0842eb81638ed0e04d12c3e6b, [@dgrove-oss](https://github.com/dgrove-oss))\n- change default nodejs kind from nodejs:10 to nodejs:14 (https://github.com/apache/openwhisk/commit/8bbcd517aac827d073b40b6c55a1e1645272ad68, [@dgrove-oss](https://github.com/dgrove-oss))\n- Make kafka version configurable (https://github.com/apache/openwhisk/commit/df1970b92d8422af29676d81f0d0e59a96bb55a3, [@ningyougang](https://github.com/ningyougang))\n- update default python3 image name (https://github.com/apache/openwhisk/commit/f7ec9e30d2de3f0c3252e32b300d4aa7412b15bf, [@dgrove-oss](https://github.com/dgrove-oss))\n- Updated Kamon bundle dependencies to 2.1.12 version (https://github.com/apache/openwhisk/commit/a201e02bfd0949f40fd12cf0b8c94b9c17956def, [@joni-jones](https://github.com/joni-jones))\n- [New Scheduler] Add ActivationService (https://github.com/apache/openwhisk/commit/cd6fded8a6836756cbfbe4159064c85683b64cd7, [@upgle](https://github.com/upgle))\n- Quote ansible default value. (https://github.com/apache/openwhisk/commit/6e5850f52dd965e6fafaccb39d28c37854571463, [@rabbah](https://github.com/rabbah))\n- Fixed link for Ansible (https://github.com/apache/openwhisk/commit/d741c8767dac386a4cf9efd64a37591e554b02f5, [@ishaanthakur](https://github.com/ishaanthakur))\n- Implement InvokerHealthyManager (https://github.com/apache/openwhisk/commit/59b67fe96f44e573f3348afed966a1cdaf80ddf2, [@ningyougang](https://github.com/ningyougang))\n- [New Scheduler] Add DataManagementService (https://github.com/apache/openwhisk/commit/ecb15098caded058ddb6976c630f5b6dcd656177, [@style95](https://github.com/style95))\n- fix: update Homebrew commands (https://github.com/apache/openwhisk/commit/625fc5b7593360bc01e74147c02ce5f1461b5cd3, [@jbampton](https://github.com/jbampton))\n- Change up travis URL. (https://github.com/apache/openwhisk/commit/659b76207e99b842874a83b2abf1bd18fc208715, [@rabbah](https://github.com/rabbah))\n- configure more github properties via asf.yaml (https://github.com/apache/openwhisk/commit/2ce44b394d01627f93c1d6efcbfeec77340bcd72, [@dgrove-oss](https://github.com/dgrove-oss))\n- fix: Python 3 urlencode (https://github.com/apache/openwhisk/commit/aa023d839f509e0557e7e1394830dd4377221bbc, [@jbampton](https://github.com/jbampton))\n- [New Scheduler] Add container counter (https://github.com/apache/openwhisk/commit/f9e469e50aac5a345a010e2bf1f71596f1b101fc, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- [New Scheduler] Implement KeepAliveService (https://github.com/apache/openwhisk/commit/e05aa44b0cab519c82cf84a8171671a21d779562, [@KeonHee](https://github.com/KeonHee))\n- chore: fix spelling (https://github.com/apache/openwhisk/commit/cc9bc49b75b5bed0e906cbf68d3c17ce84958a4b, [@jbampton](https://github.com/jbampton))\n- Make cache expiration time configurable (https://github.com/apache/openwhisk/commit/00642f737708cdbb82bd88cb74c4bdb109f4b6b0, [@fe-lix-](https://github.com/fe-lix-))\n- Add php:8.0 kind (https://github.com/apache/openwhisk/commit/d8cf17247bbcd8c1250873254d0c213fa28116ce, [@akrabat](https://github.com/akrabat))\n- [New Scheduler] Add a centralized watcher for etcd data (https://github.com/apache/openwhisk/commit/ed58b233d2848cad2d452a4d71b704764fb001b6, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- [New Scheduler] CI for testing related new scheduler (https://github.com/apache/openwhisk/commit/4a13303fae4d9750da6662bb39b3ec92d6ccf56c, [@KeonHee](https://github.com/KeonHee))\n- update unicode test dat files for runtime language levels (https://github.com/apache/openwhisk/commit/4d88ca782795b995591b4f3c0e44d61b76005232, [@dgrove-oss](https://github.com/dgrove-oss))\n- remove deprecated python:2 kind (https://github.com/apache/openwhisk/commit/3c80842d09409d0c16b8fe3e732fd9de4980ab42, [@dgrove-oss](https://github.com/dgrove-oss))\n- [New Scheduler] Etcd installation & Implements EtcdClient (https://github.com/apache/openwhisk/commit/5eda22171a238e933121b3918c5940e37fb009c5, [@KeonHee](https://github.com/KeonHee))\n- Implement FCPSchedulerServer (https://github.com/apache/openwhisk/commit/1753946ac16b91b2d2a3fc55ab215b14e71c2b39, [@ningyougang](https://github.com/ningyougang))\n- format: format the invoker's Dockerfile (https://github.com/apache/openwhisk/commit/faae555e583fb8a1b83e2b30840bf0610cbde00b, [@ZinuoCai](https://github.com/ZinuoCai))\n- Don't create prewarm container when used memory reaches the limit (https://github.com/apache/openwhisk/commit/c6e32b12ba2269e2aa4612ccb549764b9ffc3766, [@ningyougang](https://github.com/ningyougang))\n- [New Scheduler]Implement FPCEntitlementProvider (https://github.com/apache/openwhisk/commit/efdbd6049a849eb432e1c3fffc56bdb3fd344eaf, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Fix wsk action create command in Rust example (https://github.com/apache/openwhisk/commit/212d809303c984d55839090299f98cf58aed7378, [@kingledion](https://github.com/kingledion))\n- Fixes bug in invoker supervision on startup. (https://github.com/apache/openwhisk/commit/66868205b52ee65f28756038c44d8df5b96d2bcc, [@rabbah](https://github.com/rabbah))\n- Python 3 fixes. (https://github.com/apache/openwhisk/commit/9d08977cc6bc831e5e9ea9a8d869d2f9c37d3efa, [@rabbah](https://github.com/rabbah))\n- Use focal release for travis ci (https://github.com/apache/openwhisk/commit/5a847e3c71c27707776e6d9d135bbb78e8275e5a, [@jiangpengcheng](https://github.com/jiangpengcheng))\n- [New Scheduler] Add duration checker (https://github.com/apache/openwhisk/commit/a6ad9e418e605894c5e96e5601c3e1b8ded4166a, [@style95](https://github.com/style95))\n- take prewarmed container's memory as used memory (https://github.com/apache/openwhisk/commit/b0baa7b3c2aeff56fda0b826749e25df7067242a, [@ningyougang](https://github.com/ningyougang))\n- Fix heisenbug (https://github.com/apache/openwhisk/commit/2d0c8a72711cf20da4aedb8ada68d62774c0eca9, [@style95](https://github.com/style95))\n- Migrate the Travis configuration to travis-ci.com (https://github.com/apache/openwhisk/commit/a2025382fa4dbd8ce448b037b14a54d818a224ca, [@style95](https://github.com/style95))\n- Copy jmx files instead of moving to support k8s (https://github.com/apache/openwhisk/commit/6254477d5f95ee8d693e16daf52e9b1938f87b59, [@upgle](https://github.com/upgle))\n- Make runtime delete timeout configurable (https://github.com/apache/openwhisk/commit/4babe39fd2dbcc900ccedb5a5e9561d301361205, [@ningyougang](https://github.com/ningyougang))\n- Reset / Overwrite invokerId for unique name in zookeeper manually (https://github.com/apache/openwhisk/commit/526f0119ef9e89336b76c483e32c8dad75bfcdb4, [@bdoyle0182](https://github.com/bdoyle0182))\n- Hide version in fallback activation's path (https://github.com/apache/openwhisk/commit/12ca4e307e3b095a266352b7b12cf5ddaeb44577, [@upgle](https://github.com/upgle))\n- [New Scheduler] Initial commit for the scheduler component (https://github.com/apache/openwhisk/commit/7b99af975eb77fa00ac71ecf3f0c27e74a3ca8b4, [@style95](https://github.com/style95))\n- bump openwhisk-client-js to 3.21.3 (https://github.com/apache/openwhisk/commit/cb1645052dab33d18e5d0c059df6d1ef7ce3a6de, [@dgrove-oss](https://github.com/dgrove-oss))\n- re-fix: fix: add new Windows docker.exe location (https://github.com/apache/openwhisk/commit/6feda87956c0043339f59063b341d5afdeff632a, [@shazron](https://github.com/shazron))\n- add swift:5.3 kind and change default from swift:4.2 to swift:5.3 (https://github.com/apache/openwhisk/commit/f18e9d5647360eb8401fdb7f9c2605a27369c776, [@dgrove-oss](https://github.com/dgrove-oss))\n\n## Apache 1.0.0\n### Branch: [1.0.0](https://github.com/apache/openwhisk/tree/1.0.0)\n### Notable changes\n- Improvements to parameter encryption to support per-namespace keys. ([#4855](https://github.com/apache/openwhisk/pull/4855), [@rabbah](https://github.com/rabbah))\n- Use latest code if action's revision is mismatched. ([#4954](https://github.com/apache/openwhisk/pull/4954), [@upgle](https://github.com/upgle))\n- Do not delete previous annotation and support delete annotation via CLI. ([#4940](https://github.com/apache/openwhisk/pull/4940), [@ningyougang](https://github.com/ningyougang))\n- Prewarm eviction variance. ([#4916](https://github.com/apache/openwhisk/pull/4916), [@tysonnorris](https://github.com/tysonnorris))\n- Allow to get activation list by a binding package name. ([#4919](https://github.com/apache/openwhisk/pull/4919), [@upgle](https://github.com/upgle))\n- Allow parent/child transaction ids. ([#4819](https://github.com/apache/openwhisk/pull/4819), [@upgle](https://github.com/upgle))\n- Adjust prewarm container dynamically. ([#4871](https://github.com/apache/openwhisk/pull/4871), [@ningyougang](https://github.com/ningyougang))\n- Add NodeJS 14 runtime. ([#4902](https://github.com/apache/openwhisk/pull/4902), [@rabbah](https://github.com/rabbah))\n- Create AES128 and AES256 encryption for parameters. ([#4756](https://github.com/apache/openwhisk/pull/4756), [@mcdan](https://github.com/mcdan))\n- Implement an ElasticSearchActivationStore. ([#4724](https://github.com/apache/openwhisk/pull/4724), [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Add Swift 5.1 runtime to runtimes.json. ([#4803](https://github.com/apache/openwhisk/pull/4803), [@dgrove-oss](https://github.com/dgrove-oss))\n- Add volume mapping for Docker credentials. ([#4791](https://github.com/apache/openwhisk/pull/4791), [@style95](https://github.com/style95))\n- add .NET Core 3.1 runtime kind. ([#4792](https://github.com/apache/openwhisk/pull/4792), [@dgrove-oss](https://github.com/dgrove-oss))\n- Add PHP 7.4 runtime. ([#4767](https://github.com/apache/openwhisk/pull/4767), [@akrabat](https://github.com/akrabat))\n- Serialize `updated` value of entity document in response. ([#4646](https://github.com/apache/openwhisk/pull/4646), [@upgle](https://github.com/upgle))\n- Provide environment at init time. ([#4722](https://github.com/apache/openwhisk/pull/4722), [@upgle](https://github.com/upgle))\n- OpenWhisk User Events. ([#4584](https://github.com/apache/openwhisk/pull/4584), [@selfxp](https://github.com/selfxp))\n- Openwhisk in a standalone runnable jar. ([#4516](https://github.com/apache/openwhisk/pull/4516), [@chetanmeh](https://github.com/chetanmeh))\n- Update Docker client version to 18.06.3. ([#4430](https://github.com/apache/openwhisk/pull/4430), [@style95](https://github.com/style95))\n- Add `binding` annotation to record an action path not resolved. ([#4211](https://github.com/apache/openwhisk/pull/4211), [@upgle](https://github.com/upgle))\n- Add SPI for invoker. ([#4453](https://github.com/apache/openwhisk/pull/4453), [@style95](https://github.com/style95))\n- Enable CouchDB persist_path in a distributed environment as well. ([#4290](https://github.com/apache/openwhisk/pull/4290), [@style95](https://github.com/style95))\n- Feature flag to turn on/off support for provide-api-key annotation. ([#4334](https://github.com/apache/openwhisk/pull/4334), [@chetanmeh](https://github.com/chetanmeh))\n- Add annotations to inject the API key into the action context. ([#4284](https://github.com/apache/openwhisk/pull/4284), [@rabbah](https://github.com/rabbah))\n- Update CosmosDB to 2.4.2. ([#4321](https://github.com/apache/openwhisk/pull/4321), [@chetanmeh](https://github.com/chetanmeh))\n- Adding YARNContainerFactory. ([#4129](https://github.com/apache/openwhisk/pull/4129), [@SamHjelmfelt](https://github.com/SamHjelmfelt))\n- Allow persisted CouchDB directory mount. ([#4250](https://github.com/apache/openwhisk/pull/4250), [@rabbah](https://github.com/rabbah))\n- Bump ephemeral CouchDB to v2.3. ([#4202](https://github.com/apache/openwhisk/pull/4202), [@jonpspri](https://github.com/jonpspri))\n- Add Ballerina 0.990.2 runtime. ([#4239](https://github.com/apache/openwhisk/pull/4239), [@rabbah](https://github.com/rabbah))\n- Add Swift 4.2 runtime in default deployment. ([#4210](https://github.com/apache/openwhisk/pull/4210), [@csantanapr](https://github.com/csantanapr))\n- Add PHP 7.3 runtime. ([#4182](https://github.com/apache/openwhisk/pull/4182), [@akrabat](https://github.com/akrabat))\n- Add .NET Core 2.2 runtime. ([#4172](https://github.com/apache/openwhisk/pull/4172), [@shawnallen85](https://github.com/shawnallen85))\n- Updated Intellij script to start controller and invoker locally. ([#4142](https://github.com/apache/openwhisk/pull/4142), [@ddragosd](https://github.com/ddragosd))\n- Ensure ResultMessage is processed. ([#4135](https://github.com/apache/openwhisk/pull/4135), [@jiangpengcheng](https://github.com/jiangpengcheng))\n- Protect Package Bindings from containing circular references. ([#4122](https://github.com/apache/openwhisk/pull/4122), [@asteed](https://github.com/asteed))\n- Ensure, that Result-ack is sent before Completion-ack. ([#4115](https://github.com/apache/openwhisk/pull/4115), [@cbickel](https://github.com/cbickel))\n- Add NodeJS 10 runtime to default set of runtimes for ansible/vagrant. ([#4124](https://github.com/apache/openwhisk/pull/4124), [@csantanapr](https://github.com/csantanapr))\n- Enable concurrent activation processing. ([#2795](https://github.com/apache/openwhisk/pull/2795), [@tysonnorris](https://github.com/tysonnorris))\n- Rename the package from whisk to org.apache.openwhisk. ([#4073](https://github.com/apache/openwhisk/pull/4073), [@houshengbo](https://github.com/houshengbo))\n- Allow web actions from package bindings. ([#3880](https://github.com/apache/openwhisk/pull/3880), [@upgle](https://github.com/upgle))\n- Switch to Scala 2.12.7 ([#4062](https://github.com/apache/openwhisk/pull/4062), [@chetanmeh](https://github.com/chetanmeh))\n- Always return activation without logs on blocking invoke. ([#4100](https://github.com/apache/openwhisk/pull/4100), [@cbickel](https://github.com/cbickel))\n- Changes to include Go runtime. ([#4093](https://github.com/apache/openwhisk/pull/4093), [@sciabarracom](https://github.com/sciabarracom))\n- Send active-ack after log collection for non-blocking activations. ([#4041](https://github.com/apache/openwhisk/pull/4041), [@cbickel](https://github.com/cbickel))\n- Increase max-content-length to 50 MB. ([#4059](https://github.com/apache/openwhisk/pull/4059), [@chetanmeh](https://github.com/chetanmeh))\n- Using non-root user in controller. ([#3579](https://github.com/apache/openwhisk/pull/3579), [@Himavanth](https://github.com/Himavanth))\n- Customize invoker user memory for memory based load-balancing. ([#4011](https://github.com/apache/openwhisk/pull/4011), [@ningyougang](https://github.com/ningyougang))\n- Secure the invoker with SSL. ([#3968](https://github.com/apache/openwhisk/pull/3968), [@cbickel](https://github.com/cbickel))\n- Reuse a container on `applicationError`. ([#3941](https://github.com/apache/openwhisk/pull/3941), [@tysonnorris](https://github.com/tysonnorris))\n- Memory based load-balancing ([#3747](https://github.com/apache/openwhisk/pull/3747), [@cbickel](https://github.com/cbickel))\n- Activation ID in header. ([#3671](https://github.com/apache/openwhisk/pull/3671), [@style95](https://github.com/style95))\n- Treat action code as attachments. ([#3945](https://github.com/apache/openwhisk/pull/3945), [@chetanmeh](https://github.com/chetanmeh))\n- K8S: Implement invoker-node affinity and eliminate usage of kubectl. ([#3963](https://github.com/apache/openwhisk/pull/3963), [@dgrove-oss](https://github.com/dgrove-oss))\n- Add Ruby 2.5 runtime support. ([#3725](https://github.com/apache/openwhisk/pull/3725), [@remore](https://github.com/remore))\n- S3AttachmentStore. ([#3779](https://github.com/apache/openwhisk/pull/3779), [@chetanmeh](https://github.com/chetanmeh))\n- ContainerClient + Akka HTTP alternative to HttpUtils. ([#3812](https://github.com/apache/openwhisk/pull/3812), [@tysonnorris](https://github.com/tysonnorris))\n- Throttle the system based on active-ack timeouts. ([#3875](https://github.com/apache/openwhisk/pull/3875), [@markusthoemmes](https://github.com/markusthoemmes))\n- Recover image pulls by trying to run the container anyways. ([#3813](https://github.com/apache/openwhisk/pull/3813), [@markusthoemmes](https://github.com/markusthoemmes))\n- Use separate DB users for deployed components. ([#3876](https://github.com/apache/openwhisk/pull/3876), [@cbickel](https://github.com/cbickel))\n- Introduce SPI to swap authentication directives. ([#3829](https://github.com/apache/openwhisk/pull/3829), [@mhenke1](https://github.com/mhenke1))\n- ArtifactStore implementation for CosmosDB. ([#3562](https://github.com/apache/openwhisk/pull/3562), [@chetanmeh](https://github.com/chetanmeh))\n- Add support for PHP 7.2 runtime. ([#3736](https://github.com/apache/openwhisk/pull/3736), [@akrabat](https://github.com/akrabat))\n\n## Incubating 0.9.0\n### Branch: [0.9.0-incubating](https://github.com/apache/openwhisk/tree/0.9.0-incubating)\n### Notable changes\n- Initial release.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Apache OpenWhisk Community Code of Conduct\n\nPlease refer to the [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct.html).\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)\n\n# Contributing to Apache OpenWhisk\n\nAnyone can contribute to the OpenWhisk project and we welcome your contributions.\n\nThere are multiple ways to contribute: report bugs, improve the docs, and\ncontribute code, but you must follow these prerequisites and guidelines:\n\n - [Contributor License Agreement](#contributor-license-agreement)\n - [Raising issues](#raising-issues)\n - [Coding Standards](#coding-standards)\n\n### Contributor License Agreement\n\nAll contributors must sign and submit an Apache CLA (Contributor License Agreement).\n\nInstructions on how to do this can be found here:\n[http://www.apache.org/licenses/#clas](http://www.apache.org/licenses/#clas)\n\nSign the appropriate CLA and submit it to the Apache Software Foundation (ASF) secretary. You will receive a confirmation email from the ASF and be added to\nthe following list: http://people.apache.org/unlistedclas.html.  Once your name is on this list, you are done and your PR can be merged.\n\nProject committers will use this list to verify pull requests (PRs) come from contributors that have signed a CLA.\n\nWe look forward to your contributions!\n\n## Raising issues\n\nPlease raise any bug reports or enhancement requests on the respective project repository's GitHub issue tracker. Be sure to search the\nlist to see if your issue has already been raised.\n\nA good bug report is one that make it easy for us to understand what you were trying to do and what went wrong.\nProvide as much context as possible so we can try to recreate the issue.\n\nA good enhancement request comes with an explanation of what you are trying to do and how that enhancement would help you.\n\n### Discussion\n\nPlease use the project's developer email list to engage our community:\n[dev@openwhisk.apache.org](dev@openwhisk.apache.org)\n\nIn addition, we provide a \"dev\" Slack team channel for conversations at:\nhttps://openwhisk-team.slack.com/messages/dev/\n\n### Coding standards\n\nPlease ensure you follow the coding standards used throughout the existing\ncode base. Some basic rules include:\n\n - all files must have the Apache license in the header.\n - all PRs must have passing builds for all operating systems.\n - the code is correctly formatted as defined in the [Scalariform plugin properties](tools/eclipse/scala.properties). If you use IntelliJ for development this [page](https://plugins.jetbrains.com/plugin/7480-scalariform) describes the setup and configuration of the plugin.\n"
  },
  {
    "path": "CREDITS.txt",
    "content": "                                C R E D I T S\n\nThis is the file where major donations to the OpenWhisk project are listed and it\nshould be used to give appropriate visibility to those individuals, organizations\nor companies that donated resources to the effort. This file wants to be one of\nthe ways the OpenWhisk community pays back.\n\n\nCommunity Credits\n-----------------\n\nThe OpenWhisk project code was originally created, designed, developed (under an\nIBM copyright dated 2015-2016) and donated by the IBM Corporation (http://www.ibm.com/)\nto the Apache Software Foundation (ASF). The following repositories were included in\nthe initial donation:\n\n* openwhisk:\n    * Primary (core) source code repository including platform code, run books, tests and more.\n* openwhisk-catalog:\n    * Catalog of built-in system, utility, test and sample Actions, Feeds and provider integration tooling.\n* openwhisk-client-js:\n    * JavaScript (JS) client library for the OpenWhisk platform.\n* openwhisk-client-swift:\n    * Swift-based client SDK for OpenWhisk compatible with Swift 2.x and runs on iOS 9, WatchOS 2, and Darwin.\n* openwhisk-sdk-docker: \n    * SDK that shows how to create “Black box” Docker containers that can run Action (code).\n* openwhisk-client-go:\n    * API Framework written in GoLang (in-progress)\n* openwhisk-python-cli:\n    * API client written in Python.\n* openwhisk-package-pushnotifications:\n    * Push notifications to registered devices. (in-progress)\n* openwhisk-package-alarms\n    * Package that can be used to create periodic, time-based alarms\n* openwhisk-package-cloudant\n    * Package that can be used to store data to Cloudant object storage\n* openwhisk-package-twilio:\n    * Integration with Twilio. (in-progress)\n* openwhisk-package-jira:\n    * Integration with JIRA events. (in-progress)\n* openwhisk-package-rss:\n    * Integration with RSS feeds.\n* openwhisk-package-kafka:\n    * Integration with Kafka. (in-progress)\n* openwhisk-slackbot-poc:\n    * Deploy a Slackbot with the capability to run OpenWhisk actions.\n* openwhisk-wskdeploy:\n    * Utility for deploying and managing Apache OpenWhisk packages and projects.\n* openwhisk.github.io:\n    * Apache OpenWhisk website code built using Jekyll.\n* openwhisk-tutorial:\n    * Interactive tutorial framework (JavaScript) for for OpenWhisk, its CLI and packages.\n* openwhisk-vscode:\n    * Visual Studio Code extension (prototype) for authoring OpenWhisk actions inside the editor.\n* openwhisk-xcode:\n    * Collection of OpenWhisk tools for OS X implemented in Swift 3.\n* openwhisk-debugger\n    * The OpenWhisk debugger project\n* openwhisk-podspecs: \n    * CocoaPods Podspecs repository for ‘openwhisk-client-swift’ subproject.\n* openwhisk-devtools:\n    * Development tools for building and deploying Apache OpenWhisk\n* openwhisk-workshop:\n    * Workshop to help developers learn how to build serverless applications using the platform.\n* openwhisk-package-template:\n    * This is a template to be use when creating new packages for OpenWhisk.\n* openwhisk-sample-matos:\n    * Sample application with Message Hub and Object Store.\n* openwhisk-sample-slackbot:\n    * A proof-of-concept Slackbot to invoke OpenWhisk actions.\n    \nThe API Gateway code was a collaboration project jointly developed by both IBM Corporation\nand Adobe Systems Incorporated (http://www.adobe.com/) and a donation to the Apache OpenWhisk\nproject by both companies.\n\n* openwhisk-apigateway: A performant API Gateway based on Openresty and NGINX.\n\nThe OpenWhisk Composer project code was originally created, designed, developed (under an\nIBM copyright dated 2017-2018) and donated in October and November 2018\nby the IBM Corporation (http://www.ibm.com/) to the Apache Software Foundation (ASF).\nThe following repositories were included in the donation:\n\n* openwhisk-composer:\n  * A new programming model for composing cloud functions built on OpenWhisk\n* openwhisk-composer-python:\n  * A Python client library for OpenWhisk composer.\n\n\n                          The Apache OpenWhisk Community\n                           http://openwhisk.apache.org/\n"
  },
  {
    "path": "Jenkinsfile",
    "content": "#!groovy\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\ntimeout(time: 12, unit: 'HOURS') {\n\n    def port = \"444\"\n    def cert = \"domain.crt\"\n    def key = \"domain.key\"\n\n    node(\"openwhisk\") {\n        def hostName = sh(returnStdout: true, script: 'hostname').trim()\n        def domainName = hostName+\".apache.org\"\n        def home = sh(returnStdout: true, script: 'echo $HOME').trim()\n        def jobName = sh(returnStdout: true, script: 'echo $JOB_NAME').trim()\n        def jobSpace = \"${home}/jenkins-slave/workspace/${jobName}\"\n\n        lock(\"${hostName}\") {\n            sh \"mkdir -p ${jobSpace}\"\n            dir(\"${jobSpace}\") {\n                try {\n                    deleteDir()\n                    stage('Checkout') {\n                        checkout scm\n                    }\n\n                    stage('Build') {\n                        // Set up a private docker registry service, accessed by all the OpenWhisk VMs.\n                        try {\n                            sh \"docker container stop registry && docker container rm -v registry\"\n                        } catch (exp) {\n                            println(\"Unable to stop and remove the container registry.\")\n                        }\n\n                        sh \"docker run -d --restart=always --name registry -v \\\"$HOME\\\"/certs:/certs \\\n                                -e REGISTRY_HTTP_ADDR=0.0.0.0:${port} -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/${cert} \\\n                                -e REGISTRY_HTTP_TLS_KEY=/certs/${key} -p ${port}:${port} registry:2\"\n                        // Build the controller, scheduler, and invoker images.\n                        sh \"./gradlew distDocker -PdockerRegistry=${domainName}:${port}\"\n                        //Install the various modules like standalone\n                        sh \"./gradlew install\"\n                    }\n\n                    stage('Deploy Lean') {\n                        dir(\"ansible\") {\n                            // Copy the jenkins ansible configuration under the directory ansible. This can make sure the SSH is used to\n                            // access the VMs of invokers by the VM of the controller.\n                            sh '[ -f \"environments/jenkins/ansible_jenkins.cfg\" ] && cp environments/jenkins/ansible_jenkins.cfg ansible.cfg'\n                        }\n\n                        dir(\"ansible/environments/jenkins\") {\n                            sh \"cp ${hostName}.j2.ini hosts.j2.ini\"\n                        }\n\n                        dir(\"ansible/environments/jenkins/group_vars\") {\n                            sh \"cp ${hostName} all\"\n                        }\n\n                        dir(\"ansible\") {\n                            sh 'ansible-playbook -i environments/jenkins setup.yml'\n                            sh 'ansible-playbook -i environments/jenkins openwhisk.yml -e mode=clean'\n                            sh 'ansible-playbook -i environments/jenkins apigateway.yml -e mode=clean'\n                            sh 'ansible-playbook -i environments/jenkins couchdb.yml -e mode=clean'\n                            sh 'ansible-playbook -i environments/jenkins couchdb.yml'\n                            sh 'ansible-playbook -i environments/jenkins initdb.yml'\n                            sh 'ansible-playbook -i environments/jenkins wipe.yml'\n                            sh 'ansible-playbook -i environments/jenkins apigateway.yml'\n                            sh 'ansible-playbook -i environments/jenkins openwhisk.yml -e lean=true'\n                            sh 'ansible-playbook -i environments/jenkins properties.yml'\n                            sh 'ansible-playbook -i environments/jenkins routemgmt.yml'\n                            sh 'ansible-playbook -i environments/jenkins postdeploy.yml'\n                        }\n                    }\n\n                    try {\n                        stage('Test Lean Openwhisk') {\n                            sh './gradlew :tests:test --tests system.basic.WskRestBasicTests -DtestResultsDirName=test-lean-openwhisk'\n                        }\n                    } catch (exp) {\n                        println(\"Exception: \" + exp)\n                        error(exp)\n                    }\n\n                    stage('Deploy full Openwhisk') {\n                        dir(\"ansible\") {\n                            sh 'ansible-playbook -i environments/jenkins openwhisk.yml -e mode=clean'\n                            sh 'ansible-playbook -i environments/jenkins openwhisk.yml'\n                        }\n                    }\n\n                    try {\n                        stage('Test') {\n                            sh './gradlew :tests:test -DtestResultsDirName=test-openwhisk'\n                        }\n                    } catch (exp) {\n                        println(\"Exception:\" + exp)\n                    }\n\n                    try {\n                        stage('Shoot one invoker test') {\n                            def folder = \"ansible/environments/jenkins/group_vars\"\n                            def invoker1_node = sh(returnStdout: true,\n                                    script: \"grep invoker1_machine ${folder}/${hostName} | cut -d: -f2\").trim()\n                            sh \"ssh -i ${home}/secret/openwhisk_key openwhisk@${invoker1_node} 'docker stop invoker1'\"\n                            sleep time: 1, unit: 'MINUTES'\n                            sh './gradlew :tests:testShootInvoker -DtestResultsDirName=test-shoot-invoker'\n                            sh \"ssh -i ${home}/secret/openwhisk_key openwhisk@${invoker1_node} 'docker start invoker1'\"\n                        }\n                    } catch (exp) {\n                        println(\"Exception:\" + exp)\n                    }\n\n                } catch (exp) {\n                    println(\"Exception:\" + exp)\n\t\t    error(exp)\n                } finally {\n                    println(\"Executing finally block\")\n                    stage('Clean up') {\n                        dir(\"ansible\") {\n                            sh 'ansible-playbook -i environments/jenkins openwhisk.yml -e mode=clean'\n                            sh 'ansible-playbook -i environments/jenkins apigateway.yml -e mode=clean'\n                            sh 'ansible-playbook -i environments/jenkins couchdb.yml -e mode=clean'\n                        }\n                    }\n                    step([$class: 'JUnitResultArchiver', testResults: '**/test*/**/TEST-*.xml'])\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n\n========================================================================\nApache License 2.0\n========================================================================\n\nThis distribution bundles the following components, which are available under an Apache License 2.0 (https://opensource.org/licenses/Apache-2.0).\nSpray Caching 1.3.4 (io.spray:spray-caching_2.11:1.3.4 - http://spray.io/documentation/1.2.4/spray-caching/)\n  ConcurrentMapBackedCache.scala under common/scala/src/main/scala/org/apache/openwhisk/core/database/ contains implementation\n  from Spray's [[spray.caching.Cache]] and [[spray.caching.SimpleLruCache]] respectively.\n  License included at licenses/LICENSE-spray.txt, or https://github.com/spray/spray/blob/master/LICENSE\n  Copyright (C) 2011-2015 the spray project <http://spray.io>\n\nThis product bundles the files gradlew and gradlew.bat from Gradle v5.5\nwhich are distributed under the Apache License, Version 2.0.\nFor details see ./gradlew and ./gradlew.bat.\n"
  },
  {
    "path": "NOTICE.txt",
    "content": "Apache OpenWhisk\nCopyright 2016-2024 The Apache Software Foundation\n\nThis product includes software developed at\nThe Apache Software Foundation (http://www.apache.org/).\n"
  },
  {
    "path": "README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# OpenWhisk\n\n[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)\n[![Join Slack](https://img.shields.io/badge/join-slack-9B69A0.svg)](https://openwhisk-team.slack.com/)\n[![Twitter](https://img.shields.io/twitter/follow/openwhisk.svg?style=social&logo=twitter)](https://twitter.com/intent/follow?screen_name=openwhisk)\n\n[![Unit Tests](https://github.com/apache/openwhisk/actions/workflows/1-unit.yaml/badge.svg)](https://github.com/apache/openwhisk/actions/workflows/1-unit.yaml)\n[![System Tests](https://github.com/apache/openwhisk/actions/workflows/2-system.yaml/badge.svg)](https://github.com/apache/openwhisk/actions/workflows/2-system.yaml)\n[![MultiRuntime Tests](https://github.com/apache/openwhisk/actions/workflows/3-multi-runtime.yaml/badge.svg)](https://github.com/apache/openwhisk/actions/workflows/3-multi-runtime.yaml)\n[![Standalone Tests](https://github.com/apache/openwhisk/actions/workflows/4-standalone.yaml/badge.svg)](https://github.com/apache/openwhisk/actions/workflows/4-standalone.yaml)\n[![Scheduler Tests](https://github.com/apache/openwhisk/actions/workflows/5-scheduler.yaml/badge.svg)](https://github.com/apache/openwhisk/actions/workflows/5-scheduler.yaml)\n[![Performance Tests](https://github.com/apache/openwhisk/actions/workflows/6-performance.yaml/badge.svg)](https://github.com/apache/openwhisk/actions/workflows/6-performance.yaml)\n[![codecov](https://codecov.io/gh/apache/openwhisk/branch/master/graph/badge.svg)](https://codecov.io/gh/apache/openwhisk)\n\nOpenWhisk is a serverless functions platform for building cloud applications.\nOpenWhisk offers a rich programming model for creating serverless APIs from functions,\ncomposing functions into serverless workflows, and connecting events to functions using rules and triggers.\nLearn more at [http://openwhisk.apache.org](http://openwhisk.apache.org).\n\n* [Quick Start](#quick-start) (Deploy and Use OpenWhisk on your machine)\n* [Deploy to Kubernetes](#deploy-to-kubernetes) (For development and production)\n* For project contributors and Docker deployments:\n  * [Deploy to Docker for Mac](./tools/macos/README.md)\n  * [Deploy to Docker for Ubuntu](./tools/ubuntu-setup/README.md)\n* [Learn Concepts and Commands](#learn-concepts-and-commands)\n* [OpenWhisk Community and Support](#openwhisk-community-and-support)\n* [Project Repository Structure](#project-repository-structure)\n\n### Notice of Breaking Upgrade 10/17/2025\n\nApache Openwhisk has migrated to the Apache Pekko framework. The master branch as of 10/17/2025 uses Apache Pekko. This change results in a breaking change such that you must re-deploy new clusters and cutover traffic to the new cluster. All other changes should be transient to you other than instead of using Akka configuration overrides in your deployments, you would now need to update those to use the Pekko equivalent. A 3.x release branch will eventually follow this\nchange.\n\n### Quick Start\n\nThe easiest way to start using OpenWhisk is to install the \"Standalone\" OpenWhisk stack.\nThis is a full-featured OpenWhisk stack running as a Java process for convenience.\nServerless functions run within Docker containers. You will need [Docker](https://docs.docker.com/install),\n[Java](https://java.com/en/download/help/download_options.xml) and [Node.js](https://nodejs.org) available on your machine.\n\nTo get started:\n```\ngit clone https://github.com/apache/openwhisk.git\ncd openwhisk\n./gradlew core:standalone:bootRun\n```\n\n- When the OpenWhisk stack is up, it will open your browser to a functions [Playground](./docs/images/playground-ui.png),\ntypically served from http://localhost:3232. The Playground allows you create and run functions directly from your browser.\n\n- To make use of all OpenWhisk features, you will need the OpenWhisk command line tool called\n`wsk` which you can download from https://s.apache.org/openwhisk-cli-download.\nPlease refer to the [CLI configuration](./docs/cli.md) for additional details. Typically you\nconfigure the CLI for Standalone OpenWhisk as follows:\n```\nwsk property set \\\n  --apihost 'http://localhost:3233' \\\n  --auth '23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP'\n```\n\n- Standalone OpenWhisk can be configured to deploy additional capabilities when that is desirable.\nAdditional resources are available [here](./core/standalone/README.md).\n\n### Deploy to Kubernetes\n\nOpenWhisk can also be installed on a Kubernetes cluster. You can use\na managed Kubernetes cluster provisioned from a public cloud provider\n(e.g., AKS, EKS, IKS, GKE), or a cluster you manage yourself.\nAdditionally for local development, OpenWhisk is compatible with Minikube,\nand Kubernetes for Mac using the support built into Docker 18.06 (or higher).\n\nTo get started:\n\n```\ngit clone https://github.com/apache/openwhisk-deploy-kube.git\n```\n\nThen follow the instructions in the [OpenWhisk on Kubernetes README.md](https://github.com/apache/openwhisk-deploy-kube/blob/master/README.md).\n\n### Learn Concepts and Commands\n\nBrowse the [documentation](docs/) to learn more. Here are some topics you may be\ninterested in:\n\n- [System overview](docs/about.md)\n- [Getting Started](docs/README.md)\n- [Create and invoke actions](docs/actions.md)\n- [Create triggers and rules](docs/triggers_rules.md)\n- [Use and create packages](docs/packages.md)\n- [Browse and use the catalog](docs/catalog.md)\n- [OpenWhisk system details](docs/reference.md)\n- [Implementing feeds](docs/feeds.md)\n- [Developing a runtime for a new language](docs/actions-actionloop.md)\n\n### OpenWhisk Community and Support\n\nReport bugs, ask questions and request features [here on GitHub](../../issues).\n\nYou can also join the OpenWhisk Team on Slack [https://openwhisk-team.slack.com](https://openwhisk-team.slack.com) and chat with developers. To get access to our public Slack team, request an invite [https://openwhisk.apache.org/slack.html](https://openwhisk.apache.org/slack.html).\n\n### Project Repository Structure\n\nThe OpenWhisk system is built from a [number of components](docs/dev/modules.md).  The picture below groups the components by their GitHub repos. Please open issues for a component against the appropriate repo (if in doubt just open against the main openwhisk repo).\n\n![component/repo mapping](docs/images/components_to_repos.png)\n\n### What happens on an invocation?\n\nThis diagram depicts the steps which take place within Openwhisk when an action is invoked by the user:\n\n![component/repo mapping](docs/images/Openwhisk-flow-diagram.png)\n"
  },
  {
    "path": "ansible/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\nDeploying OpenWhisk using Ansible\n=========\n\n\n### Getting started\n\nIf you want to deploy OpenWhisk locally using Ansible, you first need to install Ansible on your development environment:\n\n#### Ubuntu users\n```shell script\nsudo apt-get install python-pip\nsudo pip install ansible==4.1.0\nsudo pip install jinja2==3.0.1\n```\n\n#### Docker for Mac users\n```shell script\nsudo easy_install pip\nsudo pip install ansible==4.1.0\npip install jinja2==3.0.1\n```\nDocker for Mac does not provide any official ways to meet some requirements for OpenWhisk.\nYou need to depend on the workarounds until Docker provides official methods.\n\nIf you prefer [Docker-machine](https://docs.docker.com/machine/) to [Docker for mac](https://docs.docker.com/docker-for-mac/), you can follow instructions in [docker-machine/README.md](../tools/macos/docker-machine/README.md).\n\n##### Enable Docker remote API\nThe remote Docker API is required for collecting logs using the Ansible playbook [logs.yml](logs.yml).\n\n##### Activate docker0 network (local dev only)\n\nThe OpenWhisk deployment via Ansible uses the `docker0` network interface to deploy OpenWhisk and it does not exist on Docker for Mac environment.\n\nAn expedient workaround is to add alias for `docker0` network to loopback interface.\n\n```shell script\nsudo ifconfig lo0 alias 172.17.0.1/24\n```\n\n### Using Ansible\n**Caveat:** All Ansible commands are meant to be executed from the `ansible` directory.\nThis is important because that's where `ansible.cfg` is located which contains generic settings that are needed for the remaining steps.\n\nSet the environment for the commands below by running\n```shell script\nENVIRONMENT=local  # or docker-machine or jenkins or vagrant\n```\n\nThe default environment is `local` which works for Ubuntu and\nDocker for Mac. To use the default environment, you may omit the `-i` parameter entirely. For older Mac installation using Docker Machine,\nuse `-i environments/docker-machine`.\n\nIn all instructions, replace `<openwhisk_home>` with the base directory of your OpenWhisk source tree. e.g. `openwhisk`\n\n#### Ansible with pyenv (local dev only)\n\nWhen using [pyenv](https://github.com/pyenv/pyenv) to manage your versions of python, the [ansible python interpreter](https://docs.ansible.com/ansible/latest/reference_appendices/python_3_support.html) will use your system's default python, which may have a different version.\n\nTo make sure ansible uses the same version of python which you configured, execute:\n\n```bash\necho -e \"\\nansible_python_interpreter: `which python`\\n\" >> ./environments/local/group_vars/all\n```\n\n#### Preserving configuration and log directories on reboot\nWhen using the local Ansible environment, configuration and log data is stored in `/tmp` by default. However, operating\nsystem such as Linux and Mac clean the `/tmp` directory on reboot, resulting in failures when OpenWhisk tries to start\nup again. To avoid this problem, export the `OPENWHISK_TMP_DIR` variable assigning it the path to a persistent\ndirectory before deploying OpenWhisk.\n\n#### Setup\n\nThis step should be executed once per development environment.\nIt will generate the `hosts` configuration file based on your environment settings.\n\n> This file is generated automatically for an ephemeral CouchDB instance during `setup.yml`.\n\nThe default configuration does not run multiple instances of core components (e.g., controller, invoker, kafka).\nYou may elect to enable high-availability (HA) mode by passing the Ansible option `-e mode=HA` when executing this playbook.\nThis will configure your deployment with multiple instances (e.g., two Kafka instances, and two invokers).\n\nIn addition to the host file generation, you need to configure the database for your deployment. This is done\nby modifying the file `ansible/db_local.ini` to provide the following properties.\n\n```\n[db_creds]\ndb_provider=\ndb_username=\ndb_password=\ndb_protocol=\ndb_host=\ndb_port=\n```\n\nFor convenience, you can use shell environment variables that are read by the playbook to generate the required `db_local.ini` file as shown below.\n\n```shell script\nexport OW_DB=CouchDB\nexport OW_DB_USERNAME=<your couchdb user>\nexport OW_DB_PASSWORD=<your couchdb password>\nexport OW_DB_PROTOCOL=<your couchdb protocol>\nexport OW_DB_HOST=<your couchdb host>\nexport OW_DB_PORT=<your couchdb port>\n\nansible-playbook -i environments/$ENVIRONMENT setup.yml\n```\n\n##### Use Cloudant as a datastore\n\n```shell script\nexport OW_DB=Cloudant\nexport OW_DB_USERNAME=<your cloudant user>\nexport OW_DB_PASSWORD=<your cloudant password>\nexport OW_DB_PROTOCOL=https\nexport OW_DB_HOST=<your cloudant user>.cloudant.com\nexport OW_DB_PORT=443\n\nansible-playbook -i environments/$ENVIRONMENT setup.yml\n```\n\n#### Install Prerequisites\n\n> This step is not required for local environments since all prerequisites are already installed, and therefore may be skipped.\n\nThis step needs to be done only once per target environment. It will install necessary prerequisites on all target hosts in the environment.\n\n```\nansible-playbook -i environments/$ENVIRONMENT prereq.yml\n```\n\n**Hint:** During playbook execution the `TASK [prereq : check for pip]` can show as failed. This is normal if no pip is installed. The playbook will then move on and install pip on the target machines.\n\n### [Optional] Enable the new scheduler\n\nYou can enable the new scheduler of OpenWhisk.\nIt will run one more component called \"scheduler\" and ETCD.\n\n#### Configure service providers for the scheduler\nYou can update service providers for the scheduler as follows.\n\n**common/scala/src/main/resources/reference.conf**\n\nIf you are using ElasticSearch (recommended) then replace ```NoopDurationCheckerProvider``` with ```ElasticSearchDurationCheckerProvider``` below.\n```\nwhisk.spi {\n  ArtifactStoreProvider = org.apache.openwhisk.core.database.CouchDbStoreProvider\n  ActivationStoreProvider = org.apache.openwhisk.core.database.ArtifactActivationStoreProvider\n  MessagingProvider = org.apache.openwhisk.connector.kafka.KafkaMessagingProvider\n  ContainerFactoryProvider = org.apache.openwhisk.core.containerpool.docker.DockerContainerFactoryProvider\n  LogStoreProvider = org.apache.openwhisk.core.containerpool.logging.DockerToActivationLogStoreProvider\n  LoadBalancerProvider = org.apache.openwhisk.core.loadBalancer.FPCPoolBalancer\n  EntitlementSpiProvider = org.apache.openwhisk.core.entitlement.FPCEntitlementProvider\n  AuthenticationDirectiveProvider = org.apache.openwhisk.core.controller.BasicAuthenticationDirective\n  InvokerProvider = org.apache.openwhisk.core.invoker.FPCInvokerReactive\n  InvokerServerProvider = org.apache.openwhisk.core.invoker.FPCInvokerServer\n  DurationCheckerProvider = org.apache.openwhisk.core.scheduler.queue.NoopDurationCheckerProvider\n}\n.\n.\n.\n```\n#### Configure pause grace for the scheduler\nSet the value of pause-grace to 10s by default\n\n**core/invoker/src/main/resources/application.conf**\n```\n  container-proxy {\n    timeouts {\n      # The \"unusedTimeout\" in the ContainerProxy,\n      #aka 'How long should a container sit idle until we kill it?'\n      idle-container = 10 minutes\n      pause-grace = 10 seconds\n      keeping-duration = 10 minutes\n    }\n  .\n  .\n  .\n```\n\n#### Disable the scheduler\n- You can disable the scheduler by configuring `scheduler_enable`.\n- The scheduler is enabled by default.\n\n**ansible/environments/local/group_vars/all**\n```yaml\nscheduler_enable: false\n```\n\n#### [Optional] Enable ElasticSearch Activation Store\nWhen you use the new scheduler, it is recommended to use ElasticSearch as an activation store.\n\n**ansible/environments/local/group_vars**\n```yaml\ndb_activation_backend: ElasticSearch\nelastic_cluster_name: <your elasticsearch cluster name>\nelastic_protocol: <your elasticsearch protocol>\nelastic_index_pattern: <your elasticsearch index pattern>\nelastic_base_volume: <your elasticsearch volume directory>\nelastic_username: <your elasticsearch username>\nelastic_password: <your elasticsearch username>\n```\n\nYou can also refer to this guide to [deploy OpenWhisk using ElasticSearch](https://github.com/apache/openwhisk/blob/master/ansible/README.md#using-elasticsearch-to-store-activations).\n\n### Deploying Using CouchDB\n-   Make sure your `db_local.ini` file is [setup for](#setup) CouchDB then execute:\n\n```shell script\ncd <openwhisk_home>\n./gradlew distDocker\ncd ansible\nansible-playbook -i environments/$ENVIRONMENT couchdb.yml\nansible-playbook -i environments/$ENVIRONMENT initdb.yml\nansible-playbook -i environments/$ENVIRONMENT wipe.yml\nansible-playbook -i environments/$ENVIRONMENT openwhisk.yml\n\n# installs a catalog of public packages and actions\nansible-playbook -i environments/$ENVIRONMENT postdeploy.yml\n\n# to use the API gateway\nansible-playbook -i environments/$ENVIRONMENT apigateway.yml\nansible-playbook -i environments/$ENVIRONMENT routemgmt.yml\n```\n\n- You need to run `initdb.yml` **every time** you do a fresh deploy CouchDB to initialize the subjects database.\n- The `wipe.yml` playbook should be run on a fresh deployment only, otherwise actions and activations will be lost.\n- Run `postdeploy.yml` after deployment to install a catalog of useful packages.\n- To use the API Gateway, you'll need to run `apigateway.yml` and `routemgmt.yml`.\n- Use `ansible-playbook -i environments/$ENVIRONMENT openwhisk.yml` to avoid wiping the data store. This is useful to start OpenWhisk after restarting your Operating System.\n\n#### Limitation\n\nYou cannot run multiple CouchDB nodes on a single machine. This limitation comes from Erlang EPMD which CouchDB relies on to find other nodes.\nTo deploy multiple CouchDB nodes, they should be placed on different machines respectively otherwise their ports will clash.\n\n\n### Deploying Using Cloudant\n-   Make sure your `db_local.ini` file is set up for Cloudant. See [Setup](#setup).\n-   Then execute:\n\n```shell script\ncd <openwhisk_home>\n./gradlew distDocker\ncd ansible\nansible-playbook -i environments/$ENVIRONMENT initdb.yml\nansible-playbook -i environments/$ENVIRONMENT wipe.yml\nansible-playbook -i environments/$ENVIRONMENT apigateway.yml\nansible-playbook -i environments/$ENVIRONMENT openwhisk.yml\n\n# installs a catalog of public packages and actions\nansible-playbook -i environments/$ENVIRONMENT postdeploy.yml\n\n# to use the API gateway\nansible-playbook -i environments/$ENVIRONMENT apigateway.yml\nansible-playbook -i environments/$ENVIRONMENT routemgmt.yml\n```\n\n- You need to run `initdb` on Cloudant **only once** per Cloudant database to initialize the subjects database.\n- The `initdb.yml` playbook will only initialize your database if it is not initialized already, else it will skip initialization steps.\n- The `wipe.yml` playbook should be run on a fresh deployment only, otherwise actions and activations will be lost.\n- Run `postdeploy.yml` after deployment to install a catalog of useful packages.\n- To use the API Gateway, you'll need to run `apigateway.yml` and `routemgmt.yml`.\n- Use `ansible-playbook -i environments/$ENVIRONMENT openwhisk.yml` to avoid wiping the data store. This is useful to start OpenWhisk after restarting your Operating System.\n\n### Deploying Using MongoDB\n\nYou can choose MongoDB instead of CouchDB as the database backend to store entities.\n\n- Deploy a mongodb server(Optional, for test and develop only, use an external MongoDB server in production).\n  You need to execute `pip install pymongo` first\n\n```\nansible-playbook -i environments/<environment> mongodb.yml -e mongodb_data_volume=\"/tmp/mongo-data\"\n```\n\n- Then execute\n\n```\ncd <openwhisk_home>\n./gradlew distDocker\ncd ansible\nansible-playbook -i environments/<environment> initMongodb.yml -e mongodb_connect_string=\"mongodb://172.17.0.1:27017\"\nansible-playbook -i environments/<environment> apigateway.yml -e mongodb_connect_string=\"mongodb://172.17.0.1:27017\"\nansible-playbook -i environments/<environment> openwhisk.yml -e mongodb_connect_string=\"mongodb://172.17.0.1:27017\" -e db_artifact_backend=\"MongoDB\"\n\n# installs a catalog of public packages and actions\nansible-playbook -i environments/<environment> postdeploy.yml\n\n# to use the API gateway\nansible-playbook -i environments/<environment> apigateway.yml\nansible-playbook -i environments/<environment> routemgmt.yml\n```\n\nAvailable parameters for ansible are\n```\n  mongodb:\n    connect_string: \"{{ mongodb_connect_string }}\"\n    database: \"{{ mongodb_database | default('whisks') }}\"\n    data_volume: \"{{ mongodb_data_volume | default('mongo-data') }}\"\n```\n\n### Using ElasticSearch to Store Activations\n\nYou can use ElasticSearch (ES) to store activations separately while other entities remain stored in CouchDB. There is an Ansible playbook to setup a simple ES cluster for testing and development purposes.\n\n-   Provide your custom ES related ansible arguments:\n\n```\nelastic_protocol=\"http\"\nelastic_index_pattern=\"openwhisk-%s\" // this will be combined with namespace's name, so different namespace can use different index\nelastic_base_volume=\"esdata\" // name of docker volume to store ES data\nelastic_cluster_name=\"openwhisk\"\nelastic_java_opts=\"-Xms1g -Xmx1g\"\nelastic_loglevel=\"INFO\"\nelastic_username=\"admin\"\nelastic_password=\"admin\"\nelasticsearch_connect_string=\"x.x.x.x:9200,y.y.y.y:9200\" // if you want to use an external ES cluster, add it\n```\n\n-  Then execute:\n\n```shell script\ncd <openwhisk_home>\n./gradlew distDocker\ncd ansible\n# couchdb is still needed to store subjects and actions\nansible-playbook -i environments/$ENVIRONMENT couchdb.yml\nansible-playbook -i environments/$ENVIRONMENT initdb.yml\nansible-playbook -i environments/$ENVIRONMENT wipe.yml\n# this will deploy a simple ES cluster, you can skip this to use external ES cluster\nansible-playbook -i environments/$ENVIRONMENT elasticsearch.yml\nansible-playbook -i environments/$ENVIRONMENT openwhisk.yml -e db_activation_backend=ElasticSearch\n\n# installs a catalog of public packages and actions\nansible-playbook -i environments/$ENVIRONMENT postdeploy.yml\n\n# to use the API gateway\nansible-playbook -i environments/$ENVIRONMENT apigateway.yml\nansible-playbook -i environments/$ENVIRONMENT routemgmt.yml\n```\n\n### Configuring the installation of `wsk` CLI\nThere are two installation modes to install `wsk` CLI: remote and local.\n\nThe mode \"remote\" means to download the `wsk` binaries from available web links.\nBy default, OpenWhisk sets the installation mode to remote and downloads the\nbinaries from the CLI\n[release page](https://github.com/apache/openwhisk-cli/releases),\nwhere OpenWhisk publishes the official `wsk` binaries.\n\nThe mode \"local\" means to build and install the `wsk` binaries from local CLI\nproject. You can download the source code of OpenWhisk CLI\n[here](https://github.com/apache/openwhisk-cli).\nLet's assume your OpenWhisk CLI home directory is\n`$OPENWHISK_HOME/../openwhisk-cli` and you've already `export`ed\n`OPENWHISK_HOME` to be the root directory of this project. After you download\nthe CLI repository, use the gradle command to build the binaries (you can omit\nthe `-PnativeBuild` if you want to cross-compile for all supported platforms):\n\n```shell script\ncd \"$OPENWHISK_HOME/../openwhisk-cli\"\n./gradlew releaseBinaries -PnativeBuild\n```\n\nThe binaries are generated and put into a tarball in the folder\n`../openwhisk-cli/release`.  Then, use the following Ansible command\nto (re-)configure the CLI installation:\n\n```shell script\nexport OPENWHISK_ENVIRONMENT=local  # ... or whatever\nansible-playbook -i environments/$OPENWHISK_ENVIRONMENT edge.yml -e mode=clean\nansible-playbook -i environments/$OPENWHISK_ENVIRONMENT edge.yml \\\n    -e cli_installation_mode=local \\\n    -e openwhisk_cli_home=\"$OPENWHISK_HOME/../openwhisk-cli\"\n```\n\nThe parameter `cli_installation_mode` specifies the CLI installation mode and\nthe parameter `openwhisk_cli_home` specifies the home directory of your local\nOpenWhisk CLI.  (_n.b._ `openwhisk_cli_home` defaults to\n`$OPENWHISK_HOME/../openwhisk-cli`.)\n\nOnce the CLI is installed, you can [use it to work with Whisk](../docs/cli.md).\n\n### Hot-swapping a Single Component\nThe playbook structure allows you to clean, deploy or re-deploy a single component as well as the entire OpenWhisk stack. Let's assume you have deployed the entire stack using the `openwhisk.yml` playbook. You then make a change to a single component, for example the invoker. You will probably want a new tag on the invoker image so you first build it using:\n\n```shell script\ncd <openwhisk_home>\n./gradlew :core:invoker:distDocker -PdockerImageTag=myNewInvoker\n```\nThen all you need to do is re-deploy the invoker using the new image:\n\n```shell script\ncd ansible\nansible-playbook -i environments/$ENVIRONMENT invoker.yml -e docker_image_tag=myNewInvoker\n```\n\n**Hint:** You can omit the Docker image tag parameters in which case `latest` will be used implicitly.\n\n### Cleaning a Single Component\nYou can remove a single component just as you would remove the entire deployment stack.\nFor example, if you wanted to remove only the controller you would run:\n\n```shell script\ncd ansible\nansible-playbook -i environments/$ENVIRONMENT controller.yml -e mode=clean\n```\n\n**Caveat:** In distributed environments some components (e.g. Invoker, etc.) exist on multiple machines. So if you run a playbook to clean or deploy those components, it will run on **all** of the hosts targeted by the component's playbook.\n\n\n### Cleaning an OpenWhisk Deployment\nOnce you are done with the deployment you can clean it from the target environment.\n\n```shell script\nansible-playbook -i environments/$ENVIRONMENT openwhisk.yml -e mode=clean\n```\n\n### Removing all prereqs from an environment\nThis is usually not necessary, however in case you want to uninstall all prereqs from a target environment, execute:\n\n```shell script\nansible-playbook -i environments/$ENVIRONMENT prereq.yml -e mode=clean\n```\n\n### Lean Setup\nTo have a lean setup (no Kafka, Zookeeper and no Invokers as separate entities):\n\nAt [Deploying Using CouchDB](ansible/README.md#deploying-using-cloudant) step, replace:\n```shell script\nansible-playbook -i environments/$ENVIRONMENT openwhisk.yml\n```\nby:\n```shell script\nansible-playbook -i environments/$ENVIRONMENT openwhisk.yml -e lean=true\n```\n\n### Troubleshooting\nSome of the more common problems and their solution are listed here.\n\n#### Setuptools Version Mismatch\nIf you encounter the following error message during `ansible` execution\n\n```\nERROR! Unexpected Exception: ... Requirement.parse('setuptools>=11.3'))\n```\n\nyour `setuptools` package is likely out of date. You can upgrade the package using this command:\n\n```shell script\npip install --upgrade setuptools --user python\n```\n\n\n#### Mac Setup - Python Interpreter\nThe MacOS environment assumes Python is installed in `/usr/local/bin` which is the default location when using `brew`.\nThe following error will occur if Python is located elsewhere:\n\n```\nansible all -i environments/mac -m ping\nansible | FAILED! => {\n    \"changed\": false,\n    \"failed\": true,\n    \"module_stderr\": \"/bin/sh: /usr/local/bin/python: No such file or directory\\n\",\n    \"module_stdout\": \"\",\n    \"msg\": \"MODULE FAILURE\",\n    \"parsed\": false\n}\n```\n\nAn expedient workaround is to create a link to the expected location:\n\n```shell script\nln -s $(which python) /usr/local/bin/python\n```\n\nAlternatively, you can also configure the location of Python interpreter in `environments/<environment>/group_vars`.\n\n```shell script\nansible_python_interpreter: \"/usr/local/bin/python\"\n```\n\n#### Failed to import docker-py\n\nAfter `brew install ansible`, the following lines are printed out:\n\n```\n==> Caveats\nIf you need Python to find the installed site-packages:\n  mkdir -p ~/Library/Python/2.7/lib/python/site-packages\n  echo '/usr/local/lib/python2.7/site-packages' > ~/Library/Python/2.7/lib/python/site-packages/homebrew.pth\n```\n\nJust run the two commands to fix this issue.\n\n#### Spaces in Paths\nAnsible 2.1.0.0 and earlier versions do not support a space in file paths.\nMany file imports and roles will not work correctly when included from a path that contains spaces.\nIf you encounter this error message during Ansible execution\n\n```\nfatal: [ansible]: FAILED! => {\"failed\": true, \"msg\": \"need more than 1 value to unpack\"}\n```\n\nthe path to your OpenWhisk `ansible` directory contains spaces. To fix this, please copy the source tree to a path\nwithout spaces as there is no current fix available to this problem.\n\n#### Changing limits\nThe default system throttling limits are configured in this file [./group_vars/all](./group_vars/all) and may be changed by modifying the group_vars for your specific environment.\n```\nlimits:\n  invocationsPerMinute: \"{{ limit_invocations_per_minute | default(60) }}\"\n  concurrentInvocations: \"{{ limit_invocations_concurrent | default(30) }}\"\n  firesPerMinute: \"{{ limit_fires_per_minute | default(60) }}\"\n  sequenceMaxLength: \"{{ limit_sequence_max_length | default(50) }}\"\n```\n- The `limits.invocationsPerMinute` represents the allowed namespace action invocations per minute.\n- The `limits.concurrentInvocations` represents the maximum concurrent invocations allowed per namespace.\n- The `limits.firesPerMinute` represents the allowed namespace trigger firings per minute.\n- The `limits.sequenceMaxLength` represents the maximum length of a sequence action.\n\n#### Set the timezone for containers\nThe default timezone for all system containers is UTC. The timezone may differ from your servers which could make it difficult to inspect logs. The timezone is configured globally in [group_vars/all](./group_vars/all#L280) or by passing an extra variable `-e docker_timezone=xxx` when you run an ansible-playbook.\n"
  },
  {
    "path": "ansible/ansible.cfg",
    "content": "[defaults]\n\ncallback_whitelist = profile_tasks\nretry_files_enabled = False\nhost_key_checking = False\ninventory = environments/local\ncallback_plugins = callbacks\nhash_behaviour = merge\ngather_timeout = 60\n\n[ssh_connection]\nscp_if_ssh = True\n"
  },
  {
    "path": "ansible/apigateway.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n- hosts: redis\n  roles:\n  - redis\n\n- hosts: apigateway\n  roles:\n  - apigateway\n"
  },
  {
    "path": "ansible/callbacks/logformatter.py",
    "content": "\"\"\"Python callback for highlighting Ansible logs.\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\nfrom __future__ import (absolute_import, division, print_function)\nimport os\nimport sys\nimport textwrap\nfrom ansible.plugins.callback import CallbackBase\n__metaclass__ = type\n\n\nclass CallbackModule(CallbackBase):\n    \"\"\".\"\"\"\n\n    def __init__(self):\n        \"\"\"Initialize superclass.\"\"\"\n        super(CallbackModule, self).__init__()\n\n    def emit(self, host, category, data):\n        \"\"\"Emit colorized output based upon data contents.\"\"\"\n        if type(data) == dict:\n            cmd = data['cmd'] if 'cmd' in data else None\n            msg = data['msg'] if 'msg' in data else None\n            stdout = data['stdout'] if 'stdout' in data else None\n            stderr = data['stderr'] if 'stderr' in data else None\n            reason = data['reason'] if 'reason' in data else None\n\n            print()\n            if cmd:\n                print(hilite('[%s]\\n> %s' % (category, cmd), category, wrap = False))\n            if reason:\n                print(hilite(reason, category))\n            if msg:\n                print(hilite(msg, category))\n            if stdout:\n                print(hilite(stdout, category))\n            if stderr:\n                print(hilite(stderr, category))\n\n    def runner_on_failed(self, host, res, ignore_errors=False):\n        self.emit(host, 'FAILED', res)\n\n    def runner_on_ok(self, host, res):\n        pass\n\n    def runner_on_skipped(self, host, item=None):\n        self.emit(host, 'SKIPPED', '...')\n\n    def runner_on_unreachable(self, host, res):\n        self.emit(host, 'UNREACHABLE', res)\n\n    def runner_on_async_failed(self, host, res, jid):\n        self.emit(host, 'FAILED', res)\n\n\ndef hilite(msg, status, wrap = True):\n    \"\"\"Highlight message.\"\"\"\n    def supports_color():\n        if ((sys.platform != 'win32' or 'ANSICON' in os.environ) and\n           sys.stdout.isatty()):\n            return True\n        else:\n            return False\n\n    if supports_color():\n        attr = []\n        if status == 'FAILED':\n            # red\n            attr.append('31')\n        else:\n            # bold\n            attr.append('1')\n        text = '\\x1b[%sm%s\\x1b[0m' % (';'.join(attr), msg)\n    else:\n        text = msg\n    return textwrap.fill(text, 80) if wrap else text\n"
  },
  {
    "path": "ansible/controller.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys Openwhisk Controllers.\n\n- hosts: controllers\n  vars:\n    #\n    # host_group - usually \"{{ groups['...'] }}\" where '...' is what was used\n    #   for 'hosts' above.  The hostname of each host will be looked up in this\n    #   group to assign a zero-based index.  That index will be used in concert\n    #   with 'name_prefix' below to assign a host/container name.\n    host_group: \"{{ groups['controllers'] }}\"\n    #\n    # name_prefix - a unique prefix for this set of controllers.  The prefix\n    #   will be used in combination with an index (determined using\n    #   'host_group' above) to name host/controllers.\n    name_prefix: \"controller\"\n    #\n    # controller_index_base - the deployment process allocates host docker\n    #   ports to individual controllers based on their indices.  This is an\n    #   additional offset to prevent collisions between different controller\n    #   groups. Usually 0 if only one group is being deployed, otherwise\n    #   something like \"{{ groups['firstcontrollergroup']|length }}\"\n    controller_index_base: 0\n    #\n    # select which additional capabilities (from the controller role) need\n    #   to be added to the controller.  Plugin will override default\n    #   configuration settings.  (Plugins are found in the\n    #   'roles/controller/tasks' directory for now.)\n    controller_plugins:\n      # Join an pekko cluster rather than running standalone pekko\n      - \"join_pekko_cluster\"\n\n  roles:\n    - controller\n"
  },
  {
    "path": "ansible/couchdb.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys a CouchDB for Openwhisk.\n\n- hosts: db\n  roles:\n  - couchdb\n"
  },
  {
    "path": "ansible/downloadcli-github.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook is used to download cli from Github and copy directly to bin\n\n- hosts: ansible\n  tasks:\n  - name: grab the local CLI from the Github\n    unarchive:\n      src: \"{{ openwhisk_cli.remote.location }}/{{ openwhisk_cli.archive_name}}-{{ openwhisk_cli_tag }}-{{os}}-{{arch}}.{{ext}}\"\n      dest: \"{{ openwhisk_home }}/bin\"\n      mode: \"0755\"\n      remote_src: yes\n    vars:\n      arch: \"{{ ansible_machine | replace ('x86_64', 'amd64') }}\"\n      os: \"{{ ansible_system | lower | replace('darwin', 'mac') }}\"\n      ext: \"{{ ( ansible_system in ['Windows', 'Darwin']) | ternary('zip', 'tgz') }}\"\n"
  },
  {
    "path": "ansible/downloadcli.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook downloads the Openwhisk cli aka wsk from the API host.\n\n- hosts: ansible\n  roles:\n  - cli-install\n"
  },
  {
    "path": "ansible/edge.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys Openwhisk Edge servers.\n# The edge is usually populated with NGINX serving as proxy.\n# The CLI also gets built and published for downloading from NGINX.\n# SDKs for blackbox get published to NGINX also.\n\n- hosts: edge\n  roles:\n  - nginx\n  - cli\n"
  },
  {
    "path": "ansible/elasticsearch.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys a ElasticSearch cluster\n\n- hosts: elasticsearch\n  gather_facts: yes\n  vars:\n    #\n    # host_group - usually \"{{ groups['...'] }}\" where '...' is what was used\n    #   for 'hosts' above.  The hostname of each host will be looked up in this\n    #   group to assign a zero-based index.  That index will be used in concert\n    #   with 'name_prefix' below to assign a host/container name.\n    host_group: \"{{ groups['elasticsearch'] }}\"\n    #\n    # name_prefix - a unique prefix for this set of elasticsearches.  The prefix\n    # will be used in combination with an index (determined using\n    # 'host_group' above) to name host/elasticsearcher.\n    name_prefix: \"elasticsearch\"\n  roles:\n  - elasticsearch\n"
  },
  {
    "path": "ansible/environments/docker-machine/group_vars/all",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nconfig_root_dir: /Users/Shared/wskconf\nwhisk_logs_dir: /Users/Shared/wsklogs\ndocker_registry: \"\"\ndocker_dns: \"\"\nruntimes_bypass_pull_for_local_images: true\n\nenvironment_type: \"docker-machine\"\n\nenv_hosts_dir: \"{{ playbook_dir }}/environments/docker-machine\"\n\n# The whisk_api_localhost_name is used to configure nginx to permit vanity URLs for web actions.\n# It is also used for the SSL certificate generation. For a local deployment, this is typically\n# a hostname that is resolved on the client, via /etc/hosts for example.\nwhisk_api_localhost_name: \"openwhisk\"\n\n# Hardcoded for docker-machine since db init runs on host not inside VM\ndb_prefix: whisk_dockermachine_\n\n# API GW connection configuration\napigw_auth_user: \"\"\napigw_auth_pwd: \"\"\napigw_host_v2: \"http://{{ groups['apigateway']|first }}:{{apigateway.port.api}}/v2\"\n\ninvoker_allow_multiple_instances: true\n\n# Set kafka configuration\nkafka_heap: '512m'\nkafka_topics_completed_retentionBytes: 104857600\nkafka_topics_completed_retentionMS: 300000\nkafka_topics_health_retentionBytes: 104857600\nkafka_topics_health_retentionMS: 300000\nkafka_topics_invoker_retentionBytes: 104857600\nkafka_topics_invoker_retentionMS: 300000\n"
  },
  {
    "path": "ansible/environments/docker-machine/hosts.j2.ini",
    "content": "; the first parameter in a host is the inventory_hostname\n\n; used for local actions only\nansible ansible_connection=local\n\n[edge]\n{{ docker_machine_ip }}     ansible_host={{ docker_machine_ip }}\n\n[controllers]\ncontroller0                 ansible_host={{ docker_machine_ip }}\n;{% if mode is defined and 'HA' in mode %}\n;controller1                 ansible_host={{ docker_machine_ip }}\n;{% endif %}\n\n[kafkas]\nkafka0                      ansible_host={{ docker_machine_ip }}\n{% if mode is defined and 'HA' in mode %}\nkafka1                      ansible_host={{ docker_machine_ip }}\n{% endif %}\n\n[zookeepers:children]\nkafkas\n\n[invokers]\ninvoker0                    ansible_host={{ docker_machine_ip }}\n{% if mode is defined and 'HA' in mode %}\ninvoker1                    ansible_host={{ docker_machine_ip }}\n{% endif %}\n\n[db]\n{{ docker_machine_ip }}     ansible_host={{ docker_machine_ip }}\n\n[redis]\n{{ docker_machine_ip }}     ansible_host={{ docker_machine_ip }}\n\n[apigateway]\n{{ docker_machine_ip }}     ansible_host={{ docker_machine_ip }}\n\n[elasticsearch:children]\ndb\n\n[etcd]\netcd0            ansible_host={{ docker_machine_ip }}\n{% if mode is defined and 'HA' in mode %}\netcd1            ansible_host={{ docker_machine_ip }}\n\n; define variables\n[all:vars]\nansible_connection=ssh\nansible_user=docker\nansible_ssh_private_key_file=~/.docker/machine/machines/{{ docker_machine_name | default(\"whisk\")}}/id_rsa\nansible_python_interpreter=/usr/local/bin/python\n"
  },
  {
    "path": "ansible/environments/local/group_vars/all",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nopenwhisk_tmp_dir: \"{{ lookup('env', 'OPENWHISK_TMP_DIR')|default('/tmp' if ansible_distribution == 'MacOSX' else '/var/tmp', true) }}\"\nconfig_root_dir: \"{{ openwhisk_tmp_dir }}/wskconf\"\nwhisk_logs_dir: \"{{ openwhisk_tmp_dir }}/wsklogs\"\ncoverage_enabled: \"{{ lookup('env', 'GRADLE_COVERAGE') | default('false', true) | bool}}\"\ncoverage_logs_dir: \"{{ openwhisk_tmp_dir }}/wskcov\"\ndocker_registry: \"\"\ndocker_dns: \"\"\nruntimes_bypass_pull_for_local_images: true\ninvoker_use_runc: \"{{ ansible_distribution != 'MacOSX' }}\"\n\ndb_prefix: whisk_local_\n\n# API GW connection configuration\napigw_auth_user: \"\"\napigw_auth_pwd: \"\"\napigw_host_v2: \"http://{{ groups['apigateway']|first }}:{{apigateway.port.api}}/v2\"\n\ninvoker_allow_multiple_instances: true\n\n# Set kafka configuration\nkafka_heap: '512m'\nkafka_topics_completed_retentionBytes: 104857600\nkafka_topics_completed_retentionMS: 300000\nkafka_topics_health_retentionBytes: 104857600\nkafka_topics_health_retentionMS: 300000\nkafka_topics_invoker_retentionBytes: 104857600\nkafka_topics_invoker_retentionMS: 300000\n\nenv_hosts_dir: \"{{ playbook_dir }}/environments/local\"\n\ncontainer_pool_pekko_client: true\nruntimes_enable_concurrency: true\nlimit_action_concurrency_max: 500\nnamespace_default_limit_action_concurrency_max: 500\n"
  },
  {
    "path": "ansible/environments/local/hosts.j2.ini",
    "content": "; the first parameter in a host is the inventory_hostname\n\n; used for local actions only\nansible ansible_connection=local\n\n[edge]\n172.17.0.1          ansible_host=172.17.0.1 ansible_connection=local\n\n[controllers]\ncontroller0         ansible_host=172.17.0.1 ansible_connection=local\n;{% if mode is defined and 'HA' in mode %}\n;controller1         ansible_host=172.17.0.1 ansible_connection=local\n;{% endif %}\n\n[kafkas]\nkafka0              ansible_host=172.17.0.1 ansible_connection=local\n{% if mode is defined and 'HA' in mode %}\nkafka1              ansible_host=172.17.0.1 ansible_connection=local\n{% endif %}\n\n[zookeepers:children]\nkafkas\n\n[invokers]\ninvoker0            ansible_host=172.17.0.1 ansible_connection=local\n{% if mode is defined and 'HA' in mode %}\ninvoker1            ansible_host=172.17.0.1 ansible_connection=local\n{% endif %}\n\n[schedulers]\nscheduler0       ansible_host=172.17.0.1 ansible_connection=local\n{% if mode is defined and 'HA' in mode %}\nscheduler1       ansible_host=172.17.0.1 ansible_connection=local\n{% endif %}\n\n; db group is only used if db.provider is CouchDB\n[db]\n172.17.0.1          ansible_host=172.17.0.1 ansible_connection=local\n\n[elasticsearch:children]\ndb\n\n[redis]\n172.17.0.1          ansible_host=172.17.0.1 ansible_connection=local\n\n[apigateway]\n172.17.0.1          ansible_host=172.17.0.1 ansible_connection=local\n\n[etcd]\netcd0            ansible_host=172.17.0.1 ansible_connection=local\n"
  },
  {
    "path": "ansible/etcd.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys Openwhisk Invokers.\n\n- hosts: etcd\n  roles:\n  - etcd\n"
  },
  {
    "path": "ansible/files/activations_design_document_for_activations_db.json",
    "content": "{\n  \"_id\": \"_design/activations\",\n  \"views\": {\n    \"byDate\": {\n      \"map\": \"function (doc) {\\n  if (doc.activationId !== undefined) try {\\n    emit(doc.start, [doc._id, doc._rev]);\\n  } catch (e) {}\\n}\"\n    }\n  },\n  \"language\": \"javascript\"\n}\n"
  },
  {
    "path": "ansible/files/auth.guest",
    "content": "23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP"
  },
  {
    "path": "ansible/files/auth.whisk.system",
    "content": "789c46b1-71f6-4ed5-8c54-816aa4f8c502:abczO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP"
  },
  {
    "path": "ansible/files/auth_design_document_for_subjects_db_v2.0.0.json",
    "content": "{\n  \"_id\": \"_design/subjects.v2.0.0\",\n  \"views\": {\n    \"identities\": {\n      \"map\": \"function (doc) {\\n  if (doc.namespaces && !doc.blocked) {\\n    doc.namespaces.forEach(function(namespace) {\\n      var v = {_id: namespace.name + '/limits', namespace: namespace.name, uuid: namespace.uuid, key: namespace.key};\\n      emit([namespace.name], v);\\n      emit([namespace.uuid, namespace.key], v);\\n    });\\n  }\\n}\"\n    }\n  },\n  \"language\": \"javascript\",\n  \"indexes\": {}\n}\n"
  },
  {
    "path": "ansible/files/filter_design_document.json",
    "content": "{\n  \"_id\": \"_design/snapshotFilters\",\n  \"filters\": {\n    \"withoutDeletedAndDesignDocuments\": \"function(doc, req) {\\n    return !doc._deleted && !(doc._id.indexOf(\\\"_design/\\\") == 0);\\n}\"\n  }\n}\n"
  },
  {
    "path": "ansible/files/genssl.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nPASSWORD=\"openwhisk\"\n\nif [ \"$#\" -lt 3 ]; then\n    echo \"usage: $0 <common name: host or ip> [server|client] <scriptdir> <OPTIONAL:TrustorePassword> <OPTIONAL:generateKey>\"\n    exit\nfi\nCN=$1\nTYPE=$2\nSCRIPTDIR=$3\nexport TRUSTSTORE_PASSWORD=${4:-PASSWORD}\nNAME_PREFIX=$5\nGENKEY=$6\n\n\n## generates a (self-signed) certificate\nif [[ -n $GENKEY ]]\nthen\n  openssl genrsa -out \"$SCRIPTDIR/${NAME_PREFIX}openwhisk-server-key.pem\" 2048\nfi\nfunction gen_csr(){\n  echo generating server certificate request\n  openssl req -new \\\n      -key \"$SCRIPTDIR/${NAME_PREFIX}openwhisk-server-key.pem\" \\\n      -nodes \\\n      -subj \"/C=US/ST=NY/L=Yorktown/O=OpenWhisk/CN=$CN\" \\\n      -out \"$SCRIPTDIR/${NAME_PREFIX}openwhisk-server-request.csr\"\n}\nfunction gen_cert(){\n  echo generating self-signed password-less server certificate\n  openssl x509 -req \\\n      -in \"$SCRIPTDIR/${NAME_PREFIX}openwhisk-server-request.csr\" \\\n      -signkey \"$SCRIPTDIR/${NAME_PREFIX}openwhisk-server-key.pem\" \\\n      -out \"${SCRIPTDIR}/${NAME_PREFIX}openwhisk-server-cert.pem\" \\\n      -days 365\n}\n\nfunction gen_p12_keystore(){\n  openssl pkcs12 -export -name $CN \\\n       -passout pass:$TRUSTSTORE_PASSWORD \\\n       -in \"$SCRIPTDIR/${NAME_PREFIX}openwhisk-server-cert.pem\" \\\n       -inkey \"$SCRIPTDIR/${NAME_PREFIX}openwhisk-server-key.pem\" \\\n       -out \"$SCRIPTDIR/${NAME_PREFIX}openwhisk-keystore.p12\"\n}\n\nif [ \"$TYPE\" == \"server_with_JKS_keystore\" ]; then\n  gen_csr\n  gen_cert\n  echo generate new key and place it in the keystore\n  keytool -genkey -v \\\n    -alias $CN \\\n    -dname \"C=US,ST=NY,L=Yorktown,O=OpenWhisk,CN=$CN\" \\\n    -keystore \"${SCRIPTDIR}/${NAME_PREFIX}keystore.jks\" \\\n    -keypass:env TRUSTSTORE_PASSWORD \\\n    -storepass:env TRUSTSTORE_PASSWORD \\\n    -keyalg RSA \\\n    -ext KeyUsage:critical=\"keyCertSign\" \\\n    -ext BasicConstraints:critical=\"ca:true\" \\\n    -validity 365\n  echo export private key from the keystore\n  keytool -keystore \"${SCRIPTDIR}/${NAME_PREFIX}keystore.jks\" -alias $CN -certreq -file \"${SCRIPTDIR}/${NAME_PREFIX}cert-file\" -storepass:env TRUSTSTORE_PASSWORD\n  echo sign the certificate with private key\n  openssl x509 -req -CA \"${SCRIPTDIR}/${NAME_PREFIX}openwhisk-server-cert.pem\" -CAkey \"$SCRIPTDIR/${NAME_PREFIX}openwhisk-server-key.pem\" -in \"${SCRIPTDIR}/${NAME_PREFIX}cert-file\" -out \"${SCRIPTDIR}/${NAME_PREFIX}cert-signed\" -days 365 -CAcreateserial -passin pass:$TRUSTSTORE_PASSWORD\n  echo import CA cert in the keystore\n  keytool -keystore \"${SCRIPTDIR}/${NAME_PREFIX}keystore.jks\" -alias CARoot -import -file \"${SCRIPTDIR}/${NAME_PREFIX}openwhisk-server-cert.pem\" -storepass:env TRUSTSTORE_PASSWORD -noprompt\n  echo import the private key in the keystore\n  keytool -keystore \"${SCRIPTDIR}/${NAME_PREFIX}keystore.jks\" -alias $CN -import -file \"${SCRIPTDIR}/${NAME_PREFIX}cert-signed\" -storepass:env TRUSTSTORE_PASSWORD -noprompt\n\nelif [ \"$TYPE\" == \"server\" ]; then\n    gen_csr\n    gen_cert\n    echo generate keystore\n    gen_p12_keystore\nelif [ \"$TYPE\" == \"p12_keystore_only\" ]; then\n    gen_csr\n    gen_p12_keystore\nelse\n    echo generating client ca key\n    openssl genrsa -aes256 -passout pass:$PASSWORD -out \"$SCRIPTDIR/openwhisk-client-ca-key.pem\" 2048\n\n    echo generating client ca request\n    openssl req -new \\\n    -key \"$SCRIPTDIR/openwhisk-client-ca-key.pem\" \\\n    -passin pass:$PASSWORD \\\n    -subj \"/C=US/ST=NY/L=Yorktown/O=OpenWhisk/CN=$CN\" \\\n    -out \"$SCRIPTDIR/openwhisk-client-ca.csr\"\n\n    echo generating client ca pem\n    openssl x509 -req \\\n    -in \"$SCRIPTDIR/openwhisk-client-ca.csr\" \\\n    -signkey \"$SCRIPTDIR/openwhisk-client-ca-key.pem\" \\\n    -passin pass:$PASSWORD \\\n    -out \"$SCRIPTDIR/openwhisk-client-ca-cert.pem\" \\\n    -days 365 -sha1 -extensions v3_ca\n\n    echo generating client key\n    openssl genrsa -aes256 -passout pass:$PASSWORD -out \"$SCRIPTDIR/openwhisk-client-key.pem\" 2048\n\n    echo generating client certificate csr file\n    openssl req -new \\\n    -key \"$SCRIPTDIR/openwhisk-client-key.pem\" \\\n    -passin pass:$PASSWORD \\\n    -subj \"/C=US/ST=NY/L=Yorktown/O=OpenWhisk/CN=guest\" \\\n    -out \"$SCRIPTDIR/openwhisk-client-certificate-request.csr\"\n\n    echo generating self-signed client certificate\n    echo 01 > $SCRIPTDIR/openwhisk-client-ca-cert.srl\n    openssl x509 -req \\\n    -in \"$SCRIPTDIR/openwhisk-client-certificate-request.csr\" \\\n    -CA \"$SCRIPTDIR/openwhisk-client-ca-cert.pem\" \\\n    -CAkey \"$SCRIPTDIR/openwhisk-client-ca-key.pem\" \\\n    -CAserial \"$SCRIPTDIR/openwhisk-client-ca-cert.srl\" \\\n    -passin pass:$PASSWORD \\\n    -out \"$SCRIPTDIR/openwhisk-client-cert.pem\" \\\n    -days 365 -sha1 -extensions v3_req\n\n    echo remove client key\\'s password\n    openssl rsa \\\n    -in \"$SCRIPTDIR/openwhisk-client-key.pem\" \\\n    -passin pass:$PASSWORD \\\n    -out \"$SCRIPTDIR/openwhisk-client-key.pem\"\nfi\n"
  },
  {
    "path": "ansible/files/logCleanup_design_document_for_activations_db.json",
    "content": "{\n  \"_id\": \"_design/logCleanup\",\n  \"views\": {\n    \"byDateWithLogs\": {\n      \"map\": \"function (doc) {\\n  if (doc.activationId !== undefined && doc.logs && doc.logs.length > 0) try {\\n    var deleteLogs = true;\\n    for (i = 0; i < doc.annotations.length; i++) {\\n      var a = doc.annotations[i];\\n      if (a.key == \\\"kind\\\") {\\n        deleteLogs = a.value != \\\"sequence\\\";\\n        break;\\n      }\\n    }\\n    if (deleteLogs) {\\n      emit(doc.start, doc._id);\\n    }\\n  } catch (e) {}\\n}\"\n    }\n  },\n  \"language\": \"javascript\"\n}\n"
  },
  {
    "path": "ansible/files/namespace_throttlings_design_document_for_subjects_db.json",
    "content": "{\n  \"_id\": \"_design/namespaceThrottlings\",\n  \"views\": {\n    \"blockedNamespaces\": {\n      \"map\": \"function (doc) {\\n  if (doc._id.indexOf(\\\"/limits\\\") >= 0) {\\n    if (doc.concurrentInvocations === 0 || doc.invocationsPerMinute === 0) {\\n      var namespace = doc._id.replace(\\\"/limits\\\", \\\"\\\");\\n      emit(namespace, 1);\\n    }\\n  } else if (doc.subject && doc.namespaces && doc.blocked) {\\n    doc.namespaces.forEach(function(namespace) {\\n      emit(namespace.name, 1);\\n    });\\n  }\\n}\",\n      \"reduce\": \"_sum\"\n    }\n  },\n  \"language\": \"javascript\"\n}\n"
  },
  {
    "path": "ansible/files/package-versions.ini",
    "content": "[openwhisk-cli]\ngit_tag=latest\n"
  },
  {
    "path": "ansible/files/runtimes-nodeonly.json",
    "content": "{\n    \"description\": [\n        \"This file describes the different languages (aka. managed action runtimes) supported by the system\",\n        \"as well as blackbox images that support the runtime specification.\",\n        \"Only actions with runtime families / kinds defined here can be created / read / updated / deleted / invoked.\",\n        \"Define a list of runtime families (example: 'nodejs') with at least one kind per family (example: 'nodejs:20').\",\n        \"Each runtime family needs a default kind (default: true).\",\n        \"When removing or renaming runtime families or runtime kinds from this file, preexisting actions\",\n        \"with the affected kinds can no longer be read / updated / deleted / invoked. In order to remove or rename\",\n        \"runtime families or runtime kinds, mark all affected runtime kinds as deprecated (deprecated: true) and\",\n        \"perform a manual migration of all affected actions.\",\n        \"\",\n        \"This file is meant to list a small set of runtimes used by the GitHub Action system tests. Using a small set of runtimes\",\n        \"instead of all runtimes maintained by the Apache Openwhisk community speeds up tests.\"\n    ],\n    \"runtimes\": {\n        \"nodejs\": [\n            {\n                \"kind\": \"nodejs:20\",\n                \"default\": true,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-nodejs-v20\",\n                    \"tag\": \"nightly\"\n                },\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                },\n                \"stemCells\": [{\n                    \"count\": 2,\n                    \"memory\": \"256 MB\"\n                }]\n            }\n        ]\n\n    },\n    \"blackboxes\": [\n        {\n            \"prefix\": \"openwhisk\",\n            \"name\": \"dockerskeleton\",\n            \"tag\": \"nightly\"\n        }\n    ]\n}\n"
  },
  {
    "path": "ansible/files/runtimes.json",
    "content": "{\n    \"description\": [\n        \"This file describes the different languages (aka. managed action runtimes) supported by the system\",\n        \"as well as blackbox images that support the runtime specification.\",\n        \"Only actions with runtime families / kinds defined here can be created / read / updated / deleted / invoked.\",\n        \"Define a list of runtime families (example: 'nodejs') with at least one kind per family (example: 'nodejs:20').\",\n        \"Each runtime family needs a default kind (default: true).\",\n        \"When removing or renaming runtime families or runtime kinds from this file, preexisting actions\",\n        \"with the affected kinds can no longer be read / updated / deleted / invoked. In order to remove or rename\",\n        \"runtime families or runtime kinds, mark all affected runtime kinds as deprecated (deprecated: true) and\",\n        \"perform a manual migration of all affected actions.\",\n        \"\",\n        \"This file is meant to list all stable runtimes supported by the Apache Openwhisk community.\"\n    ],\n    \"runtimes\": {\n        \"nodejs\": [\n            {\n                \"kind\": \"nodejs:18\",\n                \"default\": false,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-nodejs-v18\",\n                    \"tag\": \"nightly\"\n                },\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                }\n            },\n            {\n                \"kind\": \"nodejs:20\",\n                \"default\": true,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-nodejs-v20\",\n                    \"tag\": \"nightly\"\n                },\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                },\n                \"stemCells\": [\n                    {\n                        \"initialCount\": 2,\n                        \"memory\": \"256 MB\",\n                        \"reactive\": {\n                            \"minCount\": 1,\n                            \"maxCount\": 4,\n                            \"ttl\": \"2 minutes\",\n                            \"threshold\": 1,\n                            \"increment\": 1\n                        }\n                    }\n                ]\n            }\n        ],\n        \"python\": [\n            {\n                \"kind\": \"python:3.10\",\n                \"default\": true,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-python-v3.10\",\n                    \"tag\": \"nightly\"\n                },\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                }\n            },\n            {\n                \"kind\": \"python:3.11\",\n                \"default\": false,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-python-v3.11\",\n                    \"tag\": \"nightly\"\n                },\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                }\n            }\n        ],\n        \"swift\": [\n            {\n                \"kind\": \"swift:5.3\",\n                \"default\": true,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-swift-v5.3\",\n                    \"tag\": \"nightly\"\n                },\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                }\n            },\n            {\n                \"kind\": \"swift:5.7\",\n                \"default\": false,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-swift-v5.7\",\n                    \"tag\": \"nightly\"\n                },\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                }\n            }\n        ],\n        \"java\": [\n            {\n                \"kind\": \"java:8\",\n                \"default\": true,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"java8action\",\n                    \"tag\": \"nightly\"\n                },\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                },\n                \"requireMain\": true\n            }\n        ],\n        \"php\": [\n            {\n                \"kind\": \"php:8.1\",\n                \"default\": true,\n                \"deprecated\": false,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-php-v8.1\",\n                    \"tag\": \"nightly\"\n                },\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                }\n            }\n        ],\n        \"ruby\": [\n            {\n                \"kind\": \"ruby:2.5\",\n                \"default\": true,\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                },\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-ruby-v2.5\",\n                    \"tag\": \"nightly\"\n                }\n            }\n        ],\n        \"go\": [\n            {\n                \"kind\": \"go:1.20\",\n                \"default\": true,\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                },\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-golang-v1.20\",\n                    \"tag\": \"nightly\"\n                }\n            }\n        ],\n        \"dotnet\": [\n            {\n                \"kind\": \"dotnet:3.1\",\n                \"default\": true,\n                \"deprecated\": false,\n                \"requireMain\": true,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-dotnet-v3.1\",\n                    \"tag\": \"nightly\"\n                },\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                }\n            },\n            {\n                \"kind\": \"dotnet:6.0\",\n                \"default\": false,\n                \"deprecated\": false,\n                \"requireMain\": true,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-dotnet-v6.0\",\n                    \"tag\": \"nightly\"\n                },\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                }\n            }\n        ],\n        \"rust\": [\n            {\n                \"kind\": \"rust:1.34\",\n                \"default\": true,\n                \"image\": {\n                    \"prefix\": \"openwhisk\",\n                    \"name\": \"action-rust-v1.34\",\n                    \"tag\": \"nightly\"\n                },\n                \"deprecated\": false,\n                \"attached\": {\n                    \"attachmentName\": \"codefile\",\n                    \"attachmentType\": \"text/plain\"\n                }\n            }\n        ]\n    },\n    \"blackboxes\": [\n        {\n            \"prefix\": \"openwhisk\",\n            \"name\": \"dockerskeleton\",\n            \"tag\": \"nightly\"\n        }\n    ]\n}\n"
  },
  {
    "path": "ansible/files/whisks_design_document_for_activations_db_filters_v2.1.0.json",
    "content": "{\n  \"_id\": \"_design/whisks-filters.v2.1.0\",\n  \"language\": \"javascript\",\n  \"views\": {\n    \"activations\": {\n      \"map\": \"function (doc) {\\n  var PATHSEP = \\\"/\\\";\\n  var isActivation = function (doc) { return (doc.activationId !== undefined) };\\n  var summarize = function (doc) {\\n    var endtime = doc.end !== 0 ? doc.end : undefined;\\n    return {\\n        namespace: doc.namespace,\\n        name: doc.name,\\n        version: doc.version,\\n        publish: doc.publish,\\n        annotations: doc.annotations,\\n        activationId: doc.activationId,\\n        start: doc.start,\\n        end: endtime,\\n        duration: endtime !== undefined ? endtime - doc.start : undefined,\\n        cause: doc.cause,\\n        statusCode: (endtime !== undefined && doc.response !== undefined && doc.response.statusCode !== undefined) ? doc.response.statusCode : undefined\\n      }\\n  };\\n\\n  var pathFilter = function(doc) {\\n    for (i = 0; i < doc.annotations.length; i++) {\\n      var a = doc.annotations[i];\\n      if (a.key == \\\"path\\\") try {\\n          var p = a.value.split(PATHSEP);\\n          if (p.length == 3) {\\n            return p[1] + PATHSEP + doc.name;\\n          } else return doc.name;\\n      } catch (e) {\\n        return doc.name;\\n      }\\n    }\\n    return doc.name;\\n  }\\n\\n  if (isActivation(doc)) try {\\n    var value = summarize(doc)\\n    emit([doc.namespace+PATHSEP+pathFilter(doc), doc.start], value);\\n  } catch (e) {}\\n}\\n\",\n      \"reduce\": \"_count\"\n    }\n  }\n}\n"
  },
  {
    "path": "ansible/files/whisks_design_document_for_activations_db_filters_v2.1.1.json",
    "content": "{\n  \"_id\": \"_design/whisks-filters.v2.1.1\",\n  \"language\": \"javascript\",\n  \"views\": {\n    \"activations\": {\n      \"map\": \"function (doc) {\\n  var PATHSEP = \\\"/\\\";\\n  var isActivation = function (doc) { return (doc.activationId !== undefined) };\\n  var summarize = function (doc) {\\n    var endtime = doc.end !== 0 ? doc.end : undefined;\\n    return {\\n        namespace: doc.namespace,\\n        name: doc.name,\\n        version: doc.version,\\n        publish: doc.publish,\\n        annotations: doc.annotations,\\n        activationId: doc.activationId,\\n        start: doc.start,\\n        end: endtime,\\n        duration: endtime !== undefined ? endtime - doc.start : undefined,\\n        cause: doc.cause,\\n        statusCode: (endtime !== undefined && doc.response !== undefined && doc.response.statusCode !== undefined) ? doc.response.statusCode : undefined\\n      }\\n  };\\n  \\n  var pathFilter = function(doc) {\\n    var path = undefined;\\n    var binding = undefined;\\n    for (i = 0; i < doc.annotations.length; i++) {\\n      var a = doc.annotations[i];\\n      if (a.key == \\\"path\\\") {\\n        path = a.value;\\n      }\\n      if (a.key == \\\"binding\\\") {\\n        binding = a.value;\\n      }\\n    }\\n    try {\\n      if (binding) {\\n        var b = binding.split(PATHSEP)\\n        return b[1] + PATHSEP + doc.name;\\n      }\\n      if (path) {\\n        var p = path.split(PATHSEP);\\n        if (p.length == 3) {\\n          return p[1] + PATHSEP + doc.name;\\n        } else return doc.name;  \\n      }\\n    } catch (e) {\\n      return doc.name;\\n    }\\n    return doc.name;\\n  }\\n\\n  if (isActivation(doc)) try {\\n    var value = summarize(doc)\\n    emit([doc.namespace+PATHSEP+pathFilter(doc), doc.start], value);\\n  } catch (e) {}\\n}\\n\",\n      \"reduce\": \"_count\"\n    }\n  }\n}\n"
  },
  {
    "path": "ansible/files/whisks_design_document_for_activations_db_v2.1.0.json",
    "content": "{\n  \"_id\": \"_design/whisks.v2.1.0\",\n  \"language\": \"javascript\",\n  \"views\": {\n    \"activations\": {\n      \"map\": \"function (doc) {\\n  var PATHSEP = \\\"/\\\";\\n  var isActivation = function (doc) { return (doc.activationId !== undefined) };\\n  var summarize = function (doc) {\\n    var endtime = doc.end !== 0 ? doc.end : undefined;\\n    return {\\n        namespace: doc.namespace,\\n        name: doc.name,\\n        version: doc.version,\\n        publish: doc.publish,\\n        annotations: doc.annotations,\\n        activationId: doc.activationId,\\n        start: doc.start,\\n        end: endtime,\\n        duration: endtime !== undefined ? endtime - doc.start : undefined,\\n        cause: doc.cause,\\n        statusCode: (endtime !== undefined && doc.response !== undefined && doc.response.statusCode !== undefined) ? doc.response.statusCode : undefined\\n      }\\n  };\\n\\n  if (isActivation(doc)) try {\\n    var value = summarize(doc)\\n    emit([doc.namespace, doc.start], value);\\n  } catch (e) {}\\n}\\n\",\n      \"reduce\": \"_count\"\n    }\n  }\n}\n"
  },
  {
    "path": "ansible/files/whisks_design_document_for_entities_db_v2.1.0.json",
    "content": "{\n  \"_id\": \"_design/whisks.v2.1.0\",\n  \"language\": \"javascript\",\n  \"views\": {\n    \"rules\": {\n      \"map\": \"function (doc) {\\n  var PATHSEP = \\\"/\\\";\\n  var isRule = function (doc) {  return (doc.trigger !== undefined) };\\n  if (isRule(doc)) try {\\n    var ns = doc.namespace.split(PATHSEP);\\n    var root = ns[0];\\n    emit([doc.namespace, doc.updated], 1);\\n    if (root !== doc.namespace) {\\n      emit([root, doc.updated], 1);\\n    }\\n  } catch (e) {}\\n}\",\n      \"reduce\": \"_count\"\n    },\n    \"packages-public\": {\n      \"map\": \"function (doc) {\\n  var isPublicPackage = function(doc) { \\n    return doc.binding && doc.publish && Object.keys(doc.binding).length == 0;\\n  }\\n  if (isPublicPackage(doc)) try {\\n    var value = {\\n      namespace: doc.namespace,\\n      name: doc.name,\\n      version: doc.version,\\n      publish: doc.publish,\\n      annotations: doc.annotations,\\n      updated: doc.updated,\\n      binding: false\\n    };\\n    emit([doc.namespace, doc.updated], value);\\n  } catch (e) {}\\n}\",\n      \"reduce\": \"_count\"\n    },\n    \"packages\": {\n      \"map\": \"function (doc) {\\n  var isPackage = function (doc) {  return (doc.binding !== undefined) };\\n  if (isPackage(doc)) try {\\n    var value = {\\n      namespace: doc.namespace,\\n      name: doc.name,\\n      version: doc.version,\\n      publish: doc.publish,\\n      annotations: doc.annotations,\\n      updated: doc.updated\\n    };\\n    if (Object.keys(doc.binding).length > 0) {\\n      value.binding = doc.binding;\\n    } else {\\n      value.binding = false;\\n    }\\n    emit([doc.namespace, doc.updated], value);\\n  } catch (e) {}\\n}\",\n      \"reduce\": \"_count\"\n    },\n    \"actions\": {\n      \"map\": \"function (doc) {\\n  var PATHSEP = \\\"/\\\";\\n  var isAction = function (doc) { return (doc.exec !== undefined) };\\n  if (isAction(doc)) try {\\n    var ns = doc.namespace.split(PATHSEP);\\n    var root = ns[0];\\n    var value = {\\n      namespace: doc.namespace,\\n      name: doc.name,\\n      version: doc.version,\\n      publish: doc.publish,\\n      annotations: doc.annotations,\\n      limits: doc.limits,\\n      exec: { binary: doc.exec.binary || false},\\n      updated: doc.updated\\n    };\\n    emit([doc.namespace, doc.updated], value);\\n    if (root !== doc.namespace) {\\n      emit([root, doc.updated], value);\\n    }\\n  } catch (e) {}\\n}\",\n      \"reduce\": \"_count\"\n    },\n    \"triggers\": {\n      \"map\": \"function (doc) {\\n  var PATHSEP = \\\"/\\\";\\n  var isTrigger = function (doc) { return (doc.exec === undefined && doc.binding === undefined && doc.parameters !== undefined) };\\n  if (isTrigger(doc)) try {\\n    var ns = doc.namespace.split(PATHSEP);\\n    var root = ns[0];\\n    var value = {\\n      namespace: doc.namespace,\\n      name: doc.name,\\n      version: doc.version,\\n      publish: doc.publish,\\n      annotations: doc.annotations,\\n      updated: doc.updated\\n    };\\n    emit([doc.namespace, doc.updated], value);\\n    if (root !== doc.namespace) {\\n      emit([root, doc.updated], value);\\n    }\\n  } catch (e) {}\\n}\",\n      \"reduce\": \"_count\"\n    }\n  }\n}"
  },
  {
    "path": "ansible/group_vars/all",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nmode: deploy\nlean: false\nprompt_user: true\nopenwhisk_home: \"{{ lookup('env', 'OPENWHISK_HOME') | default(playbook_dir ~ '/..', true) }}\"\nopenwhisk_cli_home: \"{{ lookup('env', 'OPENWHISK_CLI') | default(openwhisk_home ~ '/../openwhisk-cli', true) }}\"\nexclude_logs_from: []\n\n# This whisk_api_localhost_name_default is used to configure nginx to permit vanity URLs for web actions\n# for local deployment. For a public deployment, the specific environment group vars should define\n# whisk_api_host_name; this is available to actions and hence must resolve from inside an action container\n# specific to the deployment (i.e., it may be an IP address rather than a hostname in some cases).\n# For a local deployment, use whisk_api_localhost_name. For a deployment which requires\n# different name resolution between the whisk_api_host_name and the whisk_api_local_host_name, both should\n# be defined so that the nginx configuration for the server name reflects the public facing naming (of the\n# edge router) even if it is different from the API host available to the actions. The precedence order for\n# configuring nginx and the SSL certificate generation is:\n#   whisk_api_localhost_name (first)\n#   whisk_api_host_name (second)\n#   whisk_api_localhost_name_default (last)\nwhisk_api_localhost_name_default: \"localhost\"\n\n# Type of your environment.\n# If you want to deploy everything on your local machine use 'local'.\n# If you use a docker-machine on a mac use 'docker-machine'\n# If you want to deploy Openwhisk to other machines use 'distributed'\nenvironmentInformation:\n  type: \"{{ environment_type | default('local') }}\"\n\nhosts_dir: \"{{ inventory_dir | default(env_hosts_dir) }}\"\n\nwhisk:\n  version:\n    date: \"{{ansible_date_time.iso8601}}\"\n  feature_flags:\n    require_api_key_annotation: \"{{ require_api_key_annotation | default(true) | lower }}\"\n    require_response_payload: \"{{ require_response_payload | default(true) | lower }}\"\n  cluster_name: \"{{ whisk_cluster_name | default('whisk') }}\"\n  containerProxy:\n    timeouts:\n      idleContainer: \"{{ containerProxy_timeouts_idleContainer | default('10 minutes') }}\"\n      pauseGrace: \"{{ containerProxy_timeouts_pauseGrace | default('10 seconds') }}\"\n      keepingDuration: \"{{ containerProxy_timeouts_keepingDuration | default('10 minutes') }}\"\n\n##\n# configuration parameters related to support runtimes (see org.apache.openwhisk.core.entity.ExecManifest for schema of the manifest).\n# briefly the parameters are:\n#\n#   runtimes_registry: optional registry (with trailing slash) where to pull docker images from for default runtimes (in manifest)\n#   user_images_registry: optional registry (with trailing slash) where to pull docker images from for blackbox images\n#\n#   skip_pull_runtimes: this will skip pulling the images to the invoker (images must exist there somehow)\n#\n#   runtimes_manifest: set of language runtime families grouped by language (e.g., nodejs, python) and blackbox images to pre-pull\n#\n#   runtimes_bypass_pull_for_local_images: optional, if true, allow images with a prefix that matches\n#       {{ runtimes_local_image_prefix }} to skip docker pull in invoker even if the image is not part of the blackbox set\n#\n\nmanifestfile: \"{{ manifest_file | default('/ansible/files/runtimes.json') }}\"\nruntimesManifest: \"{{ runtimes_manifest | default(lookup('file', openwhisk_home ~ '{{ manifestfile }}') | from_json) }}\"\n\nlimits:\n  invocationsPerMinute: \"{{ limit_invocations_per_minute | default(60) }}\"\n  concurrentInvocations: \"{{ limit_invocations_concurrent | default(30) }}\"\n  firesPerMinute: \"{{ limit_fires_per_minute | default(60) }}\"\n  sequenceMaxLength: \"{{ limit_sequence_max_length | default(50) }}\"\n\n# Moved here to avoid recursions. Please do not use outside of controller-dict.\n__controller_ssl_keyPrefix: \"controller-\"\n__controller_blackbox_fraction: 0.10\n\n# port means outer port\ncontroller:\n  dir:\n    become: \"{{ controller_dir_become | default(false) }}\"\n  confdir: \"{{ config_root_dir }}/controller\"\n  basePort: 10001\n  heap: \"{{ controller_heap | default('2g') }}\"\n  arguments: \"{{ controller_arguments | default('') }}\"\n  managedFraction: \"{{ controller_managed_fraction | default(1.0 - (controller_blackbox_fraction | default(__controller_blackbox_fraction))) }}\"\n  blackboxFraction: \"{{ controller_blackbox_fraction | default(__controller_blackbox_fraction) }}\"\n  timeoutFactor: \"{{ controller_timeout_factor | default(2) }}\"\n  timeoutAddon: \"{{ controller_timeout_addon | default('1 m') }}\"\n  instances: \"{{ groups['controllers'] | length }}\"\n  pekko:\n    provider: cluster\n    cluster:\n      basePort: 8000\n      host: \"{{ groups['controllers'] | map('extract', hostvars, 'ansible_host') | list }}\"\n      bindPort: 2551\n      # at this moment all controllers are seed nodes\n      seedNodes: \"{{ groups['controllers'] | map('extract', hostvars, 'ansible_host') | list }}\"\n  loadbalancer:\n    spi: \"{{ controller_loadbalancer_spi | default('') }}\"\n  authentication:\n    spi: \"{{ controller_authentication_spi | default('') }}\"\n  loglevel: \"{{ controller_loglevel | default(whisk_loglevel) | default('INFO') }}\"\n  username: \"{{ controller_username | default('controller.user') }}\"\n  password: \"{{ controller_password | default('controller.pass') }}\"\n  entitlement:\n    spi: \"{{ controller_entitlement_spi | default('') }}\"\n  protocol: \"{{ controller_protocol | default('https') }}\"\n  ssl:\n    cn: openwhisk-controllers\n    keyPrefix: \"{{ __controller_ssl_keyPrefix }}\"\n    storeFlavor: PKCS12\n    clientAuth: \"{{ controller_client_auth | default('true') }}\"\n    cert: \"{{ __controller_ssl_keyPrefix }}openwhisk-server-cert.pem\"\n    key: \"{{ __controller_ssl_keyPrefix }}openwhisk-server-key.pem\"\n    keystore:\n      password: \"openwhisk\"\n      name: \"{{ __controller_ssl_keyPrefix }}openwhisk-keystore.p12\"\n  extraEnv: \"{{ controller_extraEnv | default({}) }}\"\n  deployment:\n    ignore_error: \"{{ controller_deployment_ignore_error | default('False') }}\"\n    retries: \"{{ controller_deployment_retries | default(180) }}\"\n    delay: \"{{ controller_deployment_delay | default(5) }}\"\n\njmx:\n  basePortController: 15000\n  rmiBasePortController: 16000\n  basePortInvoker: 17000\n  rmiBasePortInvoker: 18000\n  basePortScheduler: 21000\n  rmiBasePortScheduler: 22000\n  user: \"{{ jmxuser | default('jmxuser') }}\"\n  pass: \"{{ jmxuser | default('jmxpass') }}\"\n  jvmCommonArgs: \"-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=/home/owuser/jmxremote.password -Dcom.sun.management.jmxremote.access.file=/home/owuser/jmxremote.access\"\n  enabled: \"{{ jmxremote_enabled | default('true') }}\"\n\ntransactions:\n  header: \"{{ transactions_header | default('X-Request-ID') }}\"\n\nregistry:\n  confdir: \"{{ config_root_dir }}/registry\"\n\nkafka:\n  topicsPrefix: \"{{ kafka_topics_prefix | default('') }}\"\n  topicsUserEventPrefix: \"{{ kafka_topics_userEvent_prefix | default(kafka_topics_prefix) | default('') }}\"\n  ssl:\n    client_authentication: required\n    keystore:\n      name: kafka-keystore.jks\n      password: openwhisk\n    cipher_suites:\n    - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\n    - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\n    - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\n    protocols:\n    - TLSv1.2\n  protocol: \"{{ kafka_protocol_for_setup }}\"\n  version: \"{{ kafka_version | default('2.13-2.7.0') }}\"\n  port: 9072\n  advertisedPort: 9093\n  ras:\n    port: 8093\n  heap: \"{{ kafka_heap | default('1g') }}\"\n  replicationFactor: \"{{ kafka_replicationFactor | default((groups['kafkas']|length)|int) }}\"\n  offsetsTopicReplicationFactor: \"{{ kafka_offsetsTopicReplicationFactor | default(kafka_replicationFactor) | default((groups['kafkas']|length)|int) }}\"\n  # adapt this param for production deployments depending on the number of kafka consumers\n  networkThreads: \"{{ kafka_network_threads | default(3) }}\"\n\nkafka_connect_string: \"{% set ret = [] %}\\\n                       {% for host in groups['kafkas'] %}\\\n                         {{ ret.append( hostvars[host].ansible_host + ':' + ((kafka.advertisedPort+loop.index-1)|string) ) }}\\\n                       {% endfor %}\\\n                       {{ ret | join(',') }}\"\n\nkafka_protocol_for_setup: \"{{ kafka_protocol | default('PLAINTEXT') }}\"\n\nzookeeper:\n  image:\n    amd64: zookeeper:3.4\n    arm64: arm64v8/zookeeper:3.4\n  port: 2181\n\nzookeeper_connect_string: \"{% set ret = [] %}\\\n                           {% for host in groups['zookeepers'] %}\\\n                             {{ ret.append( hostvars[host].ansible_host + ':' + ((zookeeper.port+loop.index-1)|string) ) }}\\\n                           {% endfor %}\\\n                           {{ ret | join(',') }}\"\n\ninvokerHostnameFromMap: \"{{ groups['invokers'] | map('extract', hostvars, 'ansible_host') | list | first }}\"\ninvokerHostname: \"{{ invokerHostnameFromMap | default(inventory_hostname) }}\"\n\n# Moved here to avoid recursions. Please do not use outside of invoker-dict.\n__invoker_ssl_keyPrefix: \"invoker-\"\n\ninvoker:\n  dir:\n    become: \"{{ invoker_dir_become | default(false) }}\"\n  confdir: \"{{ config_root_dir }}/invoker\"\n  port: 12001\n  heap: \"{{ invoker_heap | default('2g') }}\"\n  arguments: \"{{ invoker_arguments | default('') }}\"\n  userMemory: \"{{ invoker_user_memory | default('2048m') }}\"\n  userCpus: \"{{ invoker_user_cpus | default() }}\"\n  # Specify if it is allowed to deploy more than 1 invoker on a single machine.\n  allowMultipleInstances: \"{{ invoker_allow_multiple_instances | default(false) }}\"\n  # Specify if it should use runc or docker to pause/unpause containers\n  useRunc: \"{{ invoker_use_runc | default(true) }}\"\n  docker:\n    become: \"{{ invoker_docker_become | default(false) }}\"\n    runcdir: \"{{ invoker_runcdir | default('/run/docker/runtime-runc/moby') }}\"\n    volumes: \"{{ invoker_docker_volumes | default([]) }}\"\n  loglevel: \"{{ invoker_loglevel | default(whisk_loglevel) | default('INFO') }}\"\n  jmxremote:\n    jvmArgs: \"{% if inventory_hostname in groups['invokers'] %}\n    {{ jmx.jvmCommonArgs }} -Djava.rmi.server.hostname={{ invokerHostname }} -Dcom.sun.management.jmxremote.rmi.port={{ jmx.rmiBasePortInvoker + groups['invokers'].index(inventory_hostname) }} -Dcom.sun.management.jmxremote.port={{ jmx.basePortInvoker + groups['invokers'].index(inventory_hostname) }}\n    {% endif %}\"\n  extraEnv: \"{{ invoker_extraEnv | default({}) }}\"\n  protocol: \"{{ invoker_protocol | default('https') }}\"\n  username: \"{{ invoker_username | default('invoker.user') }}\"\n  password: \"{{ invoker_password | default('invoker.pass') }}\"\n  ssl:\n    cn: \"openwhisk-invokers\"\n    keyPrefix: \"{{ __invoker_ssl_keyPrefix }}\"\n    storeFlavor: \"PKCS12\"\n    clientAuth: \"{{ invoker_client_auth | default('true') }}\"\n    cert: \"{{ __invoker_ssl_keyPrefix }}openwhisk-server-cert.pem\"\n    key: \"{{ __invoker_ssl_keyPrefix }}openwhisk-server-key.pem\"\n    keystore:\n      password: \"{{ invoker_keystore_password | default('openwhisk') }}\"\n      name: \"{{ __invoker_ssl_keyPrefix }}openwhisk-keystore.p12\"\n  container:\n    creationMaxPeek: \"{{ container_creation_max_peek | default(500) }}\"\n  reactiveSpi: \"{{ invokerReactive_spi | default('') }}\"\n  serverSpi: \"{{ invokerServer_spi | default('') }}\"\n  deployment:\n    ignore_error: \"{{ invoker_deployment_ignore_error | default('False') }}\"\n    retries: \"{{ invoker_deployment_retries | default(180) }}\"\n    delay: \"{{ invoker_deployment_delay | default(5) }}\"\n\nuserLogs:\n  spi: \"{{ userLogs_spi | default('org.apache.openwhisk.core.containerpool.logging.DockerToActivationLogStoreProvider') }}\"\n\nnginx:\n  confdir: \"{{ config_root_dir }}/nginx\"\n  htmldir: \"{{ ui_path | default(false) }}\"\n  dir:\n    become: \"{{ nginx_dir_become | default(false) }}\"\n  version: \"{{ nginx_version | default('1.21.1') }}\"\n  port:\n    http: 80\n    https: 443\n  ssl:\n    path: \"{{ nginx_ssl_path | default(playbook_dir +'/roles/nginx/files') }}\"\n    cert: \"{{ nginx_ssl_server_cert | default('openwhisk-server-cert.pem') }}\"\n    key: \"{{ nginx_ssl_server_key | default('openwhisk-server-key.pem') }}\"\n    client_ca_cert: \"{{ nginx_ssl_client_ca_cert | default('openwhisk-client-ca-cert.pem') }}\"\n    verify_client: \"{{ nginx_ssl_verify_client | default('off') }}\"\n    password_file: \"{{ nginx_ssl_password_file | default(false) }}\"\n  wpn:\n    router: \"{{ nginx_wpn_router | default('1') }}\"\n  special_users: \"{{ nginx_special_users | default('[]') }}\"\n\n# These are the variables to define all database relevant settings.\n# The authKeys are the users, that are initially created to use OpenWhisk.\n# The keys are stored in ansible/files and will be inserted into the authentication database.\n# The key db.whisk.actions is the name of the database where all artifacts of the user are stored. These artifacts are actions, triggers, rules and packages.\n# The key db.whisk.activation is the name of the database where all activations are stored.\n# The key db.whisk.auth is the name of the authentication database where all keys of all users are stored.\n# The db_prefix is defined for each environment on its own. The CouchDb credentials are also defined for each environment on its own.\ndb:\n  provider: \"{{ db_provider | default(lookup('ini', 'db_provider section=db_creds file={{ playbook_dir }}/db_local.ini')) }}\"\n  protocol: \"{{ db_protocol | default(lookup('ini', 'db_protocol section=db_creds file={{ playbook_dir }}/db_local.ini')) }}\"\n  port: \"{{ db_port | default(lookup('ini', 'db_port section=db_creds file={{ playbook_dir }}/db_local.ini')) }}\"\n  host: \"{{ db_host | default(lookup('ini', 'db_host section=db_creds file={{ playbook_dir }}/db_local.ini')) }}\"\n  persist_path: \"{{ db_persist_path | default(false) }}\"\n  instances: \"{{ groups['db'] | length }}\"\n  authkeys:\n  - guest\n  - whisk.system\n  whisk:\n    actions: \"{{ db_prefix }}whisks\"\n    activations: \"{{ db_prefix }}activations\"\n    auth: \"{{ db_prefix }}subjects\"\n  credentials:\n    admin:\n      user: \"{{ db_username | default(lookup('ini', 'db_username section=db_creds file={{ playbook_dir }}/db_local.ini')) }}\"\n      pass: \"{{ db_password | default(lookup('ini', 'db_password section=db_creds file={{ playbook_dir }}/db_local.ini')) }}\"\n    controller:\n      user: \"{{ db_controller_user | default(lookup('ini', 'db_username section=controller file={{ playbook_dir }}/db_local.ini')) }}\"\n      pass: \"{{ db_controller_pass | default(lookup('ini', 'db_password section=controller file={{ playbook_dir }}/db_local.ini')) }}\"\n    invoker:\n      user: \"{{ db_invoker_user | default(lookup('ini', 'db_username section=invoker file={{ playbook_dir }}/db_local.ini')) }}\"\n      pass: \"{{ db_invoker_pass | default(lookup('ini', 'db_password section=invoker file={{ playbook_dir }}/db_local.ini')) }}\"\n    scheduler:\n      user: \"{{ db_scheduler_user | default(lookup('ini', 'db_username section=scheduler file={{ playbook_dir }}/db_local.ini')) }}\"\n      pass: \"{{ db_scheduler_pass | default(lookup('ini', 'db_password section=scheduler file={{ playbook_dir }}/db_local.ini')) }}\"\n  artifact_store:\n    backend: \"{{ db_artifact_backend | default('CouchDB') }}\"\n  activation_store:\n    backend: \"{{ db_activation_backend | default('CouchDB') }}\"\n  elasticsearch:\n    protocol: \"{{ elastic_protocol | default('http') }}\"\n    port: \"{{ elastic_port | default(9200) }}\"\n    index_pattern: \"{{ elastic_index_pattern | default('openwhisk-%s') }}\"\n    base_transport_port: \"{{ elastic_transport_port_base | default(9300) }}\"\n    confdir: \"{{ config_root_dir }}/elasticsearch\"\n    dir:\n      become: \"{{ elastic_dir_become | default(false) }}\"\n    base_volume: \"{{ elastic_base_volume | default('esdata') }}\"\n    cluster_name: \"{{ elastic_cluster_name | default('openwhisk') }}\"\n    java_opts: \"{{ elastic_java_opts | default('-Xms1g -Xmx1g') }}\"\n    loglevel: \"{{ elastic_loglevel | default('INFO') }}\"\n    # the user id of elasticsearch process, default is 1000, if you have enabled user namespace\n    # for docker daemon, this need to be changed correspondingly\n    uid: \"{{ elastic_uid | default(1000) }}\"\n    auth:\n      admin:\n        username: \"{{ elastic_username | default('admin') }}\"\n        password: \"{{ elastic_password | default('admin') }}\"\n  mongodb:\n    connect_string: \"{{ mongodb_connect_string | default('mongodb://172.17.0.1:27017') }}\"\n    database: \"{{ mongodb_database | default('whisks') }}\"\n    data_volume: \"{{ mongodb_data_volume | default('mongo-data') }}\"\n\napigateway:\n  port:\n    api: 9000\n    mgmt: 9001\n  # Default to 'nightly', which tracks the head revision of the master branch of apigateway's gitrepo\n  version: nightly\n\nredis:\n  version: 4.0\n  port: 6379\n  password: openwhisk\n\nlinux:\n  version: 4.4.0-31\n\ncouchdb:\n  version: 2.3\n\nelasticsearch:\n  version: 6.7.2\n\nelasticsearch_connect_string: \"{% set ret = [] %}\\\n                               {% for host in groups['elasticsearch'] %}\\\n                               {{ ret.append( hostvars[host].ansible_host + ':' + ((db.elasticsearch.port|int+loop.index-1)|string) ) }}\\\n                               {% endfor %}\\\n                               {{ ret | join(',') }}\"\nmongodb:\n  version: 4.4.0\n\ndocker:\n  # The user to install docker for. Defaults to the ansible user if not set. This will be the user who is able to run\n  # docker commands on a machine setup with prereq_build.yml\n  #user:\n  image:\n    prefix: \"{{ docker_image_prefix | default('whisk') }}\"\n    tag: \"{{ docker_image_tag | default('latest') }}\"\n  version: 1.12.0-0~trusty\n  storagedriver: overlay\n  port: 4243\n  restart:\n    policy: always\n  pull:\n    retries: 10\n    delay: 10\n  timezone: \"{{ docker_timezone | default('UTC') }}\"\n\ncli:\n  path: \"{{ openwhisk_home }}/bin/wsk\"\n\n# The default name space is /whisk.system. The catalog namespace must begin with a slash \"/\".\ncatalog_namespace: \"/whisk.system\"\n\n# The catalog_auth_key is used to determine the secret key to authenticate the openwhisk service.\n# The value for this variable can be set to either the secret key itself or the file, which\n# saves the secret key.\n# By default, we take the key from ansible/files/auth.whisk.system.\ncatalog_auth_key: \"{{ playbook_dir }}/files/auth.whisk.system\"\n\n# The catalog_repos is used to specify all the catalog names and repository URLs,\n# so that openwhisk knows where to download the catalog and install them. The key\n# specifies the catalog name and the url saves the URL of the repository. The location\n# specifies the location to save the code of the catalog. The version specifies the hash\n# of the commit to be cloned. If it is omit or set to HEAD, the latest commit will be\n# selected. The repo_update specifies whether to retrieve new revisions from the origin\n# repository and the default value is yes, meaning that it will retrieve the new\n# revisions. The keys url and location are mandatory and the keys version and repo_update\n# are optional. To add a new repository, please follow the template by adding:\n#\n# catalog_repos:\n#   ...\n#   <catalog-name>:\n#     url: <URL of repository>, mandatory.\n#     location: <local location to save the catalog>, mandatory.\n#     version: <hash of the commit>, optional, default to HEAD.\n#     repo_update: <whether to retrieve new revisions from the origin repository>,\n#                  optional, default to no. Yes means to retrieve the new revisions, and\n#                  no means not to retrieve the new revisions.\n#\ncatalog_repos:\n  openwhisk-catalog:\n    url: https://github.com/apache/openwhisk-catalog.git\n    # Set the local location as the same level as openwhisk home, but it can be changed.\n    location: \"{{ openwhisk_home }}/../openwhisk-catalog\"\n    version: \"HEAD\"\n    repo_update: \"no\"\n\n# The openwhisk_cli is used to determine how to install the OpenWhisk CLI. The\n# installation_mode can be specified into two modes: remote and local.\n# The mode remote means to download the available binaries from the releases page\n# of the official openwhisk cli repository. The mode local means to build the binaries\n# locally in a directory and get them from the local directory. The default value\n# for openwhisk is local.\n#\n# The name specifies the package name of the binaries in remote mode.\n#\n# The dest_name specifies the package name of the binaries in Nginx in remote mode.\n#\n# The location specifies the official website where Openwhisk CLI is hosted in\n# remote mode or location to save the binaries of the OpenWhisk CLI in local mode.\n\nopenwhisk_cli_tag: \"{{ cli_tag | default(lookup('ini', 'git_tag section=openwhisk-cli file=' ~ openwhisk_home ~ '/ansible/files/package-versions.ini')) }}\"\nopenwhisk_cli:\n  installation_mode: \"{{ cli_installation_mode | default(lookup('env', 'OPENWHISK_CLI_MODE')) | default('remote', true) }}\"\n  archive_name: OpenWhisk_CLI\n  nginxdir:\n    name: \"{{ nginx.confdir }}/cli/go/download\"\n    become: \"{{ cli_dir_become | default(false) }}\"\n  local:\n    location: \"{{ openwhisk_cli_home }}/build\"\n  remote:\n    location: \"https://github.com/apache/openwhisk-cli/releases/download/{{ openwhisk_cli_tag }}\"\n\n# Controls access to log directories\nlogs:\n  dir:\n    become: \"{{ logs_dir_become | default(false) }}\"\n\n# Metrics Configuration\nmetrics:\n  log:\n    enabled: \"{{ metrics_log | default(true) }}\"\n  kamon:\n    enabled: \"{{ metrics_kamon | default(false) }}\"\n    tags: \"{{ metrics_kamon_tags | default(false) }}\"\n    host: \"{{ metrics_kamon_statsd_host | default('') }}\"\n    port: \"{{ metrics_kamon_statsd_port | default('8125') }}\"\n\nuser_events: \"{{ user_events_enabled | default(false) | lower }}\"\n\nzeroDowntimeDeployment:\n  enabled: \"{{ zerodowntime_deployment_switch | default(false) }}\"\n\netcd:\n  version: \"{{ etcd_version | default('3.5.21') }}\"\n  client:\n    port: 2379\n  server:\n    port: 2480\n  cluster:\n    token: \"{{ etcd_cluster_token | default('openwhisk-etcd-token') }}\"\n  dir:\n    data: \"{{ etcd_data_dir | default('') }}\"\n  lease:\n    timeout: \"{{ etcd_lease_timeout | default(10) }}\"\n  loglevel: \"{{ etcd_log_level | default('info') }}\"\n  quota_backend_bytes: \"{{ etcd_quota_backend_bytes | default(2147483648) }}\" # 2GB\n  snapshot_count: \"{{ etcd_snapshot_count | default(10000) }}\"\n  auto_compaction_retention: \"{{ etcd_auto_compaction_retention | default('10m') }}\"\n  auto_compaction_mode: \"{{ etcd_auto_compaction_mode | default('periodic') }}\"\n  pool_threads: \"{{ etcd_pool_threads | default(10) }}\"\n\netcd_connect_string: \"{% set ret = [] %}\\\n                      {% for host in groups['etcd'] %}\\\n                        {{ ret.append( hostvars[host].ansible_host + ':' + ((etcd.client.port+loop.index-1)|string) ) }}\\\n                      {% endfor %}\\\n                      {{ ret | join(',') }}\"\n\n\n__scheduler_blackbox_fraction: 0.10\n\nwatcher:\n    eventNotificationDelayMs: \"{{ watcher_notification_delay | default('5000 ms')  }}\"\n\ndurationChecker:\n    spi: \"{{ duration_checker_spi | default('') }}\"\n    timeWindow: \"{{ duration_checker_time_window | default('1 d') }}\"\n\nenable_scheduler: \"{{ scheduler_enable | default(true) }}\"\n\nscheduler:\n  protocol: \"{{ scheduler_protocol | default('http') }}\"\n  dir:\n    become: \"{{ scheduler_dir_become | default(false) }}\"\n  confdir: \"{{ config_root_dir }}/scheduler\"\n  basePort: 14001\n  grpc:\n    basePort: 13001\n    tls: \"{{ scheduler_grpc_tls | default(false) }}\"\n  maxPeek: \"{{ scheduler_max_peek | default(128) }}\"\n  heap: \"{{ scheduler_heap | default('2g') }}\"\n  arguments: \"{{ scheduler_arguments | default('') }}\"\n  instances: \"{{ groups['schedulers'] | length }}\"\n  username: \"{{ scheduler_username | default('scheduler.user') }}\"\n  password: \"{{ scheduler_password | default('scheduler.pass') }}\"\n  pekko:\n    provider: cluster\n    cluster:\n      basePort: 17355\n      host: \"{{ groups['schedulers'] | map('extract', hostvars, 'ansible_host') | list }}\"\n      bindPort: 3551\n      # at this moment all schedulers are seed nodes\n      seedNodes: \"{{ groups['schedulers'] | map('extract', hostvars, 'ansible_host') | list }}\"\n  loglevel: \"{{ scheduler_loglevel | default(whisk_loglevel) | default('INFO') }}\"\n  extraEnv: \"{{ scheduler_extraEnv | default({}) }}\"\n  dataManagementService:\n    retryInterval: \"{{ scheduler_dataManagementService_retryInterval | default('1 second') }}\"\n  inProgressJobRetention: \"{{ scheduler_inProgressJobRetention | default('20 seconds') }}\"\n  blackboxMultiple: \"{{ scheduler_blackboxMultiple | default(15) }}\"\n  managedFraction: \"{{ scheduler_managed_fraction | default(1.0 - (scheduler_blackbox_fraction | default(__scheduler_blackbox_fraction))) }}\"\n  blackboxFraction: \"{{ scheduler_blackbox_fraction | default(__scheduler_blackbox_fraction) }}\"\n  scheduling:\n    staleThreshold: \"{{ scheduler_scheduling_staleThreshold | default('100 milliseconds') }}\"\n    checkInterval: \"{{ scheduler_scheduling_checkInterval | default('100 milliseconds') }}\"\n    dropInterval: \"{{ scheduler_scheduling_dropInterval | default('10 seconds') }}\"\n  queueManager:\n    maxSchedulingTime: \"{{ scheduler_maxSchedulingTime | default('20 second') }}\"\n    maxRetriesToGetQueue: \"{{ scheduler_maxRetriesToGetQueue | default(13) }}\"\n  queue:\n    # the queue's state Running timeout, e.g. if have no activation comes into queue when Running, the queue state will be changed from Running to Idle and delete the decision algorithm actor\n    idleGrace: \"{{ scheduler_queue_idleGrace | default('10 minutes') }}\"\n    # the queue's state Idle timeout, e.g. if have no activation comes into queue when Idle, the queue state will be changed from Idle to Removed\n    stopGrace: \"{{ scheduler_queue_stopGrace | default('10 minutes') }}\"\n    # the queue's state Paused timeout, e.g. if have no activation comes into queue when Paused, the queue state will be changed from Paused to Removed\n    flushGrace: \"{{ scheduler_queue_flushGrace | default('60 seconds') }}\"\n    gracefulShutdownTimeout: \"{{ scheduler_queue_gracefulShutdownTimeout | default('5 seconds') }}\"\n    maxRetentionSize: \"{{ scheduler_queue_maxRetentionSize | default(10000) }}\"\n    maxRetentionMs: \"{{ scheduler_queue_maxRetentionMs | default(60000) }}\"\n    maxBlackboxRetentionMs: \"{{ scheduler_queue_maxBlackboxRetentionMs | default(300000) }}\"\n    throttlingFraction: \"{{ scheduler_queue_throttlingFraction | default(0.9) }}\"\n    durationBufferSize: \"{{ scheduler_queue_durationBufferSize | default(10) }}\"\n  deployment_ignore_error: \"{{ scheduler_deployment_ignore_error | default('False') }}\"\n"
  },
  {
    "path": "ansible/initMongoDB.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook will initialize the immortal DBs in the database account.\n# This step is usually done only once per deployment.\n\n- hosts: ansible\n  tasks:\n  - name: create necessary auth keys\n    mongodb:\n      connect_string: \"{{ db.mongodb.connect_string }}\"\n      database: \"{{ db.mongodb.database }}\"\n      collection: \"whiskauth\"\n      doc:\n        _id: \"{{ item }}\"\n        subject: \"{{ item }}\"\n        namespaces:\n          - name: \"{{ item }}\"\n            uuid: \"{{ key.split(':')[0] }}\"\n            key: \"{{ key.split(':')[1] }}\"\n      mode: \"doc\"\n      force_update: True\n    vars:\n      key: \"{{ lookup('file', 'files/auth.{{ item }}') }}\"\n    with_items: \"{{ db.authkeys }}\"\n"
  },
  {
    "path": "ansible/initdb.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook will initialize the immortal DBs in the database account.\n# This step is usually done only once per deployment.\n\n- hosts: ansible\n  tasks:\n  - include_tasks: tasks/initdb.yml\n"
  },
  {
    "path": "ansible/invoker.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys Openwhisk Invokers.\n\n- hosts: invokers\n  vars:\n    #\n    # host_group - usually \"{{ groups['...'] }}\" where '...' is what was used\n    #   for 'hosts' above.  The hostname of each host will be looked up in this\n    #   group to assign a zero-based index.  That index will be used in concert\n    #   with 'name_prefix' below to assign a host/container name.\n    host_group: \"{{ groups['invokers'] }}\"\n    #\n    # name_prefix - a unique prefix for this set of invokers.  The prefix\n    #   will be used in combination with an index (determined using\n    #   'host_group' above) to name host/invokers.\n    name_prefix: \"invoker\"\n    #\n    # invoker_index_base - the deployment process allocates host docker\n    #   ports to individual invokers based on their indices.  This is an\n    #   additional offset to prevent collisions between different invoker\n    #   groups. Usually 0 if only one group is being deployed, otherwise\n    #   something like \"{{ groups['firstinvokergroup']|length }}\"\n    invoker_index_base: 0\n\n  roles:\n    - invoker\n"
  },
  {
    "path": "ansible/kafka.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys an Openwhisk Kafka bus.\n\n- hosts: zookeepers\n  roles:\n  - zookeeper\n\n- hosts: kafkas\n  roles:\n  - kafka\n"
  },
  {
    "path": "ansible/library/mongodb.py",
    "content": "#!/usr/bin/python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom __future__ import absolute_import, division, print_function\n__metaclass__ = type\n\n\nDOCUMENTATION = '''\n---\nmodule: mongodb\nshort_description:  A module which support some simple operations on MongoDB.\ndescription:\n    - Including add user/insert document/create indexes in MongoDB\noptions:\n    connect_string:\n        description:\n            - The uri of mongodb server\n        required: true\n    database:\n        description:\n            - The name of the database you want to manipulate\n        required: true\n    user:\n        description:\n            - The name of the user to add or remove, required when use 'user' mode\n        required: false\n        default: null\n    password:\n        description:\n            - The password to use for the user, required when use 'user' mode\n        required: false\n        default: null\n    roles:\n        description:\n            - The roles of the user, it's a list of dict, each dict requires two fields: 'db' and 'role', required when use 'user' mode\n        required: false\n        default: null\n    collection:\n        required: false\n        description:\n            - The name of the collection you want to manipulate, required when use 'doc' or 'indexes' mode\n    doc:\n        required: false\n        description:\n            - The document you want to insert into MongoDB, required when use 'doc' mode\n    indexes:\n        required: false\n        description:\n            - The indexes you want to create in MongoDB, it's a list of dict, you can see the example for the usage, required when use 'index' mode\n    force_update:\n        required: false\n        description:\n            - Whether replace/update existing user or doc or raise DuplicateKeyError, default is false\n    mode:\n        required: false\n        default: user\n        choices: ['user', 'doc', 'index']\n        description:\n            - use 'user' mode if you want to add user, 'doc' mode to insert document, 'index' mode to create indexes\n\nrequirements: [ \"pymongo\" ]\nauthor:\n    - \"Jinag PengCheng\"\n'''\n\nEXAMPLES = '''\n# add user\n- mongodb:\n    connect_string: mongodb://localhost:27017\n    database: admin\n    user: test\n    password: 123456\n    roles:\n      - db: test_database\n        role: read\n    force_update: true\n\n# add doc\n- mongodb:\n    connect_string: mongodb://localhost:27017\n    mode: doc\n    database: admin\n    collection: main\n    doc:\n      id: \"id/document\"\n      title: \"the name of document\"\n      content: \"which doesn't matter\"\n    force_update: true\n\n# add indexes\n- mongodb:\n    connect_string: mongodb://localhost:27017\n    mode: index\n    database: admin\n    collection: main\n    indexes:\n      - index:\n        - field: updated_at\n          direction: 1\n        - field: name\n          direction: -1\n        name: test-index\n        unique: true\n'''\n\nimport traceback\n\nfrom ansible.module_utils.basic import AnsibleModule\nfrom ansible.module_utils._text import to_native\n\ntry:\n    from pymongo import ASCENDING, DESCENDING, GEO2D, GEOHAYSTACK, GEOSPHERE, HASHED, TEXT\n    from pymongo import IndexModel\n    from pymongo import MongoClient\n    from pymongo.errors import DuplicateKeyError\nexcept ImportError:\n    pass\n\n\n# =========================================\n# MongoDB module specific support methods.\n#\n\nclass UnknownIndexPlugin(Exception):\n    pass\n\n\ndef check_params(params, mode, module):\n    missed_params = []\n    for key in OPERATIONS[mode]['required']:\n        if params[key] is None:\n            missed_params.append(key)\n\n    if missed_params:\n        module.fail_json(msg=\"missing required arguments: %s\" % (\",\".join(missed_params)))\n\n\ndef _recreate_user(module, db, user, password, roles):\n    try:\n        db.command(\"dropUser\", user)\n        db.command(\"createUser\", user, pwd=password, roles=roles)\n    except Exception as e:\n        module.fail_json(msg='Unable to create user: %s' % to_native(e), exception=traceback.format_exc())\n\n\n\ndef user(module, client, db_name, **kwargs):\n    roles = kwargs['roles']\n    if roles is None:\n        roles = []\n    db = client[db_name]\n\n    try:\n        db.command(\"createUser\", kwargs['user'], pwd=kwargs['password'], roles=roles)\n    except DuplicateKeyError as e:\n        if kwargs['force_update']:\n            _recreate_user(module, db, kwargs['user'], kwargs['password'], roles)\n        else:\n            module.fail_json(msg='Unable to create user: %s' % to_native(e), exception=traceback.format_exc())\n    except Exception as e:\n        module.fail_json(msg='Unable to create user: %s' % to_native(e), exception=traceback.format_exc())\n\n    module.exit_json(changed=True, user=kwargs['user'])\n\n\ndef doc(module, client, db_name, **kwargs):\n    coll = client[db_name][kwargs['collection']]\n    try:\n        coll.insert_one(kwargs['doc'])\n    except DuplicateKeyError as e:\n        if kwargs['force_update']:\n            try:\n                coll.replace_one({'_id': kwargs['doc']['_id']}, kwargs['doc'])\n            except Exception as e:\n                module.fail_json(msg='Unable to insert doc: %s' % to_native(e), exception=traceback.format_exc())\n        else:\n            module.fail_json(msg='Unable to insert doc: %s' % to_native(e), exception=traceback.format_exc())\n    except Exception as e:\n        module.fail_json(msg='Unable to insert doc: %s' % to_native(e), exception=traceback.format_exc())\n\n    kwargs['doc']['_id'] = str(kwargs['doc']['_id'])\n    module.exit_json(changed=True, doc=kwargs['doc'])\n\n\ndef _clean_index_direction(direction):\n    if direction in [\"1\", \"-1\"]:\n        direction = int(direction)\n\n    if direction not in [ASCENDING, DESCENDING, GEO2D, GEOHAYSTACK, GEOSPHERE, HASHED, TEXT]:\n        raise UnknownIndexPlugin(\"Unable to create indexes: Unknown index plugin: %s\" % direction)\n    return direction\n\n\ndef _clean_index_options(options):\n    res = {}\n    supported_options = set(['name', 'unique', 'background', 'sparse', 'bucketSize', 'min', 'max', 'expireAfterSeconds'])\n    for key in set(options.keys()).intersection(supported_options):\n        res[key] = options[key]\n        if key in ['min', 'max', 'bucketSize', 'expireAfterSeconds']:\n            res[key] = int(res[key])\n\n    return res\n\n\ndef parse_indexes(idx):\n    keys = [(k['field'], _clean_index_direction(k['direction'])) for k in idx.pop('index')]\n    options = _clean_index_options(idx)\n    return IndexModel(keys, **options)\n\n\ndef index(module, client, db_name, **kwargs):\n    parsed_indexes = map(parse_indexes, kwargs['indexes'])\n    try:\n        coll = client[db_name][kwargs['collection']]\n        coll.create_indexes(parsed_indexes)\n    except Exception as e:\n        module.fail_json(msg='Unable to create indexes: %s' % to_native(e), exception=traceback.format_exc())\n\n    module.exit_json(changed=True, indexes=kwargs['indexes'])\n\n\nOPERATIONS = {\n    'user': { 'function': user, 'params': ['user', 'password', 'roles', 'force_update'], 'required': ['user', 'password']},\n    'doc': {'function': doc, 'params': ['doc', 'collection', 'force_update'], 'required': ['doc', 'collection']},\n    'index': {'function': index, 'params': ['indexes', 'collection'], 'required': ['indexes', 'collection']}\n}\n\n\n# =========================================\n# Module execution.\n#\n\ndef main():\n    module = AnsibleModule(\n        argument_spec=dict(\n            connect_string=dict(required=True),\n            database=dict(required=True, aliases=['db']),\n            mode=dict(default='user', choices=['user', 'doc', 'index']),\n            user=dict(default=None),\n            password=dict(default=None, no_log=True),\n            roles=dict(default=None, type='list'),\n            collection=dict(default=None),\n            doc=dict(default=None, type='dict'),\n            force_update=dict(default=False, type='bool'),\n            indexes=dict(default=None, type='list'),\n        )\n    )\n\n    mode = module.params['mode']\n\n    db_name = module.params['database']\n\n    params = {key: module.params[key] for key in OPERATIONS[mode]['params']}\n    check_params(params, mode, module)\n\n    try:\n        client = MongoClient(module.params['connect_string'])\n    except NameError:\n        module.fail_json(msg='the python pymongo module is required')\n    except Exception as e:\n        module.fail_json(msg='unable to connect to database: %s' % to_native(e), exception=traceback.format_exc())\n\n    OPERATIONS[mode]['function'](module, client, db_name, **params)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "ansible/logs.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook is used for utilities around logs\n\n- import_playbook: properties.yml\n\n- hosts: ansible\n  tasks:\n    - name: remove \"logs\" folder\n      file: path=\"{{ openwhisk_home }}/logs\" state=absent\n    - name: create \"logs\" folder\n      file: path=\"{{ openwhisk_home }}/logs\" state=directory\n    - name: dump entity views\n      local_action: shell \"{{ openwhisk_home }}/bin/wskadmin\" db get whisks --docs --view whisks.v2.1.0/{{ item }} | tail -n +2 > \"{{ openwhisk_home }}/logs/db-{{ item }}.log\"\n      with_items:\n        - actions\n        - triggers\n        - rules\n        - packages\n      when: \"'db' not in exclude_logs_from\"\n    - name: dump activations and subjects database\n      local_action: shell \"{{ openwhisk_home }}/bin/wskadmin\" db get {{ item }} --docs | tail -n +2 > \"{{ openwhisk_home }}/logs/db-{{ item }}.log\"\n      with_items:\n        - activations\n        - subjects\n      when: \"'db' not in exclude_logs_from\"\n    - name: create \"test reports\" folder\n      file: path=\"{{ openwhisk_home }}/logs/test-reports\" state=directory\n    - name: collect test reports\n      local_action: shell cp -r \"{{ openwhisk_home }}/tests/build/{{ item }}\" \"{{ openwhisk_home }}/logs/test-reports/\"\n      with_items:\n        - reports\n        - test-results\n      ignore_errors: true\n      when: \"'tests' not in exclude_logs_from\"\n    - name: create \"perf reports\" folder\n      file: path=\"{{ openwhisk_home }}/logs/perf-reports\" state=directory\n    - name: collect perf logs\n      local_action: shell cp  \"{{ openwhisk_home }}/tests/performance/gatling_tests/build/gatling.log\" \"{{ openwhisk_home }}/logs/perf-reports/\"\n      ignore_errors: true\n\n- hosts: all:!ansible\n  serial: 1\n  tasks:\n  - name: init var docker_host_flag\n    set_fact:\n      docker_host_flag: \"\"\n  - name: set host flag when using docker remote API\n    set_fact:\n      docker_host_flag: \"--host tcp://{{ ansible_host }}:{{ docker.port }}\"\n    when: environmentInformation.type != \"local\"\n  - name: get all docker containers\n    local_action: shell docker {{ docker_host_flag }} ps -a --format=\"{% raw %}{{.Names}}{% endraw %}\"\n    register: container_names\n  - name: get logs from all containers\n    local_action: shell docker {{ docker_host_flag }} logs {{ item }} > \"{{ openwhisk_home }}/logs/{{ item }}.log\" 2>&1; exit 0\n    with_items: \"{{ container_names.stdout_lines | difference('whisk_docker_registry') }}\"\n    when: \"'docker' not in exclude_logs_from\"\n  - name: workaround to make synchronize work\n    set_fact:\n      ansible_ssh_private_key_file: \"{{ ansible_ssh_private_key_file }}\"\n    when: ansible_ssh_private_key_file is defined\n  - name: fetch logs from all machines\n    synchronize: src=\"{{ whisk_logs_dir }}/\" dest=\"{{ openwhisk_home }}/logs\" mode=pull\n    when: \"'machine' not in exclude_logs_from\"\n    ignore_errors: true\n"
  },
  {
    "path": "ansible/mongodb.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys a MongoDB for Openwhisk.\n\n- hosts: localhost\n  tasks:\n  - name: check if db_local.ini exists?\n    tags: ini\n    stat: path=\"{{ playbook_dir }}/db_local.ini\"\n    register: db_check\n\n  - name: prepare db_local.ini\n    tags: ini\n    local_action: template src=\"db_local.ini.j2\" dest=\"{{ playbook_dir }}/db_local.ini\"\n    when: not db_check.stat.exists\n\n# This is for test, only deploy it on the first node, please use a shard cluster mongodb for\n# production env\n- hosts: db[0]\n  roles:\n    - mongodb\n"
  },
  {
    "path": "ansible/openwhisk.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys an Openwhisk stack.\n# It assumes you have already set up your database with the respective db provider\n# playbook (currently cloudant.yml or couchdb.yml).\n# It assumes that wipe.yml have being deployed at least once.\n\n- import_playbook: etcd.yml\n  when: enable_scheduler\n\n- import_playbook: kafka.yml\n  when: not lean\n\n- import_playbook: controller.yml\n\n- import_playbook: scheduler.yml\n  when: enable_scheduler\n\n- import_playbook: invoker.yml\n  when: not lean\n\n- import_playbook: edge.yml\n\n- import_playbook: downloadcli.yml\n"
  },
  {
    "path": "ansible/postdeploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook installs additional packages after whisk has been deployed.\n\n- import_playbook: properties.yml\n\n- hosts: ansible\n  tasks:\n    - include_tasks: tasks/installOpenwhiskCatalog.yml\n      when: (mode == \"deploy\")\n      with_dict: \"{{ catalog_repos }}\"\n"
  },
  {
    "path": "ansible/prereq.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook prepares all target hosts for Openwhisk installation.\n# It will install all necessary packages to run Openwhisk playbooks.\n\n- hosts: all:!ansible\n  serial: 1\n  roles:\n  - prereq\n"
  },
  {
    "path": "ansible/properties.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook writes whisk.properties.\n\n- hosts: ansible\n  tasks:\n    - import_tasks: tasks/writeWhiskProperties.yml\n      when: mode == \"deploy\"\n"
  },
  {
    "path": "ansible/publish.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook updates CLIs and SDKs on an existing edge host.\n# Artifacts get built and published to NGINX. This assumes an already running edge host in an Openwhisk deployment.\n\n- hosts: edge\n  roles:\n  - cli\n  - sdk\n"
  },
  {
    "path": "ansible/recreateDesignDocs.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook recreates all design documents in the whisks and the activations DB.\n\n- hosts: ansible\n  tasks:\n    - import_tasks: tasks/recreateViews.yml\n      when: mode == \"deploy\"\n"
  },
  {
    "path": "ansible/roles/apigateway/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove apigateway container.\n\n- name: remove apigateway\n  docker_container:\n    name: apigateway\n    state: absent\n  ignore_errors: True\n"
  },
  {
    "path": "ansible/roles/apigateway/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install apigateway\n\n- name: (re)start apigateway\n  docker_container:\n    name: apigateway\n    image: \"{{ apigateway.docker_image | default('openwhisk/apigateway:' ~ apigateway.version) }}\"\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    hostname: apigateway\n    env:\n      \"REDIS_HOST\": \"{{ groups['redis'] | first }}\"\n      \"REDIS_PORT\": \"{{ redis.port }}\"\n      \"REDIS_PASS\": \"{{ redis.password }}\"\n      \"PUBLIC_MANAGEDURL_HOST\": \"{{ ansible_host }}\"\n      \"PUBLIC_MANAGEDURL_PORT\": \"{{ apigateway.port.mgmt }}\"\n      \"TZ\": \"{{ docker.timezone }}\"\n    ports:\n      - \"{{ apigateway.port.mgmt }}:8080\"\n      - \"{{ apigateway.port.api }}:9000\"\n    pull: \"{{ apigateway_local_build is undefined }}\"\n\n- name: wait until the API Gateway in this host is up and running\n  uri:\n    url: \"http://{{ groups['apigateway'] | first }}:{{ apigateway.port.api }}/v1/apis\"\n  register: result\n  until: result.status == 200\n  retries: 12\n  delay: 5\n"
  },
  {
    "path": "ansible/roles/apigateway/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install apigateway in the group 'apigateway' in the environment inventory\n# In deploy mode it will deploy apigateway.\n# In clean mode it will remove the apigateway containers.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/cli/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n\n- name: remove cli nginx directory\n  file:\n    path: \"{{ openwhisk_cli.nginxdir.name }}\"\n    state: absent\n  become: \"{{ openwhisk_cli.nginxdir.become }}\"\n"
  },
  {
    "path": "ansible/roles/cli/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Tasks for handling CLI customization and publishing\n#\n# Note:  The configuration directory is actually located on the local machine;\n#        this script is run under the local host, usually 172.17.0.1 (docker local)\n\n- name: \"Ensure nginx directory for cli exists\"\n  file:\n    path: \"{{ openwhisk_cli.nginxdir.name }}\"\n    state: directory\n  become: \"{{ openwhisk_cli.nginxdir.become }}\"\n\n- name: \"Ensure temporary directory exists\"\n  file:\n    path: \"{{ nginx.confdir }}/cli_temp\"\n    state: directory\n\n- name: \"Download release archive to build directory ...\"\n  get_url:\n    url: \"{{ openwhisk_cli.remote.location }}/{{ openwhisk_cli.archive_name}}-{{ openwhisk_cli_tag }}-all.tgz\"\n    dest: \"{{ nginx.confdir }}/cli_temp/{{ openwhisk_cli.archive_name }}.tgz\"\n    headers: \"{{ openwhisk_cli.remote.headers | default('{}') }}\"\n  when: openwhisk_cli.installation_mode == \"remote\"\n\n- name: \"... or Copy release archive to build directory\"\n  copy:\n    src: \"{{ openwhisk_cli_home }}/release/{{ openwhisk_cli.archive_name}}-{{ openwhisk_cli_tag }}-all.tgz\"\n    dest: \"{{ nginx.confdir }}/cli_temp/{{ openwhisk_cli.archive_name }}.tgz\"\n  when: openwhisk_cli.installation_mode == \"local\"\n\n# Unarchiving here may fail for a local Mac development environment with an error stating \"GNU tar required.\" To fix the\n# problem run \"brew install gnu-tar\".\n- name: \"Expand the archive into the build directory\"\n  unarchive:\n    src: \"{{ nginx.confdir }}/cli_temp/{{ openwhisk_cli.archive_name }}.tgz\"\n    dest: \"{{ openwhisk_cli.nginxdir.name }}\"\n    remote_src: yes\n\n- name: \"Delete temp directory\"\n  file:\n    path: \"{{ nginx.confdir }}/cli_temp\"\n    state: absent\n    force: yes\n\n- name: \"Generate a list of individual tarballs to expand\"\n  find:\n      paths: \"{{ openwhisk_cli.nginxdir.name }}\"\n      patterns: '*.tgz'\n      recurse: true\n  register: individual_tarballs\n\n- name: \"Unarchive the individual tarballs\"\n  unarchive:\n    src: \"{{ item.path }}\"\n    dest: \"{{ item.path | dirname }}\"\n    remote_src: yes\n  with_items: \"{{ individual_tarballs.files }}\"\n\n- name: \"Generate a list of individual zipfiles to expand\"\n  find:\n      paths: \"{{ openwhisk_cli.nginxdir.name }}\"\n      patterns: '*.zip'\n      recurse: true\n  register: individual_zipfiles\n\n- name: \"Unarchive the individual zipfiles into binaries\"\n  unarchive:\n    src: \"{{ item.path }}\"\n    dest: \"{{ item.path | dirname }}\"\n    remote_src: yes\n  with_items: \"{{ individual_zipfiles.files }}\"\n"
  },
  {
    "path": "ansible/roles/cli/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will configure and publish the CLI tarball.\n# In deploy mode it will generate a new CLI config, generate a tarball and copy it to nginx.\n# In clean mode it will clean the cli.nginxdir\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/cli-install/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n\n- name: remove wsk binary\n  file:\n    path: \"{{ openwhisk_home }}/bin/{{ wsk }}\"\n    state: absent\n  vars:\n    wsk: \"{{ ( ansible_system == 'Windows') | ternary('wsk.exe', 'wsk') }}\"\n"
  },
  {
    "path": "ansible/roles/cli-install/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Install the appropriate CLI into the ansible host\n\n- name: grab the local CLI from the binaries unarchived into nginx\n  get_url:\n    url: \"https://{{host}}/cli/go/download/{{os}}/{{arch}}/{{wsk}}\"\n    dest: \"{{ openwhisk_home }}/bin\"\n    mode: \"0755\"\n    validate_certs: no\n  vars:\n    host: \"{{ groups['edge'] | first }}\"\n    arch: \"{{ ansible_machine | replace ('x86_64', 'amd64') }}\"\n    os: \"{{ ansible_system | lower | replace('darwin', 'mac') }}\"\n    wsk: \"{{ ( ansible_system == 'Windows') | ternary('wsk.exe', 'wsk') }}\"\n"
  },
  {
    "path": "ansible/roles/cli-install/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will configure and publish the CLI tarball.\n# In deploy mode it will generate a new CLI config, generate a tarball and copy it to nginx.\n# In clean mode it will clean the cli.nginxdir\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/controller/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove controller containers.\n\n- name: get controller name\n  set_fact:\n    controller_name: \"{{ name_prefix ~ host_group.index(inventory_hostname) }}\"\n\n- name: remove controller\n  docker_container:\n    name: \"{{ controller_name }}\"\n    state: absent\n  ignore_errors: \"True\"\n\n- name: remove controller log directory\n  file:\n    path: \"{{ whisk_logs_dir }}/{{ controller_name }}\"\n    state: absent\n  become: \"{{ logs.dir.become }}\"\n\n- name: remove controller conf directory\n  file:\n    path: \"{{ controller.confdir }}/{{ controller_name }}\"\n    state: absent\n  become: \"{{ controller.dir.become }}\"\n"
  },
  {
    "path": "ansible/roles/controller/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install Controller in group 'controllers' in the environment\n# inventory\n\n- import_tasks: docker_login.yml\n\n- name: get controller name and index\n  set_fact:\n    controller_name: \"{{ name_prefix ~ host_group.index(inventory_hostname) }}\"\n    controller_index:\n      \"{{ (controller_index_base|int) + host_group.index(inventory_hostname) }}\"\n\n- name: \"pull the {{ docker.image.tag }} image of controller\"\n  shell: \"docker pull {{docker_registry}}{{ docker.image.prefix }}/controller:{{docker.image.tag}}\"\n  when: docker_registry != \"\"\n  register: result\n  until: (result.rc == 0)\n  retries: \"{{ docker.pull.retries }}\"\n  delay: \"{{ docker.pull.delay }}\"\n\n- name: ensure controller log directory is created with permissions\n  file:\n    path: \"{{ whisk_logs_dir }}/{{ controller_name }}\"\n    state: directory\n    mode: 0777\n  become: \"{{ logs.dir.become }}\"\n\n# We need to create the file with proper permissions because the dir creation above\n# does not result in a dir with full permissions in docker machine especially with macos mounts\n- name: ensure controller log file is created with permissions\n  file:\n    path: \"{{ whisk_logs_dir }}/{{ controller_name }}/{{ controller_name }}_logs.log\"\n    state: touch\n    mode: 0777\n  when: environment_type is defined and environment_type == \"docker-machine\"\n\n- name: ensure controller config directory is created with permissions\n  file:\n    path: \"{{ controller.confdir }}/{{ controller_name }}\"\n    state: directory\n    mode: 0777\n  become: \"{{ controller.dir.become }}\"\n\n- name: copy jmxremote password file\n  when: jmx.enabled\n  template:\n    src: \"jmxremote.password.j2\"\n    dest: \"{{ controller.confdir }}/{{ controller_name }}/jmxremote.password\"\n    mode: 0777\n\n- name: copy jmxremote access file\n  when: jmx.enabled\n  template:\n    src: \"jmxremote.access.j2\"\n    dest: \"{{ controller.confdir }}/{{ controller_name }}/jmxremote.access\"\n    mode: 0777\n\n- name: \"copy kafka truststore/keystore\"\n  when: kafka.protocol == 'SSL'\n  copy:\n    src:\n      \"{{openwhisk_home~'/ansible/roles/kafka/files/'~kafka.ssl.keystore.name}}\"\n    dest: \"{{ controller.confdir }}/{{ controller_name }}\"\n\n- name: copy nginx certificate keystore\n  when: controller.protocol == 'https'\n  copy:\n    src: files/{{ controller.ssl.keystore.name }}\n    mode: 0666\n    dest: \"{{ controller.confdir }}/{{ controller_name }}\"\n  become: \"{{ controller.dir.become }}\"\n\n- name: copy certificates\n  when: controller.protocol == 'https'\n  copy:\n    src: \"{{ openwhisk_home }}/ansible/roles/controller/files/{{ item }}\"\n    mode: 0666\n    dest: \"{{ controller.confdir }}/{{ controller_name }}\"\n  with_items:\n    - \"{{ controller.ssl.cert }}\"\n    - \"{{ controller.ssl.key }}\"\n  become: \"{{ controller.dir.become }}\"\n\n- name: check, that required databases exist\n  include_tasks: \"{{ openwhisk_home }}/ansible/tasks/db/checkDb.yml\"\n  vars:\n    dbName: \"{{ item }}\"\n    dbUser: \"{{ db.credentials.controller.user }}\"\n    dbPass: \"{{ db.credentials.controller.pass }}\"\n  with_items:\n    - \"{{ db.whisk.actions }}\"\n    - \"{{ db.whisk.auth }}\"\n    - \"{{ db.whisk.activations }}\"\n\n- name: prepare controller port\n  set_fact:\n    controller_port: \"{{ controller.basePort + (controller_index | int) }}\"\n    ports_to_expose:\n      - \"{{ controller.basePort + (controller_index | int) }}:8080\"\n\n- name: expose additional ports if jmxremote is enabled\n  when: jmx.enabled\n  vars:\n    jmx_remote_port: \"{{ jmx.basePortController + (controller_index|int) }}\"\n    jmx_remote_rmi_port:\n      \"{{ jmx.rmiBasePortController + (controller_index|int) }}\"\n  set_fact:\n    ports_to_expose: >-\n      {{ ports_to_expose }} +\n      [ '{{ jmx_remote_port }}:{{ jmx_remote_port }}' ] +\n      [ '{{ jmx_remote_rmi_port }}:{{ jmx_remote_rmi_port }}' ]\n    controller_args: >-\n      {{ controller.arguments }}\n      {{ jmx.jvmCommonArgs }}\n      -Djava.rmi.server.hostname={{ inventory_hostname }}\n      -Dcom.sun.management.jmxremote.rmi.port={{ jmx_remote_rmi_port }}\n      -Dcom.sun.management.jmxremote.port={{ jmx_remote_port }}\n\n- name: Load config from template\n  set_fact:\n      openwhisk_config: \"{{ lookup('template', 'config.j2') | b64encode }}\"\n\n- name: populate environment variables for controller\n  set_fact:\n    env:\n      \"JAVA_OPTS\":\n        -Xmx{{ controller.heap }}\n        -XX:+CrashOnOutOfMemoryError\n        -XX:+UseGCOverheadLimit\n        -XX:ErrorFile=/logs/java_error.log\n        -XX:+HeapDumpOnOutOfMemoryError\n        -XX:HeapDumpPath=/logs\n      \"CONTROLLER_OPTS\": \"{{ controller_args | default(controller.arguments) }}\"\n      \"JMX_REMOTE\": \"{{ jmx.enabled }}\"\n      \"OPENWHISK_ENCODED_CONFIG\": \"{{ openwhisk_config }}\"\n      \"PORT\": \"8080\"\n      \"TZ\": \"{{ docker.timezone }}\"\n\n      \"CONFIG_whisk_info_date\": \"{{ whisk.version.date }}\"\n      \"CONFIG_whisk_info_buildNo\": \"{{ docker.image.tag }}\"\n      \"CONFIG_whisk_cluster_name\": \"{{ whisk.cluster_name | lower }}\"\n      \"CONFIG_whisk_controller_username\": \"{{ controller.username }}\"\n      \"CONFIG_whisk_controller_password\": \"{{ controller.password }}\"\n\n      \"KAFKA_HOSTS\": \"{{ kafka_connect_string }}\"\n      \"CONFIG_whisk_kafka_replicationFactor\":\n        \"{{ kafka.replicationFactor | default() }}\"\n      \"CONFIG_whisk_kafka_topics_cacheInvalidation_retentionBytes\":\n        \"{{ kafka_topics_cacheInvalidation_retentionBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_cacheInvalidation_retentionMs\":\n        \"{{ kafka_topics_cacheInvalidation_retentionMS | default() }}\"\n      \"CONFIG_whisk_kafka_topics_cacheInvalidation_segmentBytes\":\n        \"{{ kafka_topics_cacheInvalidation_segmentBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_completed_retentionBytes\":\n        \"{{ kafka_topics_completed_retentionBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_completed_retentionMs\":\n        \"{{ kafka_topics_completed_retentionMS | default() }}\"\n      \"CONFIG_whisk_kafka_topics_completed_segmentBytes\":\n        \"{{ kafka_topics_completed_segmentBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_health_retentionBytes\":\n        \"{{ kafka_topics_health_retentionBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_health_retentionMs\":\n        \"{{ kafka_topics_health_retentionMS | default() }}\"\n      \"CONFIG_whisk_kafka_topics_health_segmentBytes\":\n        \"{{ kafka_topics_health_segmentBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_prefix\":\n        \"{{ kafka.topicsPrefix }}\"\n      \"CONFIG_whisk_kafka_topics_userEvent_prefix\":\n        \"{{ kafka.topicsUserEventPrefix }}\"\n      \"CONFIG_whisk_kafka_common_securityProtocol\":\n        \"{{ kafka.protocol }}\"\n      \"CONFIG_whisk_kafka_common_sslTruststoreLocation\":\n        \"/conf/{{ kafka.ssl.keystore.name }}\"\n      \"CONFIG_whisk_kafka_common_sslTruststorePassword\":\n        \"{{ kafka.ssl.keystore.password }}\"\n      \"CONFIG_whisk_kafka_common_sslKeystoreLocation\":\n        \"/conf/{{ kafka.ssl.keystore.name }}\"\n      \"CONFIG_whisk_kafka_common_sslKeystorePassword\":\n        \"{{ kafka.ssl.keystore.password }}\"\n\n      \"CONFIG_whisk_couchdb_protocol\": \"{{ db.protocol }}\"\n      \"CONFIG_whisk_couchdb_host\": \"{{ db.host }}\"\n      \"CONFIG_whisk_couchdb_port\": \"{{ db.port }}\"\n      \"CONFIG_whisk_couchdb_username\": \"{{ db.credentials.controller.user }}\"\n      \"CONFIG_whisk_couchdb_password\": \"{{ db.credentials.controller.pass }}\"\n      \"CONFIG_whisk_couchdb_provider\": \"{{ db.provider }}\"\n      \"CONFIG_whisk_couchdb_databases_WhiskAuth\": \"{{ db.whisk.auth }}\"\n      \"CONFIG_whisk_couchdb_databases_WhiskEntity\": \"{{ db.whisk.actions }}\"\n      \"CONFIG_whisk_couchdb_databases_WhiskActivation\":\n        \"{{ db.whisk.activations }}\"\n      \"CONFIG_whisk_db_subjectsDdoc\": \"{{ db_whisk_subjects_ddoc | default() }}\"\n      \"CONFIG_whisk_db_actionsDdoc\": \"{{ db_whisk_actions_ddoc | default() }}\"\n      \"CONFIG_whisk_db_activationsDdoc\": \"{{ db_whisk_activations_ddoc | default() }}\"\n      \"CONFIG_whisk_db_activationsFilterDdoc\": \"{{ db_whisk_activations_filter_ddoc | default() }}\"\n      \"CONFIG_whisk_userEvents_enabled\": \"{{ user_events | default(false) | lower }}\"\n\n      \"LIMITS_ACTIONS_INVOKES_PERMINUTE\": \"{{ limits.invocationsPerMinute }}\"\n      \"LIMITS_ACTIONS_INVOKES_CONCURRENT\": \"{{ limits.concurrentInvocations }}\"\n      \"LIMITS_TRIGGERS_FIRES_PERMINUTE\": \"{{ limits.firesPerMinute }}\"\n      \"LIMITS_ACTIONS_SEQUENCE_MAXLENGTH\": \"{{ limits.sequenceMaxLength }}\"\n\n      \"CONFIG_whisk_memory_min\": \"{{ limit_action_memory_min | default() }}\"\n      \"CONFIG_whisk_memory_max\": \"{{ limit_action_memory_max | default() }}\"\n      \"CONFIG_whisk_memory_std\": \"{{ limit_action_memory_std | default() }}\"\n\n      \"CONFIG_whisk_timeLimit_min\": \"{{ limit_action_time_min | default() }}\"\n      \"CONFIG_whisk_timeLimit_max\": \"{{ limit_action_time_max | default() }}\"\n      \"CONFIG_whisk_timeLimit_std\": \"{{ limit_action_time_std | default() }}\"\n\n      \"CONFIG_whisk_concurrencyLimit_min\": \"{{ limit_action_concurrency_min | default() }}\"\n      \"CONFIG_whisk_concurrencyLimit_max\": \"{{ limit_action_concurrency_max | default() }}\"\n      \"CONFIG_whisk_concurrencyLimit_std\": \"{{ limit_action_concurrency_std | default() }}\"\n\n      \"CONFIG_whisk_namespaceDefaultLimit_memory_min\": \"{{ namespace_default_limit_action_memory_min | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_memory_max\": \"{{ namespace_default_limit_action_memory_max | default() }}\"\n\n      \"CONFIG_whisk_namespaceDefaultLimit_timeLimit_min\": \"{{ namespace_default_limit_action_time_min | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_timeLimit_max\": \"{{ namespace_default_limit_action_time_max | default() }}\"\n\n      \"CONFIG_whisk_namespaceDefaultLimit_concurrencyLimit_min\": \"{{ namespace_default_limit_action_concurrency_min | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_concurrencyLimit_max\": \"{{ namespace_default_limit_action_concurrency_max | default() }}\"\n\n      \"CONFIG_whisk_featureFlags_requireApiKeyAnnotation\": \"{{ whisk.feature_flags.require_api_key_annotation | default(true) | lower }}\"\n      \"CONFIG_whisk_featureFlags_requireResponsePayload\": \"{{ whisk.feature_flags.require_response_payload | default(true) | lower }}\"\n\n      \"CONFIG_whisk_activation_payload_max\":\n        \"{{ limit_activation_payload | default() }}\"\n\n      \"RUNTIMES_MANIFEST\": \"{{ runtimesManifest | to_json }}\"\n      \"CONFIG_whisk_runtimes_defaultImagePrefix\":\n        \"{{ runtimes_default_image_prefix | default() }}\"\n      \"CONFIG_whisk_runtimes_defaultImageTag\":\n        \"{{ runtimes_default_image_tag | default() }}\"\n      \"CONFIG_whisk_runtimes_bypassPullForLocalImages\":\n        \"{{ runtimes_bypass_pull_for_local_images | default() | lower }}\"\n      \"CONFIG_whisk_runtimes_localImagePrefix\":\n        \"{{ runtimes_local_image_prefix | default() }}\"\n\n      \"METRICS_KAMON\": \"{{ metrics.kamon.enabled | default(false) | lower }}\"\n      \"METRICS_KAMON_TAGS\": \"{{ metrics.kamon.tags | default() | lower }}\"\n      \"METRICS_LOG\": \"{{ metrics.log.enabled | default(false) | lower }}\"\n      \"CONFIG_whisk_controller_protocol\": \"{{ controller.protocol }}\"\n      \"CONFIG_whisk_controller_https_keystorePath\":\n        \"/conf/{{ controller.ssl.keystore.name }}\"\n      \"CONFIG_whisk_controller_https_keystorePassword\":\n        \"{{ controller.ssl.keystore.password }}\"\n      \"CONFIG_whisk_controller_https_keystoreFlavor\":\n        \"{{ controller.ssl.storeFlavor }}\"\n      \"CONFIG_whisk_controller_https_clientAuth\":\n        \"{{ controller.ssl.clientAuth }}\"\n      \"CONFIG_whisk_loadbalancer_managedFraction\":\n        \"{{ controller.managedFraction }}\"\n      \"CONFIG_whisk_loadbalancer_blackboxFraction\":\n        \"{{ controller.blackboxFraction }}\"\n      \"CONFIG_whisk_loadbalancer_timeoutFactor\":\n        \"{{ controller.timeoutFactor }}\"\n      \"CONFIG_whisk_loadbalancer_timeoutAddon\":\n        \"{{ controller.timeoutAddon }}\"\n\n      \"CONFIG_kamon_statsd_hostname\": \"{{ metrics.kamon.host }}\"\n      \"CONFIG_kamon_statsd_port\": \"{{ metrics.kamon.port }}\"\n\n      \"CONFIG_whisk_spi_LogStoreProvider\": \"{{ userLogs.spi }}\"\n      \"CONFIG_whisk_spi_LoadBalancerProvider\":\n        \"{{ controller.loadbalancer.spi }}\"\n      \"CONFIG_whisk_spi_EntitlementSpiProvider\": \"{{ controller.entitlement.spi }}\"\n\n      \"CONFIG_whisk_spi_AuthenticationDirectiveProvider\": \"{{ controller.authentication.spi }}\"\n      \"CONFIG_logback_log_level\": \"{{ controller.loglevel }}\"\n\n      \"CONFIG_whisk_transactions_header\": \"{{ transactions.header }}\"\n\n      \"CONFIG_whisk_controller_activation_pollingFromDb\": \"{{ controller_activation_pollingFromDb | default(true) | lower }}\"\n\n- name: merge extra env variables\n  set_fact:\n    env: \"{{ env | combine(controller.extraEnv) }}\"\n\n- name: setup elasticsearch activation store env\n  set_fact:\n    elastic_env:\n      \"CONFIG_whisk_activationStore_elasticsearch_protocol\": \"{{ db.elasticsearch.protocol}}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_hosts\": \"{{ elasticsearch_connect_string }}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_indexPattern\": \"{{ db.elasticsearch.index_pattern }}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_username\": \"{{ db.elasticsearch.auth.admin.username }}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_password\": \"{{ db.elasticsearch.auth.admin.password }}\"\n      \"CONFIG_whisk_spi_ActivationStoreProvider\": \"org.apache.openwhisk.core.database.elasticsearch.ElasticSearchActivationStoreProvider\"\n  when: db.activation_store.backend == \"ElasticSearch\"\n\n- name: merge elasticsearch activation store env\n  set_fact:\n    env: \"{{ env | combine(elastic_env) }}\"\n  when: db.activation_store.backend == \"ElasticSearch\"\n\n- name: setup mongodb artifact store env\n  set_fact:\n    mongodb_env:\n      \"CONFIG_whisk_mongodb_uri\": \"{{ db.mongodb.connect_string }}\"\n      \"CONFIG_whisk_mongodb_database\": \"{{ db.mongodb.database }}\"\n      \"CONFIG_whisk_spi_ArtifactStoreProvider\": \"org.apache.openwhisk.core.database.mongodb.MongoDBArtifactStoreProvider\"\n  when: db.artifact_store.backend == \"MongoDB\"\n\n- name: merge mongodb artifact store env\n  set_fact:\n    env: \"{{ env | combine(mongodb_env) }}\"\n  when: db.artifact_store.backend == \"MongoDB\"\n\n- name: setup scheduler env\n  set_fact:\n    scheduler_env:\n      \"CONFIG_whisk_etcd_hosts\": \"{{ etcd_connect_string }}\"\n      \"CONFIG_whisk_etcd_lease_timeout\": \"{{ etcd.lease.timeout }}\"\n      \"CONFIG_whisk_etcd_pool_threads\": \"{{ etcd.pool_threads }}\"\n      \"CONFIG_whisk_scheduler_grpc_tls\": \"{{ scheduler.grpc.tls | default('false') | lower }}\"\n      \"CONFIG_whisk_scheduler_maxPeek\": \"{{ scheduler.maxPeek }}\"\n      \"CONFIG_whisk_spi_LoadBalancerProvider\": \"org.apache.openwhisk.core.loadBalancer.FPCPoolBalancer\"\n      \"CONFIG_whisk_spi_EntitlementSpiProvider\": \"org.apache.openwhisk.core.entitlement.FPCEntitlementProvider\"\n  when: enable_scheduler\n\n- name: merge scheduler env\n  set_fact:\n    env: \"{{ env | combine(scheduler_env) }}\"\n  when: enable_scheduler\n\n- name: populate volumes for controller\n  set_fact:\n    controller_volumes:\n       - \"{{ whisk_logs_dir }}/{{ controller_name }}:/logs\"\n       - \"{{ controller.confdir }}/{{ controller_name }}:/conf\"\n\n- name: check if coverage collection is enabled\n  set_fact:\n    coverage_enabled: false\n  when: coverage_enabled is undefined\n\n- name: ensure controller coverage directory is created with permissions\n  file:\n    path: \"{{ coverage_logs_dir }}/controller/{{ item }}\"\n    state: directory\n    mode: 0777\n  with_items:\n    - controller\n    - common\n  become: \"{{ logs.dir.become }}\"\n  when: coverage_enabled\n\n- name: extend controller volume for coverage\n  set_fact:\n    controller_volumes: \"{{ controller_volumes|default({}) + [coverage_logs_dir+'/controller:/coverage']  }}\"\n  when: coverage_enabled\n\n- name: include plugins\n  include_tasks: \"{{ item }}.yml\"\n  with_items: \"{{ controller_plugins | default([]) }}\"\n  when: not lean\n\n- name: lean controller setup\n  include_tasks: \"lean.yml\"\n  when: lean\n\n# Before redeploy controller, should remove that controller instance from nginx\n- name: remove the controller from nginx's upstream configuration\n  shell:\n    docker exec -t nginx sh -c \"sed -i  \\\"s/ server {{ ansible_host }}:{{ controller.basePort + (controller_index | int) }}/ \\#server {{ ansible_host }}:{{ controller.basePort + (controller_index | int) }}/g\\\"  /etc/nginx/nginx.conf  && nginx -s reload\"\n  delegate_to: \"{{ item }}\"\n  with_items: \"{{ groups['edge'] }}\"\n  when: zeroDowntimeDeployment.enabled == true\n\n- name: wait some time for controllers fire all existing triggers\n  shell: sleep 5s\n  when: zeroDowntimeDeployment.enabled == true\n\n- name: wait until {{ controller_name }} executes all existing activations\n  uri:\n    url: \"{{ controller.protocol }}://{{ ansible_host }}:{{ controller.basePort + (controller_index | int) }}/activation/count\"\n    validate_certs: no\n    client_key: \"{{ controller.confdir }}/controller{{ groups['controllers'].index(inventory_hostname) }}/{{ controller.ssl.key }}\"\n    client_cert: \"{{ controller.confdir }}/controller{{ groups['controllers'].index(inventory_hostname) }}/{{ controller.ssl.cert }}\"\n    return_content: yes\n    user: \"{{ controller.username }}\"\n    password: \"{{ controller.password }}\"\n    force_basic_auth: yes\n  register: result\n  until: result.content == '0'\n  retries: \"{{ controller.deployment.retries }}\"\n  delay: \"{{ controller.deployment.delay }}\"\n  when: zeroDowntimeDeployment.enabled == true\n  ignore_errors: \"{{ controller.deployment.ignore_error }}\"\n\n- name: Disable {{ controller_name }} before remove controller\n  uri:\n    url: \"{{ controller.protocol }}://{{ ansible_host }}:{{ controller.basePort + groups['controllers'].index(inventory_hostname) }}/disable\"\n    validate_certs: no\n    client_key: \"{{ controller.confdir }}/{{ controller_name }}/{{ controller.ssl.key }}\"\n    client_cert: \"{{ controller.confdir }}/{{ controller_name }}/{{ controller.ssl.cert }}\"\n    method: POST\n    status_code: 200\n    user: \"{{ controller.username }}\"\n    password: \"{{ controller.password }}\"\n    force_basic_auth: yes\n  ignore_errors: \"{{ controller.deployment.ignore_error }}\"\n  when: zeroDowntimeDeployment.enabled == true\n\n- name: wait some time for controller to gracefully shutdown the consumer for activation ack\n  shell: sleep 5s\n  when: zeroDowntimeDeployment.enabled == true\n\n- name: (re)start controller\n  docker_container:\n    name: \"{{ controller_name }}\"\n    image:\n      \"{{docker_registry~docker.image.prefix}}/controller:{{ 'cov' if (coverage_enabled) else docker.image.tag }}\"\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    hostname: \"{{ controller_name }}\"\n    env: \"{{ env }}\"\n    volumes: \"{{ controller_volumes }}\"\n    ports: \"{{ ports_to_expose }}\"\n    # userns_mode, pid_mode and privileged required when controller running in lean mode\n    userns_mode: \"{{ userns_mode | default('') }}\"\n    pid_mode: \"{{ pid_mode | default('') }}\"\n    privileged: \"{{ privileged | default('no') }}\"\n    command:\n      /bin/sh -c\n      \"exec /init.sh {{ controller_index }}\n      >> /logs/{{ controller_name }}_logs.log 2>&1\"\n\n- name: wait until the Controller in this host is up and running\n  block:\n    - name: ping controller health endpoint\n      uri:\n        url: \"{{ controller.protocol }}://{{ ansible_host }}:{{ controller_port }}/ping\"\n        validate_certs: no\n        client_key: \"{{ controller.confdir }}/{{ controller_name }}/{{ controller.ssl.key }}\"\n        client_cert: \"{{ controller.confdir }}/{{ controller_name }}/{{ controller.ssl.cert }}\"\n        return_content: yes\n        user: \"{{ controller.username }}\"\n        password: \"{{ controller.password }}\"\n        force_basic_auth: yes\n      register: result\n      until: result.status == 200\n      retries: 12\n      delay: 10\n      failed_when: result.status is defined and result.status not in [200]\n  rescue:\n    - name: dump controller docker logs\n      shell: |\n        docker logs {{ controller_name }} >/tmp/controller-docker.log 2>&1 || true\n        cat /tmp/controller-docker.log\n      register: controller_logs\n      failed_when: false\n    - name: output controller logs for debugging\n      debug:\n        var: controller_logs.stdout_lines\n    - name: dump controller file logs\n      shell: |\n        cat /var/tmp/wsklogs/{{ controller_name }}/{{ controller_name }}_logs.log\n      register: controller_file_logs\n      failed_when: false\n    - name: output controller file logs for debugging\n      debug:\n        var: controller_file_logs.stdout_lines\n    - fail:\n        msg: \"Controller failed to start; logs emitted above\"\n\n- name: warm up activation path\n  uri:\n    url:\n      \"{{controller.protocol}}://{{ lookup('file', '{{ catalog_auth_key }}')}}@{{ansible_host}}:{{controller_port}}/api/v1/namespaces/_/actions/invokerHealthTestAction{{controller_index}}?blocking=false&result=false\"\n    validate_certs: \"no\"\n    client_key:\n      \"{{ controller.confdir }}/{{ controller_name }}/{{ controller.ssl.key }}\"\n    client_cert:\n      \"{{ controller.confdir }}/{{ controller_name }}/{{ controller.ssl.cert }}\"\n    method: POST\n  ignore_errors: True\n\n- name: wait for all invokers in {{ controller_name }} to become up\n  uri:\n    url: \"{{ controller.protocol }}://{{ ansible_host }}:{{ controller.basePort + (controller_index | int) }}/invokers\"\n    validate_certs: no\n    client_key: \"{{ controller.confdir }}/controller{{ groups['controllers'].index(inventory_hostname) }}/{{ controller.ssl.key }}\"\n    client_cert: \"{{ controller.confdir }}/controller{{ groups['controllers'].index(inventory_hostname) }}/{{ controller.ssl.cert }}\"\n    return_content: yes\n  register: invokerStatus\n  until: invokerStatus.json|length >= 1 and \"unhealthy\" not in invokerStatus.content\n  retries: 14\n  delay: 5\n  when: zeroDowntimeDeployment.enabled == true\n\n# When all invokers report their status to controller, add the controller instance to nginx when exist at least one invoker is up\n- name: Add the controller back to nginx's upstream configuration when there exist at least one healthy invoker\n  shell:\n    docker exec -t nginx sh -c \"sed -i  \\\"s/ \\#server {{ ansible_host }}:{{ controller.basePort + (controller_index | int) }}/ server {{ ansible_host }}:{{ controller.basePort + (controller_index | int) }}/g\\\"  /etc/nginx/nginx.conf  && nginx -s reload\"\n  delegate_to: \"{{ item }}\"\n  with_items: \"{{ groups['edge'] }}\"\n  ignore_errors: True\n  when: zeroDowntimeDeployment.enabled == true and \"up\" in invokerStatus.content\n"
  },
  {
    "path": "ansible/roles/controller/tasks/join_pekko_cluster.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n#\n#  Controller 'plugin' that will add the items necessary to the controller\n#  environment to cause the controller to join a specified pekko cluster\n#\n\n- name: add pekko port to ports_to_expose\n  set_fact:\n    ports_to_expose: >-\n      {{ ports_to_expose }} +\n      [ \"{{ (controller.pekko.cluster.basePort + (controller_index | int)) }}:\"\n      + \"{{ controller.pekko.cluster.bindPort }}\" ]\n\n- name: add seed nodes to controller environment\n  set_fact:\n    env: >-\n      {{ env | combine({\n        'CONFIG_pekko_cluster_seedNodes_' ~ seedNode.0:\n          'pekko://controller-actor-system@'~seedNode.1~':'~(controller.pekko.cluster.basePort+seedNode.0)\n      }) }}\n  with_indexed_items: \"{{ controller.pekko.cluster.seedNodes }}\"\n  loop_control:\n    loop_var: seedNode\n\n- name: Add pekko environment to controller environment\n  vars:\n    pekko_env:\n      \"CONFIG_pekko_actor_provider\": \"{{ controller.pekko.provider }}\"\n      \"CONFIG_pekko_remote_artery_canonical_hostname\":\n        \"{{ controller.pekko.cluster.host[(controller_index | int)] }}\"\n      \"CONFIG_pekko_remote_artery_canonical_port\":\n        \"{{ controller.pekko.cluster.basePort + (controller_index | int) }}\"\n      \"CONFIG_pekko_remote_artery_bind_hostname\": \"0.0.0.0\"\n      \"CONFIG_pekko_remote_artery_bind_port\":\n        \"{{ controller.pekko.cluster.bindPort }}\"\n  set_fact:\n    env: \"{{ env | combine(pekko_env) }}\"\n"
  },
  {
    "path": "ansible/roles/controller/tasks/lean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This plugin will  provide controller with Lean Controller parameters\n\n- name: set inventory_hostname to invoker and save controllers data that can be changed by invoker task\n  set_fact:\n    controller_env: \"{{ env }}\"\n    inventory_hostname: \"invoker0\"\n    invoker_index_base: 0\n    name_prefix: \"invoker\"\n    host_group: \"{{ groups['invokers'] }}\"\n\n- name: include invoker data\n  include_tasks: \"../invoker/tasks/deploy.yml\"\n\n- name: save invoker volumes\n  set_fact:\n    invoker_volumes: \"{{ volumes.split(',') | reject('search','/logs') | reject('search','/conf') | reject('search','/coverage') | list }}\"\n\n- name: populate volumes\n  set_fact:\n    controller_volumes: >-\n      {{ invoker_volumes }} +\n      {{ controller_volumes }}\n\n- name: populate environment variables for LEAN controller\n  vars:\n    lean_env:\n      \"CONFIG_whisk_spi_MessagingProvider\": \"org.apache.openwhisk.connector.lean.LeanMessagingProvider\"\n      \"CONFIG_whisk_spi_LoadBalancerProvider\": \"org.apache.openwhisk.core.loadBalancer.LeanBalancer\"\n  set_fact:\n    env: \"{{ env | combine(controller_env) | combine(lean_env) }}\"\n\n- name: provide extended docker container params for controller\n  set_fact:\n    userns_mode: \"host\"\n    pid_mode: \"host\"\n    privileged: \"yes\"\n\n"
  },
  {
    "path": "ansible/roles/controller/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install controller in group 'controllers' in the environment inventory\n# In deploy mode it will deploy controllers.\n# In clean mode it will remove the controller containers.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/controller/templates/config.j2",
    "content": "include classpath(\"application.conf\")\n\n# Specify any custom config here. For example\n# whisk {\n#   metrics {\n#     prometheus-enabled = true\n#   }\n# }\n"
  },
  {
    "path": "ansible/roles/couchdb/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove CouchDB server\n\n- name: remove CouchDB\n  docker_container:\n    name: couchdb\n    state: absent\n  ignore_errors: True\n"
  },
  {
    "path": "ansible/roles/couchdb/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will run a CouchDB server on the db group\n\n- name: set the coordinator to the first node\n  set_fact:\n    coordinator: \"{{ groups['db'][0] }}\"\n\n- name: \"Set the volumes\"\n  set_fact:\n    volumes: []\n\n- name: \"Set the nodes\"\n  set_fact:\n    couchdb_nodes: []\n\n- name: check if db credentials are valid for CouchDB\n  fail: msg=\"The db provider in your {{ hosts_dir }}/group_vars/all is {{ db.provider }}, it has to be CouchDB, pls double check\"\n  when: db.provider != \"CouchDB\"\n\n- name: ensure directory for persisting db exists\n  stat:\n    path: \"{{ db.persist_path }}\"\n  register: db_persist_path_exists\n  when: db.persist_path\n\n- name: fail if path to persist db does not exist\n  fail:\n    msg: directory for persisting db does not exist '{{ db.persist_path }}'\n  when: db.persist_path and not (db_persist_path_exists.stat.exists and db_persist_path_exists.stat.isdir)\n\n- name: \"mount directory for persisting dbs\"\n  set_fact:\n    volumes: \"{{ volumes }} + [ '{{ db.persist_path }}:/opt/couchdb/data' ]\"\n  when: db.persist_path\n\n- include_tasks: gen_erl_cookie.yml\n  when: (db.instances|int >= 2)\n\n- name: \"set the erlang cookie volume\"\n  set_fact:\n    volumes: \"{{ volumes }} + [ '{{ config_root_dir }}/erlang.cookie:/opt/couchdb/.erlang.cookie' ]\"\n  when: (db.instances|int >= 2)\n\n- name: \"(re)start CouchDB from '{{ couchdb_image }} ' \"\n  vars:\n    couchdb_image: \"{{ couchdb.docker_image | default('apache/couchdb:' ~ couchdb.version ) }}\"\n  docker_container:\n    name: couchdb\n    image: \"{{ couchdb_image }}\"\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    volumes: \"{{ volumes }}\"\n    ports:\n      - \"{{ db.port }}:5984\"\n      - \"4369:4369\"\n      - \"9100:9100\"\n    env:\n      COUCHDB_USER: \"{{ db.credentials.admin.user }}\"\n      COUCHDB_PASSWORD: \"{{ db.credentials.admin.pass }}\"\n      NODENAME: \"{{ ansible_host }}\"\n      TZ: \"{{ docker.timezone }}\"\n    pull: \"{{ couchdb.pull_couchdb | default(true) }}\"\n\n- name: wait until CouchDB in this host is up and running\n  uri:\n    url: \"{{ db.protocol }}://{{ ansible_host }}:{{ db.port }}/_utils/\"\n  register: result\n  until: result.status == 200\n  retries: 12\n  delay: 5\n\n- name: check if '_users' database exists\n  uri:\n    url: \"{{ db.protocol }}://{{ ansible_host }}:{{ db.port }}/_users\"\n    method: HEAD\n    status_code: 200,404\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  register: create_users_db\n\n- name: create '_users' database for singleton mode if necessary\n  uri:\n    url: \"{{ db.protocol }}://{{ ansible_host }}:{{ db.port }}/_users\"\n    method: PUT\n    status_code: 201\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  when: (create_users_db.status == 404) and (inventory_hostname == coordinator) and (couchdb.version is version_compare('2.0','>='))\n\n- name: check whether couchdb is clustered\n  uri:\n    url: \"{{ db.protocol }}://{{ ansible_host }}:{{ db.port }}/_cluster_setup\"\n    method: GET\n    status_code: 200\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  register: cluster_state\n  run_once: true\n\n- name: check clustered nodes\n  uri:\n    url: \"{{ db.protocol }}://{{ ansible_host }}:{{ db.port }}/_membership\"\n    method: GET\n    status_code: 200\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  register: nodes_state\n  run_once: true\n\n- name: generates couchdb node name\n  set_fact:\n    couchdb_nodes: \"{{ couchdb_nodes }} + [ 'couchdb@{{ item }}' ]\"\n  with_items: \"{{ groups['db'] }}\"\n  run_once: true\n\n- name: check if there is a new node\n  set_fact:\n    require_clustering: true\n  when: item not in nodes_state.json.cluster_nodes\n  with_items: \"{{ couchdb_nodes }}\"\n  run_once: true\n\n- name: set node name\n  set_fact:\n    node_name: \"couchdb@{{ ansible_host }}\"\n\n- name: enable the cluster setup mode\n  uri:\n    url: \"{{ db.protocol }}://{{ ansible_host }}:{{ db.port }}/_cluster_setup\"\n    method: POST\n    body: >\n        {\"action\": \"enable_cluster\", \"bind_address\":\"0.0.0.0\", \"username\": \"{{ db.credentials.admin.user }}\", \"password\":\"{{ db.credentials.admin.pass }}\", \"port\": {{ db.port }}, \"node_count\": \"{{ groups['db'] | length }}\", \"remote_node\": \"{{ ansible_host }}\", \"remote_current_user\": \"{{ db.credentials.admin.user }}\", \"remote_current_password\": \"{{ db.credentials.admin.pass }}\"}\n    body_format: json\n    status_code: 201\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  when: (inventory_hostname == coordinator) and (db.instances|int >= 2) and require_clustering is defined\n\n- name: add remote nodes to the cluster\n  uri:\n    url: \"{{ db.protocol }}://{{ coordinator }}:{{ db.port }}/_cluster_setup\"\n    method: POST\n    body: >\n        {\"action\": \"add_node\", \"host\":\"{{ ansible_host }}\", \"port\": {{ db.port }}, \"username\": \"{{ db.credentials.admin.user }}\", \"password\":\"{{ db.credentials.admin.pass }}\"}\n    body_format: json\n    status_code: 201\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  when: (inventory_hostname != coordinator) and (db.instances|int >= 2)  and require_clustering is defined and node_name not in nodes_state.json.cluster_nodes\n\n- name: finish the cluster setup mode\n  uri:\n    url: \"{{ db.protocol }}://{{ ansible_host }}:{{ db.port }}/_cluster_setup\"\n    method: POST\n    body: >\n        {\"action\": \"finish_cluster\"}\n    body_format: json\n    status_code: 201\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  when: (inventory_hostname == coordinator) and (db.instances|int >= 2) and (cluster_state.json.state != \"cluster_finished\")\n\n"
  },
  {
    "path": "ansible/roles/couchdb/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will deploy a database server. Use the role if you want to use CouchCB locally.\n# In deploy mode it will start the CouchDB container.\n# In clean mode it will remove the CouchDB container.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/elasticsearch/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove ElasticSearch server\n\n- name: set elasticsearch container name and volume\n  set_fact:\n    elasticsearch_name: \"{{ name_prefix ~ host_group.index(inventory_hostname) }}\"\n    volume_name: \"{{ db.elasticsearch.base_volume ~ host_group.index(inventory_hostname) }}\"\n\n- name: remove ElasticSearch\n  vars:\n    elasticsearch_image: \"{{ elasticsearch.docker_image | default('docker.elastic.co/elasticsearch/elasticsearch:' ~ elasticsearch.version ) }}\"\n  docker_container:\n    name: \"{{ elasticsearch_name }}\"\n    image: \"{{ elasticsearch_image }}\"\n    keep_volumes: False\n    state: absent\n  ignore_errors: True\n\n- name: remove ElasticSearch conf dir\n  file:\n    path: \"{{ db.elasticsearch.confdir }}/{{ elasticsearch_name }}\"\n    state: absent\n  become: \"{{ db.elasticsearch.dir.become }}\"\n"
  },
  {
    "path": "ansible/roles/elasticsearch/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will run a ElasticSearch server on the db group\n\n- name: set the vm.max_map_count to 262144\n  sysctl:\n    name: vm.max_map_count\n    value: '262144'\n  become: true\n\n- name: set elasticsearch container name, volume and port\n  set_fact:\n    elasticsearch_name: \"{{ name_prefix ~ host_group.index(inventory_hostname) }}\"\n    volume_name: \"{{ db.elasticsearch.base_volume ~ host_group.index(inventory_hostname) }}\"\n    http_port: \"{{ (db.elasticsearch.port|int) + host_group.index(inventory_hostname) }}\"\n    transport_port: \"{{ (db.elasticsearch.base_transport_port|int) + host_group.index(inventory_hostname) }}\"\n\n- name: ensure elasticserach config directory is created with permissions\n  file:\n    path: \"{{ db.elasticsearch.confdir }}/{{ elasticsearch_name }}\"\n    state: directory\n    mode: 0755\n  become: \"{{ db.elasticsearch.dir.become }}\"\n\n# create volume directory if it's a directory path(not a named volume)\n- name: ensure elasticserach volume directory is created with permissions\n  file:\n    path: \"{{ volume_name }}\"\n    state: directory\n    mode: 0700\n    owner: \"{{ db.elasticsearch.uid }}\"\n  become: true\n  when: volume_name is search(\"/\")\n\n- name: copy elasticsearch config file\n  template:\n    src: \"elasticsearch.yml.j2\"\n    dest: \"{{ db.elasticsearch.confdir }}/{{ elasticsearch_name }}/elasticsearch.yml\"\n    mode: 0644\n  become: \"{{ db.elasticsearch.dir.become }}\"\n\n- name: copy elasticsearch log config file\n  template:\n    src: \"log4j2.properties.j2\"\n    dest: \"{{ db.elasticsearch.confdir }}/{{ elasticsearch_name }}/log4j2.properties\"\n    mode: 0644\n  become: \"{{ db.elasticsearch.dir.become }}\"\n\n- name: \"(re)start ElasticSearch from '{{ elasticsearch_image }} ' \"\n  vars:\n    elasticsearch_image: \"{{ elasticsearch.docker_image | default('docker.elastic.co/elasticsearch/elasticsearch:' ~ elasticsearch.version ) }}\"\n  docker_container:\n    name: \"{{ elasticsearch_name }}\"\n    image: \"{{ elasticsearch_image }}\"\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    ports:\n      - \"{{ http_port }}:9200\"\n      - \"{{ transport_port }}:{{ transport_port }}\"\n    volumes:\n      - \"{{ db.elasticsearch.confdir }}/{{ elasticsearch_name }}/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml\"\n      - \"{{ db.elasticsearch.confdir }}/{{ elasticsearch_name }}/log4j2.properties:/usr/share/elasticsearch/config/log4j2.properties\"\n      - \"{{ volume_name }}:/usr/share/elasticsearch/data\"\n    pull: \"{{ docker.pull_elasticsearch | default(true) }}\"\n    ulimits:\n      - \"nofile:262144:262144\"\n      - \"memlock:-1:-1\"\n    env:\n      TZ: \"{{ docker.timezone }}\"\n      ES_JAVA_OPTS: \"{{ db.elasticsearch.java_opts }}\"\n\n- name: wait until ElasticSearch in this host is up and running\n  uri:\n    url: \"{{ db.elasticsearch.protocol }}://{{ ansible_host }}:{{ http_port }}\"\n  register: result\n  until: result.status == 200\n  retries: 12\n  delay: 5\n"
  },
  {
    "path": "ansible/roles/elasticsearch/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will deploy a database server. Use the role if you want to use ElasticSearch locally.\n# In deploy mode it will start the ElasticSearch container.\n# In clean mode it will remove the ElasticSearch container.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/elasticsearch/templates/elasticsearch.yml.j2",
    "content": "cluster.name: \"{{ db.elasticsearch.cluster_name }}\"\nnode.name: \"{{ elasticsearch_name }}\"\nnetwork.host: 0.0.0.0\nnetwork.publish_host: {{ ansible_default_ipv4.address | default(ansible_host | default('127.0.0.1')) }}\n\nhttp.port: 9200\ntransport.tcp.port: {{ transport_port }}\n\n# minimum_master_nodes need to be explicitly set when bound on a public IP\n# set to 1 to allow single node clusters\n# Details: https://github.com/elastic/elasticsearch/pull/17282\ndiscovery.zen.ping.unicast.hosts:\n{% for es in groups['elasticsearch'] %}\n   - {{ hostvars[es].ansible_host }}:{{ db.elasticsearch.base_transport_port|int + host_group.index(es)|int }}\n{% endfor %}\ndiscovery.zen.minimum_master_nodes: {{ (host_group|length / 2 + 1) | int}}\n\ngateway.recover_after_nodes: {{ (host_group|length / 2 + 1) | int }}\ngateway.expected_nodes: {{ host_group|length }}\ngateway.recover_after_time: 5m\n\nxpack.security.enabled: false\nbootstrap.memory_lock: true\n"
  },
  {
    "path": "ansible/roles/elasticsearch/templates/log4j2.properties.j2",
    "content": "status = error\n\nappender.console.type = Console\nappender.console.name = console\nappender.console.layout.type = PatternLayout\nappender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n\n\nrootLogger.appenderRef.console.ref = console\n\nrootLogger.level = {{ db.elasticsearch.loglevel }}\n"
  },
  {
    "path": "ansible/roles/etcd/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove etcd containers.\n\n- name: remove etcd\n  docker_container:\n    name: etcd{{ groups['etcd'].index(inventory_hostname) }}\n    keep_volumes: True\n    state: absent\n  ignore_errors: True\n"
  },
  {
    "path": "ansible/roles/etcd/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install etcd in group 'etcd' in the environment inventory\n\n- name: \"Set the name of the etcd node\"\n  set_fact:\n    name: \"etcd{{ groups['etcd'].index(inventory_hostname) }}\"\n\n- name: \"set the volume_dir\"\n  set_fact:\n    volume_dir: \"{{ etcd.dir.data }}/etcd{{ groups['etcd'].index(inventory_hostname) }}:/etcd-data\"\n  when: etcd_data_dir is defined\n\n\n- name: \"Set the cluster of the etcd cluster\"\n  set_fact:\n    cluster: \"{% set etcdhosts = [] %}\n              {% for host in groups['etcd'] %}\n                  {{ etcdhosts.append('etcd' + ((loop.index-1)|string) + '=' + 'http://' + hostvars[host].ansible_host + ':' + ((2480+loop.index-1)|string) ) }}\n              {% endfor %}\n              {{ etcdhosts | join(',') }}\"\n\n- name: (re)start etcd\n  docker_container:\n    name: etcd{{ groups['etcd'].index(inventory_hostname) }}\n    image: bitnamilegacy/etcd:{{ etcd.version }}\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    volumes: \"{{volume_dir | default([])}}\"\n    env:\n     \"ALLOW_NONE_AUTHENTICATION\": \"yes\"\n    ports:\n      - \"{{ etcd.client.port + groups['etcd'].index(inventory_hostname) }}:{{ etcd.client.port + groups['etcd'].index(inventory_hostname) }}\"\n      - \"{{ etcd.server.port + groups['etcd'].index(inventory_hostname) }}:{{ etcd.server.port + groups['etcd'].index(inventory_hostname) }}\"\n    pull: \"{{ etcd.pull_etcd | default(true) }}\"\n\n- name: wait until etcd in this host is up and running\n  uri:\n    url: \"http://{{ ansible_host }}:{{ etcd.client.port + groups['etcd'].index(inventory_hostname) }}/health\"\n  register: result\n  until: result.status == 200\n  retries: 12\n  delay: 5\n"
  },
  {
    "path": "ansible/roles/etcd/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install etcd in group 'etcd' in the environment inventory\n# In deploy mode it will deploy etcd containers.\n# In clean mode it will remove etcd containers.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/invoker/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove invoker containers.\n\n- name: get invoker name and index\n  set_fact:\n    invoker_name: \"{{ name_prefix ~ ((invoker_index_base | int) + host_group.index(inventory_hostname)) }}\"\n    invoker_index: \"{{ (invoker_index_base | int) + host_group.index(inventory_hostname) }}\"\n\n- name: disable invoker{{ groups['invokers'].index(inventory_hostname) }}\n  uri:\n    url: \"{{ invoker.protocol }}://{{ ansible_host }}:{{ invoker.port + groups['invokers'].index(inventory_hostname) }}/disable\"\n    validate_certs: no\n    client_key: \"{{ invoker.confdir }}/invoker{{ groups['invokers'].index(inventory_hostname) }}/{{ invoker.ssl.key }}\"\n    client_cert: \"{{ invoker.confdir }}/invoker{{ groups['invokers'].index(inventory_hostname) }}/{{ invoker.ssl.cert }}\"\n    method: POST\n    status_code: 200\n    user: \"{{ invoker.username }}\"\n    password: \"{{ invoker.password }}\"\n    force_basic_auth: yes\n  ignore_errors: \"{{ invoker.deployment.ignore_error }}\"\n  when: zeroDowntimeDeployment.enabled == true and enable_scheduler\n\n- name: wait invoker{{ groups['invokers'].index(inventory_hostname) }} to clean up all existing containers\n  uri:\n    url: \"{{ invoker.protocol }}://{{ ansible_host }}:{{ invoker.port + groups['invokers'].index(inventory_hostname) }}/pool/count\"\n    validate_certs: no\n    client_key: \"{{ invoker.confdir }}/invoker{{ groups['invokers'].index(inventory_hostname) }}/{{ invoker.ssl.key }}\"\n    client_cert: \"{{ invoker.confdir }}/invoker{{ groups['invokers'].index(inventory_hostname) }}/{{ invoker.ssl.cert }}\"\n    user: \"{{ invoker.username }}\"\n    password: \"{{ invoker.password }}\"\n    force_basic_auth: yes\n    return_content: yes\n  register: result\n  until: result.content == '0'\n  retries: \"{{ invoker.deployment.retries }}\"\n  delay: \"{{ invoker.deployment.delay }}\"\n  when: zeroDowntimeDeployment.enabled == true and enable_scheduler\n  ignore_errors: \"{{ invoker.deployment.ignore_error }}\"\n\n- name: remove invoker\n  docker_container:\n    name: \"{{ invoker_name }}\"\n    image: \"{{ docker_registry }}{{ docker.image.prefix }}/invoker:{{ docker.image.tag }}\"\n    state: absent\n    stop_timeout: 60\n    timeout: 120\n  ignore_errors: True\n\n# In case the invoker could not clean up completely in time.\n- name: pause/resume at runc-level to restore docker consistency\n  shell: |\n        DOCKER_PAUSED=$(docker ps --filter status=paused --filter name=wsk{{ invoker_index }} -q --no-trunc)\n        for C in $DOCKER_PAUSED; do runc --root {{ invoker.docker.runcdir }} pause $C; done\n        DOCKER_RUNNING=$(docker ps --filter status=running --filter name=wsk{{ invoker_index }} -q --no-trunc)\n        for C2 in $DOCKER_RUNNING; do runc --root {{ invoker.docker.runcdir }} resume $C2; done\n        TOTAL=$(($(echo $DOCKER_PAUSED | wc -w)+$(echo $DOCKER_RUNNING | wc -w)))\n        echo \"Handled $TOTAL remaining actions.\"\n  register: runc_output\n  ignore_errors: True\n  become: \"{{ invoker.docker.become }}\"\n\n- debug: msg=\"{{ runc_output.stdout }}\"\n\n- name: unpause remaining actions\n  shell: \"docker unpause $(docker ps -aq --filter status=paused --filter name=wsk{{ invoker_index }})\"\n  failed_when: False\n\n- name: remove remaining actions\n  shell: \"docker rm -f $(docker ps -aq --filter name=wsk{{ invoker_index }})\"\n  failed_when: False\n\n- name: remove invoker log directory\n  file:\n    path: \"{{ whisk_logs_dir }}/{{ invoker_name }}\"\n    state: absent\n  become: \"{{ logs.dir.become }}\"\n  when: mode == \"clean\"\n\n- name: remove invoker conf directory\n  file:\n    path: \"{{ invoker.confdir }}/{{ invoker_name }}\"\n    state: absent\n  become: \"{{ invoker.dir.become }}\"\n  when: mode == \"clean\"\n\n# Workaround for orphaned ifstate.veth* files on Ubuntu 14.04\n# See https://github.com/moby/moby/issues/22513\n# Remove inactive files older than 60 minutes\n- name: \"Clean orphaned ifstate.veth* files on Ubuntu 14.04\"\n  shell: |\n    ACTIVE_VETH_IFACES=$(ip -oneline link show | grep --only-matching --extended-regexp 'veth[0-9a-f]+' | tr '\\n' '|' | sed -e 's/.$//')\n    EXCLUDE_REGEX=$(if [ -z ${ACTIVE_VETH_IFACES} ]; then echo 'No active veth interfaces found' >&2; else printf '( -not -regex  /run/network/ifstate\\.(%s) ) -and ' ${ACTIVE_VETH_IFACES}; fi)\n    find /run/network -regextype posix-egrep ${EXCLUDE_REGEX} -name 'ifstate.veth*' -and -mmin +60 -delete\n  become: True\n  ignore_errors: True\n  when: ansible_distribution == 'Ubuntu' and ansible_distribution_version == '14.04'\n"
  },
  {
    "path": "ansible/roles/invoker/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role installs invokers.\n\n###\n# When the zero-downtime-deployment is enabled, clean.yml is used to gracefully shut down the invoker.\n#\n- import_tasks: clean.yml\n  when: zeroDowntimeDeployment.enabled == true and enable_scheduler\n\n- import_tasks: docker_login.yml\n\n- name: get invoker name and index\n  set_fact:\n    invoker_name: \"{{ name_prefix ~ ((invoker_index_base | int) + host_group.index(inventory_hostname)) }}\"\n    invoker_index: \"{{ (invoker_index_base | int) + host_group.index(inventory_hostname) }}\"\n\n- name: \"pull invoker image with tag {{docker.image.tag}}\"\n  shell: \"docker pull {{docker_registry}}{{ docker.image.prefix }}/invoker:{{docker.image.tag}}\"\n  when: docker_registry != \"\"\n  register: result\n  until: (result.rc == 0)\n  retries: \"{{ docker.pull.retries }}\"\n  delay: \"{{ docker.pull.delay }}\"\n\n###\n# This task assumes that the images are local to the invoker host already if there is no prefix or tag\n# which is usually the case for a local deployment. A distributed deployment will specify the prefix, or tag\n# to pull the images from the appropriate registry. If a runtimes_registry is optionally specified, pull images\n# from there; this permits a (private) registry to be used for caching the images. The registry if specified\n# must include a trailing '/'.\n#\n- name: \"pull runtime action images per manifest\"\n  shell: \"docker pull {{runtimes_registry | default()}}{{inv_item.prefix}}/{{inv_item.name}}:{{inv_item.tag | default()}}\"\n  loop: \"{{ runtimesManifest.runtimes.values() | sum(start=[]) | selectattr('deprecated', 'equalto',false)  | map(attribute='image') | list | unique }}\"\n  when: skip_pull_runtimes is not defined or not (skip_pull_runtimes == True or skip_pull_runtimes.lower() == \"true\")\n  register: result\n  until: (result.rc == 0)\n  retries: \"{{ docker.pull.retries }}\"\n  delay: \"{{ docker.pull.delay }}\"\n  loop_control:\n    loop_var: inv_item\n\n###\n# See comment above for pulling other runtime images.\n#\n- name: \"pull blackboxes action images per manifest\"\n  shell: \"docker pull {{runtimes_registry | default()}}{{inv_item.prefix}}/{{inv_item.name}}:{{inv_item.tag | default()}}\"\n  loop: \"{{ runtimesManifest.blackboxes }}\"\n  when: skip_pull_runtimes is not defined or not (skip_pull_runtimes == True or skip_pull_runtimes.lower() == \"true\")\n  register: result\n  until: (result.rc == 0)\n  retries: \"{{ docker.pull.retries }}\"\n  delay: \"{{ docker.pull.delay }}\"\n  loop_control:\n    loop_var: inv_item\n\n- name: \"determine docker root dir on docker-machine\"\n  uri:  url=\"http://{{ ansible_host }}:{{ docker.port }}/info\" return_content=yes\n  register: dockerInfo_output\n  when: environmentInformation.type == 'docker-machine'\n\n- set_fact:\n    dockerInfo: \"{{ dockerInfo_output['json'] }}\"\n  when: environmentInformation.type == \"docker-machine\"\n\n- name: \"determine docker root dir\"\n  shell: docker info -f json\n  args:\n    executable: /bin/bash\n  register: dockerInfo_output\n  when: environmentInformation.type != \"docker-machine\"\n\n- set_fact:\n    dockerInfo: \"{{ dockerInfo_output.stdout|from_json }}\"\n  when: environmentInformation.type != \"docker-machine\"\n\n- name: ensure invoker log directory is created with permissions\n  file:\n    path: \"{{ whisk_logs_dir }}/{{ invoker_name }}\"\n    state: directory\n    mode: 0777\n  become: \"{{ logs.dir.become }}\"\n\n- name: ensure invoker config directory is created with permissions\n  file:\n    path: \"{{ invoker.confdir }}/{{ invoker_name }}\"\n    state: directory\n    mode: 0777\n  become: \"{{ invoker.dir.become }}\"\n\n- name: \"copy kafka truststore/keystore\"\n  when: kafka.protocol == 'SSL'\n  copy:\n    src: \"{{ openwhisk_home }}/ansible/roles/kafka/files/{{ kafka.ssl.keystore.name }}\"\n    dest: \"{{ invoker.confdir }}/{{ invoker_name }}\"\n\n- name: copy keystore, key and cert\n  when: invoker.protocol == \"https\"\n  copy:\n    src: \"{{ inv_item }}\"\n    mode: 0666\n    dest: \"{{ invoker.confdir }}/{{ invoker_name }}\"\n  become: \"{{ invoker.dir.become }}\"\n  with_items:\n  - \"{{ openwhisk_home }}/ansible/roles/invoker/files/{{ invoker.ssl.keystore.name }}\"\n  - \"{{ openwhisk_home }}/ansible/roles/invoker/files/{{ invoker.ssl.key }}\"\n  - \"{{ openwhisk_home }}/ansible/roles/invoker/files/{{ invoker.ssl.cert }}\"\n  loop_control:\n    loop_var: inv_item\n\n- name: check, that required databases exist\n  include_tasks: \"{{ openwhisk_home }}/ansible/tasks/db/checkDb.yml\"\n  vars:\n    dbName: \"{{ inv_item }}\"\n    dbUser: \"{{ db.credentials.invoker.user }}\"\n    dbPass: \"{{ db.credentials.invoker.pass }}\"\n  with_items:\n  - \"{{ db.whisk.actions }}\"\n  - \"{{ db.whisk.activations }}\"\n  loop_control:\n    loop_var: inv_item\n\n- name: get running invoker information\n  uri: url=\"http://{{ ansible_host }}:{{ docker.port }}/containers/json?filters={{ '{\"name\":[ \"invoker\" ],\"ancestor\":[ \"invoker\" ]}' | urlencode }}\" return_content=yes\n  register: invokerInfo_output\n  when: environmentInformation.type == \"docker-machine\"\n\n- set_fact:\n    invokerInfo: \"{{ invokerInfo_output['json'] }}\"\n  when: environmentInformation.type == \"docker-machine\"\n\n- name: \"get invoker info\"\n  shell: |\n    INFO=`echo -e \"GET http:/v1.24/containers/json?filters={{ '{\"name\":[ \"invoker\" ],\"ancestor\":[ \"invoker\" ]}' | urlencode }} HTTP/1.0\\r\\n\" | nc -U /var/run/docker.sock | grep \"{\"`\n    if [ -z \"$INFO\" ]; then\n      echo []\n    else\n      echo $INFO\n    fi\n  args:\n    executable: /bin/bash\n  register: invokerInfo_output\n  when: environmentInformation.type != \"docker-machine\"\n\n- set_fact:\n    invokerInfo: \"{{ invokerInfo_output.stdout|from_json }}\"\n  when: environmentInformation.type != \"docker-machine\"\n\n- name: determine if more than one invoker is running\n  fail:\n    msg: \"more than one invoker is running\"\n  when: not invoker.allowMultipleInstances and invokerInfo|length > 1\n\n- name: determine if index of invoker is same with index of inventory host\n  fail:\n    msg: \"invoker index is invalid. expected: /invoker{{ groups['invokers'].index(inventory_hostname) }} found: {{ inv_item.Names[0] }}\"\n  with_items: \"{{ invokerInfo }}\"\n  when: not invoker.allowMultipleInstances and inv_item.Names[0] != \"/{{ invoker_name }}\"\n  loop_control:\n    loop_var: inv_item\n\n- name: copy jmxremote password file\n  when: jmx.enabled\n  template:\n    src: \"jmxremote.password.j2\"\n    dest: \"{{ invoker.confdir  }}/{{ invoker_name }}/jmxremote.password\"\n    mode: 0777\n\n- name: copy jmxremote access file\n  when: jmx.enabled\n  template:\n    src: \"jmxremote.access.j2\"\n    dest: \"{{ invoker.confdir  }}/{{ invoker_name }}/jmxremote.access\"\n    mode: 0777\n\n- name: add additional jvm params if jmxremote is enabled\n  when: jmx.enabled\n  set_fact:\n    invoker_args: \"{{ invoker.arguments }} {{ invoker.jmxremote.jvmArgs }}\"\n\n- name: prepare invoker ports\n  set_fact:\n    invoker_ports_to_expose: [\"{{ invoker.port + (invoker_index | int) }}:8080\"]\n\n- name: expose additional ports if jmxremote is enabled\n  when: jmx.enabled\n  set_fact:\n    invoker_ports_to_expose: \"{{ invoker_ports_to_expose }} + [ \\\"{{ jmx.basePortInvoker + (invoker_index | int) }}:{{ jmx.basePortInvoker + (invoker_index | int) }}\\\" ] + [ \\\"{{ jmx.rmiBasePortInvoker + (invoker_index | int) }}:{{ jmx.rmiBasePortInvoker + (invoker_index | int) }}\\\" ]\"\n\n\n- name: Load config from template\n  set_fact:\n    openwhisk_config: \"{{ lookup('template', 'config.j2') | b64encode }}\"\n\n- name: populate environment variables for invoker\n  set_fact:\n    env:\n      \"JAVA_OPTS\": \"-Xmx{{ invoker.heap }} -XX:+CrashOnOutOfMemoryError -XX:+UseGCOverheadLimit -XX:ErrorFile=/logs/java_error.log\"\n      \"INVOKER_OPTS\": \"{{ invoker_args | default(invoker.arguments) }}\"\n      \"JMX_REMOTE\": \"{{ jmx.enabled }}\"\n      \"OPENWHISK_ENCODED_CONFIG\": \"{{ openwhisk_config }}\"\n      \"PORT\": \"8080\"\n      \"TZ\": \"{{ docker.timezone }}\"\n      \"KAFKA_HOSTS\": \"{{ kafka_connect_string }}\"\n      \"CONFIG_whisk_kafka_replicationFactor\": \"{{ kafka.replicationFactor | default() }}\"\n      \"CONFIG_whisk_kafka_topics_invoker_retentionBytes\": \"{{ kafka_topics_invoker_retentionBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_invoker_retentionMs\": \"{{ kafka_topics_invoker_retentionMS | default() }}\"\n      \"CONFIG_whisk_kakfa_topics_invoker_segmentBytes\": \"{{ kafka_topics_invoker_segmentBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_prefix\": \"{{ kafka.topicsPrefix }}\"\n      \"CONFIG_whisk_kafka_topics_userEvent_prefix\": \"{{ kafka.topicsUserEventPrefix }}\"\n      \"CONFIG_whisk_kafka_common_securityProtocol\": \"{{ kafka.protocol }}\"\n      \"CONFIG_whisk_kafka_common_sslTruststoreLocation\": \"/conf/{{ kafka.ssl.keystore.name }}\"\n      \"CONFIG_whisk_kafka_common_sslTruststorePassword\": \"{{ kafka.ssl.keystore.password }}\"\n      \"CONFIG_whisk_kafka_common_sslKeystoreLocation\": \"/conf/{{ kafka.ssl.keystore.name }}\"\n      \"CONFIG_whisk_kafka_common_sslKeystorePassword\": \"{{ kafka.ssl.keystore.password }}\"\n      \"CONFIG_whisk_userEvents_enabled\": \"{{ user_events | default(false) | lower }}\"\n      \"ZOOKEEPER_HOSTS\": \"{{ zookeeper_connect_string }}\"\n      \"CONFIG_whisk_couchdb_protocol\": \"{{ db.protocol }}\"\n      \"CONFIG_whisk_couchdb_host\": \"{{ db.host }}\"\n      \"CONFIG_whisk_couchdb_port\": \"{{ db.port }}\"\n      \"CONFIG_whisk_couchdb_username\": \"{{ db.credentials.invoker.user }}\"\n      \"CONFIG_whisk_couchdb_password\": \"{{ db.credentials.invoker.pass }}\"\n      \"CONFIG_whisk_couchdb_provider\": \"{{ db.provider }}\"\n      \"CONFIG_whisk_couchdb_databases_WhiskAuth\": \"{{ db.whisk.auth }}\"\n      \"CONFIG_whisk_couchdb_databases_WhiskEntity\": \"{{ db.whisk.actions }}\"\n      \"CONFIG_whisk_couchdb_databases_WhiskActivation\": \"{{ db.whisk.activations }}\"\n      \"DB_WHISK_ACTIONS\": \"{{ db.whisk.actions }}\"\n      \"DB_WHISK_ACTIVATIONS\": \"{{ db.whisk.activations }}\"\n      \"DB_WHISK_AUTHS\": \"{{ db.whisk.auth }}\"\n      \"CONFIG_whisk_db_subjectsDdoc\": \"{{ db_whisk_subjects_ddoc | default() }}\"\n      \"CONFIG_whisk_db_actionsDdoc\": \"{{ db_whisk_actions_ddoc | default() }}\"\n      \"CONFIG_whisk_db_activationsDdoc\": \"{{ db_whisk_activations_ddoc | default() }}\"\n      \"CONFIG_whisk_db_activationsFilterDdoc\": \"{{ db_whisk_activations_filter_ddoc | default() }}\"\n      \"WHISK_API_HOST_PROTO\": \"{{ whisk_api_host_proto | default('https') }}\"\n      \"WHISK_API_HOST_PORT\": \"{{ whisk_api_host_port | default('443') }}\"\n      \"WHISK_API_HOST_NAME\": \"{{ whisk_api_host_name | default(groups['edge'] | first) }}\"\n      \"CONFIG_whisk_containerFactory_runtimesRegistry_url\": \"{{ runtimes_registry | default('') }}\"\n      \"CONFIG_whisk_containerFactory_userImagesRegistry_url\": \"{{ user_images_registry | default('') }}\"\n      \"RUNTIMES_MANIFEST\": \"{{ runtimesManifest | to_json }}\"\n      \"CONFIG_whisk_runtimes_bypassPullForLocalImages\": \"{{ runtimes_bypass_pull_for_local_images | default() | lower }}\"\n      \"CONFIG_whisk_runtimes_localImagePrefix\": \"{{ runtimes_local_image_prefix | default() }}\"\n      \"CONFIG_whisk_containerFactory_containerArgs_network\": \"{{ invoker_container_network_name | default('bridge') }}\"\n      \"INVOKER_CONTAINER_POLICY\": \"{{ invoker_container_policy_name | default()}}\"\n      \"CONFIG_whisk_containerPool_userMemory\": \"{{ hostvars[groups['invokers'][invoker_index | int]].user_memory | default(invoker.userMemory) }}\"\n      \"CONFIG_whisk_containerPool_userCpus\": \"{{ invoker.userCpus | default() }}\"\n      \"CONFIG_whisk_docker_client_parallelRuns\": \"{{ invoker_parallel_runs | default() }}\"\n      \"CONFIG_whisk_docker_containerFactory_useRunc\": \"{{ invoker.useRunc | default(false) | lower }}\"\n      \"WHISK_LOGS_DIR\": \"{{ whisk_logs_dir }}\"\n      \"METRICS_KAMON\": \"{{ metrics.kamon.enabled | default(false) | lower }}\"\n      \"METRICS_KAMON_TAGS\": \"{{ metrics.kamon.tags | default() | lower }}\"\n      \"METRICS_LOG\": \"{{ metrics.log.enabled | default(false) | lower }}\"\n      \"CONFIG_kamon_statsd_hostname\": \"{{ metrics.kamon.host }}\"\n      \"CONFIG_kamon_statsd_port\": \"{{ metrics.kamon.port }}\"\n      \"CONFIG_whisk_spi_LogStoreProvider\": \"{{ userLogs.spi }}\"\n      \"CONFIG_whisk_spi_InvokerProvider\": \"{{ invoker.reactiveSpi }}\"\n      \"CONFIG_whisk_spi_InvokerServerProvider\": \"{{ invoker.serverSpi }}\"\n      \"CONFIG_logback_log_level\": \"{{ invoker.loglevel }}\"\n      \"CONFIG_whisk_memory_min\": \"{{ limit_action_memory_min | default() }}\"\n      \"CONFIG_whisk_memory_max\": \"{{ limit_action_memory_max | default() }}\"\n      \"CONFIG_whisk_memory_std\": \"{{ limit_action_memory_std | default() }}\"\n      \"CONFIG_whisk_timeLimit_min\": \"{{ limit_action_time_min | default() }}\"\n      \"CONFIG_whisk_timeLimit_max\": \"{{ limit_action_time_max | default() }}\"\n      \"CONFIG_whisk_timeLimit_std\": \"{{ limit_action_time_std | default() }}\"\n      \"CONFIG_whisk_concurrencyLimit_min\": \"{{ limit_action_concurrency_min | default() }}\"\n      \"CONFIG_whisk_concurrencyLimit_max\": \"{{ limit_action_concurrency_max | default() }}\"\n      \"CONFIG_whisk_concurrencyLimit_std\": \"{{ limit_action_concurrency_std | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_memory_min\": \"{{ namespace_default_limit_action_memory_min | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_memory_max\": \"{{ namespace_default_limit_action_memory_max | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_timeLimit_min\": \"{{ namespace_default_limit_action_time_min | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_timeLimit_max\": \"{{ namespace_default_limit_action_time_max | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_concurrencyLimit_min\": \"{{ namespace_default_limit_action_concurrency_min | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_concurrencyLimit_max\": \"{{ namespace_default_limit_action_concurrency_max | default() }}\"\n      \"CONFIG_whisk_activation_payload_max\": \"{{ limit_activation_payload | default() }}\"\n      \"CONFIG_whisk_transactions_header\": \"{{ transactions.header }}\"\n      \"CONFIG_whisk_containerPool_pekkoClient\": \"{{ container_pool_pekko_client | default('false') | lower }}\"\n      \"CONFIG_whisk_containerFactory_containerArgs_extraEnvVars_0\": \"__OW_ALLOW_CONCURRENT={{ runtimes_enable_concurrency | default('false') }}\"\n      \"CONFIG_whisk_invoker_protocol\": \"{{ invoker.protocol }}\"\n      \"CONFIG_whisk_invoker_https_keystorePath\": \"/conf/{{ invoker.ssl.keystore.name }}\"\n      \"CONFIG_whisk_invoker_https_keystorePassword\": \"{{ invoker.ssl.keystore.password }}\"\n      \"CONFIG_whisk_invoker_https_keystoreFlavor\": \"{{ invoker.ssl.storeFlavor }}\"\n      \"CONFIG_whisk_invoker_https_clientAuth\": \"{{ invoker.ssl.clientAuth }}\"\n      \"CONFIG_whisk_invoker_resource_tags\": \"{% if tags is defined %} '{{ tags | join(',') }}' {% else %} '' {% endif %}\"\n      \"CONFIG_whisk_invoker_dedicated_namespaces\": \"{% if dedicatedNamespaces is defined %} '{{ dedicatedNamespaces | join(',') }}' {% else %} '' {% endif %}\"\n      \"CONFIG_whisk_containerProxy_timeouts_idleContainer\": \"{{ whisk.containerProxy.timeouts.idleContainer }}\"\n      \"CONFIG_whisk_containerProxy_timeouts_pauseGrace\": \"{{ whisk.containerProxy.timeouts.pauseGrace  }}\"\n      \"CONFIG_whisk_containerProxy_timeouts_keepingDuration\": \"{{ whisk.containerProxy.timeouts.keepingDuration }}\"\n      \"CONFIG_whisk_containerPool_prewarmExpirationCheckInitDelay\": \"{{ container_pool_prewarm_expirationCheckInitDelay | default('10 minutes') }}\"\n      \"CONFIG_whisk_containerPool_prewarmExpirationCheckInterval\": \"{{ container_pool_prewarm_expirationCheckInterval | default('10 minutes') }}\"\n      \"CONFIG_whisk_containerPool_prewarmExpirationCheckIntervalVariance\": \"{{ container_pool_prewarm_expirationCheckIntervalVariance | default('10 seconds') }}\"\n      \"CONFIG_whisk_containerPool_prewarmPromotion\": \"{{ container_pool_strict | default('false') | lower }}\"\n      \"CONFIG_whisk_containerPool_prewarmMaxRetryLimit\": \"{{ container_pool_prewarm_max_retry_limit | default(5) }}\"\n      \"CONFIG_whisk_containerPool_memorySyncInterval\": \"{{ container_pool_memorySyncInterval | default('1 second') }}\"\n      \"CONFIG_whisk_containerPool_batchDeletionSize\": \"{{ container_pool_batchDeletionSize | default(10) }}\"\n      \"CONFIG_whisk_invoker_username\": \"{{ invoker.username }}\"\n      \"CONFIG_whisk_invoker_password\": \"{{ invoker.password }}\"\n      \"CONFIG_whisk_cluster_name\": \"{{ whisk.cluster_name | lower }}\"\n\n- name: extend invoker dns env\n  set_fact:\n    env: \"{{ env | default({}) | combine( {'CONFIG_whisk_containerFactory_containerArgs_dnsServers_' ~ inv_item.0: inv_item.1} ) }}\"\n  with_indexed_items: \"{{ (invoker_container_network_dns_servers | default()).split(' ')}}\"\n  loop_control:\n    loop_var: inv_item\n\n- name: merge extra env variables\n  set_fact:\n    env: \"{{ env | combine(invoker.extraEnv) }}\"\n\n- name: setup elasticsearch activation store env\n  set_fact:\n    elastic_env:\n      \"CONFIG_whisk_activationStore_elasticsearch_protocol\": \"{{ db.elasticsearch.protocol}}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_hosts\": \"{{ elasticsearch_connect_string }}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_indexPattern\": \"{{ db.elasticsearch.index_pattern }}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_username\": \"{{ db.elasticsearch.auth.admin.username }}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_password\": \"{{ db.elasticsearch.auth.admin.password }}\"\n      \"CONFIG_whisk_spi_ActivationStoreProvider\": \"org.apache.openwhisk.core.database.elasticsearch.ElasticSearchActivationStoreProvider\"\n  when: db.activation_store.backend == \"ElasticSearch\"\n\n- name: merge elasticsearch activation store env\n  set_fact:\n    env: \"{{ env | combine(elastic_env) }}\"\n  when: db.activation_store.backend == \"ElasticSearch\"\n\n- name: setup mongodb artifact store env\n  set_fact:\n    mongodb_env:\n      \"CONFIG_whisk_mongodb_uri\": \"{{ db.mongodb.connect_string }}\"\n      \"CONFIG_whisk_mongodb_database\": \"{{ db.mongodb.database }}\"\n      \"CONFIG_whisk_spi_ArtifactStoreProvider\": \"org.apache.openwhisk.core.database.mongodb.MongoDBArtifactStoreProvider\"\n  when: db.artifact_store.backend == \"MongoDB\"\n\n- name: merge mongodb artifact store env\n  set_fact:\n    env: \"{{ env | combine(mongodb_env) }}\"\n  when: db.artifact_store.backend == \"MongoDB\"\n\n- name: setup scheduler env\n  set_fact:\n    scheduler_env:\n      \"CONFIG_whisk_etcd_hosts\": \"{{ etcd_connect_string }}\"\n      \"CONFIG_whisk_etcd_lease_timeout\": \"{{ etcd.lease.timeout }}\"\n      \"CONFIG_whisk_etcd_pool_threads\": \"{{ etcd.pool_threads }}\"\n      \"CONFIG_whisk_scheduler_dataManagementService_retryInterval\": \"{{ scheduler.dataManagementService.retryInterval }}\"\n      \"CONFIG_whisk_invoker_containerCreation_maxPeek\": \"{{ invoker.container.creationMaxPeek }}\"\n      \"CONFIG_whisk_spi_InvokerProvider\": \"org.apache.openwhisk.core.invoker.FPCInvokerReactive\"\n      \"CONFIG_whisk_spi_InvokerServerProvider\": \"org.apache.openwhisk.core.invoker.FPCInvokerServer\"\n  when: enable_scheduler\n\n- name: merge scheduler env\n  set_fact:\n    env: \"{{ env | combine(scheduler_env) }}\"\n  when: enable_scheduler\n\n- name: include plugins\n  include_tasks: \"{{ inv_item }}.yml\"\n  with_items: \"{{ invoker_plugins | default([]) }}\"\n  loop_control:\n    loop_var: inv_item\n\n- name: set invoker volumes\n  set_fact:\n    volumes: \"/sys/fs/cgroup:/sys/fs/cgroup,\\\n      {{ whisk_logs_dir }}/{{ invoker_name }}:/logs,\\\n      {{ invoker.confdir }}/{{ invoker_name }}:/conf,\\\n      {{ dockerInfo['DockerRootDir'] }}/containers/:/containers,\\\n      {{ docker_sock | default('/var/run/docker.sock') }}:/var/run/docker.sock\"\n###\n# The root runc directory varies based on the version of docker and runc.\n# When docker>=18.06 uses runc the directory is /run/docker/runtime-runc/moby.\n# While runc itself uses /run/runc for a root user or /run/user/<uid>/runc for a non-root user.\n# Currently, the invoker is running as a root user so the below configuration works as expected.\n# But when the invoker needs to run as a non-root user or the version docker needs to be changed,\n# the following configuration should be properly updated as well.\n#\n# Alternatively, we can disable the runc with invoker.userRunc = false.\n#\n- name: set invoker runc volume\n  set_fact:\n    volumes: \"{{ volumes }},{{ invoker.docker.runcdir }}:/run/runc\"\n  when: invoker.useRunc == true\n\n- name: define options when deploying invoker on Ubuntu\n  set_fact:\n    volumes: \"{{ volumes|default('') }},/usr/lib/x86_64-linux-gnu/libapparmor.so.1:/usr/lib/x86_64-linux-gnu/libapparmor.so.1\"\n  when: ansible_distribution == \"Ubuntu\"\n\n- name: check if coverage collection is enabled\n  set_fact:\n    coverage_enabled: false\n  when: coverage_enabled is undefined\n\n- name: ensure invoker coverage directory is created with permissions\n  file:\n    path: \"{{ coverage_logs_dir }}/invoker/{{ inv_item }}\"\n    state: directory\n    mode: 0777\n  with_items:\n    - invoker\n    - common\n  become: \"{{ logs.dir.become }}\"\n  when: coverage_enabled\n  loop_control:\n    loop_var: inv_item\n\n- name: extend invoker volume for coverage\n  set_fact:\n    volumes: \"{{ volumes|default('') }},{{ coverage_logs_dir }}/invoker:/coverage\"\n  when: coverage_enabled\n\n- name: set invoker docker volumes\n  set_fact:\n    volumes: \"{{ volumes|default('') }},{{ invoker.docker.volumes | join(',') }}\"\n  when: invoker.docker.volumes|length > 0\n\n- name: start invoker\n  docker_container:\n    userns_mode: \"host\"\n    pid_mode: \"host\"\n    privileged: \"yes\"\n    name: \"{{ invoker_name }}\"\n    hostname: \"{{ invoker_name }}\"\n    restart_policy: \"{{ docker.restart.policy }}\"\n    image: \"{{ docker_registry }}{{ docker.image.prefix }}/invoker:{{ 'cov' if (coverage_enabled) else docker.image.tag }}\"\n    state: started\n    recreate: true\n    env: \"{{ env }}\"\n    volumes: \"{{ volumes }}\"\n    ports: \"{{ invoker_ports_to_expose }}\"\n    command: /bin/sh -c \"exec /init.sh --id {{ invoker_index }} --uniqueName {{ invoker_index }} >> /logs/{{ invoker_name }}_logs.log 2>&1\"\n  when: not lean\n\n- name: wait until Invoker is up and running\n  block:\n    - uri:\n        url: \"{{ invoker.protocol }}://{{ ansible_host }}:{{ invoker.port + (invoker_index | int) }}/ping\"\n        validate_certs: \"no\"\n        client_key: \"{{ invoker.confdir }}/{{ invoker_name }}/{{ invoker.ssl.key }}\"\n        client_cert: \"{{ invoker.confdir }}/{{ invoker_name }}/{{ invoker.ssl.cert }}\"\n      register: result\n      until: result.status == 200\n      retries: 12\n      delay: 5\n  rescue:\n    - name: dump invoker docker logs for debugging\n      shell: \"docker logs {{ invoker_name }}\"\n      register: invoker_logs\n      failed_when: false\n    - name: output invoker docker logs for debugging\n      debug:\n        var: invoker_logs.stdout_lines\n    - name: dump invoker file logs from /var/tmp/wsklogs\n      shell: \"cat /var/tmp/wsklogs/{{ invoker_name }}/{{ invoker_name }}_logs.log\"\n      register: invoker_file_logs\n      failed_when: false\n    - name: output invoker file logs for debugging\n      debug:\n        var: invoker_file_logs.stdout_lines\n    - fail:\n        msg: \"Invoker failed to start; logs emitted above\"\n  when: not lean\n"
  },
  {
    "path": "ansible/roles/invoker/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install invoker in group 'invokers' in the environment inventory\n# In deploy mode it will deploy invokers.\n# In clean mode it will remove the invoker containers.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/invoker/templates/config.j2",
    "content": "include classpath(\"application.conf\")\n\n# Specify any custom config here. For example\n# whisk {\n#   metrics {\n#     prometheus-enabled = true\n#   }\n# }"
  },
  {
    "path": "ansible/roles/kafka/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove kafka and zookeeper containers.\n\n- name: remove old kafka\n  docker_container:\n    name: kafka\n    image: \"{{ docker_registry }}{{ docker.image.prefix }}/kafka:{{ docker.image.tag }}\"\n    keep_volumes: False\n    state: absent\n  ignore_errors: True\n\n- name: remove kafka\n  docker_container:\n    name: kafka{{ groups['kafkas'].index(inventory_hostname) }}\n    image: \"{{ docker_registry }}{{ docker.image.prefix }}/kafka:{{ docker.image.tag }}\"\n    keep_volumes: False\n    state: absent\n  ignore_errors: True\n"
  },
  {
    "path": "ansible/roles/kafka/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install Kafka with Zookeeper in group 'kafka' in the environment inventory\n\n- name: \"create kafka certificate directory\"\n  file:\n    path: \"{{ config_root_dir }}/kafka/certs\"\n    state: directory\n    mode: 0777\n\n- name: \"copy keystore\"\n  when: kafka.protocol == 'SSL'\n  copy:\n    src: \"files/{{ kafka.ssl.keystore.name }}\"\n    dest: \"{{ config_root_dir }}/kafka/certs\"\n\n- name: add kafka default env vars\n  set_fact:\n    kafka_env_vars:\n      \"KAFKA_DEFAULT_REPLICATION_FACTOR\": \"{{ kafka.replicationFactor }}\"\n      \"KAFKA_BROKER_ID\": \"{{ groups['kafkas'].index(inventory_hostname) }}\"\n      \"KAFKA_HEAP_OPTS\": \"-Xmx{{ kafka.heap }} -Xms{{ kafka.heap }}\"\n      \"KAFKA_ZOOKEEPER_CONNECT\": \"{{ zookeeper_connect_string }}\"\n      \"KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR\": \"{{ kafka.offsetsTopicReplicationFactor }}\"\n      \"KAFKA_AUTO_CREATE_TOPICS_ENABLE\": \"false\"\n      \"KAFKA_NUM_NETWORK_THREADS\": \"{{ kafka.networkThreads }}\"\n      \"TZ\": \"{{ docker.timezone }}\"\n\n- name: add kafka non-ssl vars\n  when: kafka.protocol != 'SSL'\n  set_fact:\n    kafka_non_ssl_vars:\n      \"KAFKA_ADVERTISED_PORT\": \"{{ kafka.advertisedPort + groups['kafkas'].index(inventory_hostname) }}\"\n      \"KAFKA_PORT\": \"{{ kafka.advertisedPort + groups['kafkas'].index(inventory_hostname) }}\"\n      \"KAFKA_LISTENER_SECURITY_PROTOCOL_MAP\": \"EXTERNAL:PLAINTEXT\"\n      \"KAFKA_LISTENERS\": \"EXTERNAL://:{{ kafka.advertisedPort + groups['kafkas'].index(inventory_hostname) }}\"\n      \"KAFKA_ADVERTISED_LISTENERS\": \"EXTERNAL://{{ ansible_host }}:{{ kafka.advertisedPort + groups['kafkas'].index(inventory_hostname) }}\"\n      \"KAFKA_INTER_BROKER_LISTENER_NAME\": \"EXTERNAL\"\n\n- name: add kafka ssl env vars\n  when: kafka.protocol == 'SSL'\n  set_fact:\n    kafka_ssl_env_vars:\n      \"KAFKA_ADVERTISED_PORT\": \"{{ kafka.advertisedPort + groups['kafkas'].index(inventory_hostname) }}\"\n      \"KAFKA_PORT\": \"{{ kafka.port + groups['kafkas'].index(inventory_hostname) }}\"\n      \"KAFKA_LISTENER_SECURITY_PROTOCOL_MAP\": \"INTERNAL:PLAINTEXT,EXTERNAL:SSL\"\n      \"KAFKA_LISTENERS\": \"EXTERNAL://:{{ kafka.advertisedPort + groups['kafkas'].index(inventory_hostname) }},INTERNAL://:{{ kafka.port + groups['kafkas'].index(inventory_hostname) }}\"\n      \"KAFKA_ADVERTISED_LISTENERS\": \"EXTERNAL://{{ ansible_host }}:{{ kafka.advertisedPort + groups['kafkas'].index(inventory_hostname) }},INTERNAL://{{ ansible_host }}:{{ kafka.port + groups['kafkas'].index(inventory_hostname) }}\"\n      \"KAFKA_INTER_BROKER_LISTENER_NAME\": \"INTERNAL\"\n      \"KAFKA_SSL_KEYSTORE_LOCATION\": \"/config/{{ kafka.ssl.keystore.name }}\"\n      \"KAFKA_SSL_KEYSTORE_PASSWORD\": \"{{ kafka.ssl.keystore.password }}\"\n      \"KAFKA_SSL_KEY_PASSWORD\": \"{{ kafka.ssl.keystore.password }}\"\n      \"KAFKA_SSL_TRUSTSTORE_LOCATION\": \"/config/{{ kafka.ssl.keystore.name }}\"\n      \"KAFKA_SSL_TRUSTSTORE_PASSWORD\": \"{{ kafka.ssl.keystore.password }}\"\n      \"KAFKA_SSL_CLIENT_AUTH\": \"{{ kafka.ssl.client_authentication }}\"\n      \"KAFKA_SSL_CIPHER_SUITES\": \"{{ kafka.ssl.cipher_suites | join(',') }}\"\n      \"KAFKA_SSL_ENABLED_PROTOCOLS\": \"{{ kafka.ssl.protocols | join(',') }}\"\n\n- name: \"join kafka ssl env vars\"\n  when: kafka.protocol == 'SSL'\n  set_fact:\n    kafka_env_vars: \"{{ kafka_env_vars | combine(kafka_ssl_env_vars) }}\"\n\n- name: join kafka non-ssl env vars\n  when: kafka.protocol != 'SSL'\n  set_fact:\n    kafka_env_vars: \"{{ kafka_env_vars | combine(kafka_non_ssl_vars) }}\"\n\n- name: \"(re)start kafka using '{{ kafka_image }}' \"\n  vars:\n    zookeeper_idx: \"{{ groups['kafkas'].index(inventory_hostname) % (groups['zookeepers'] | length) }}\"\n    kafka_image: \"{{ kafka.docker_image | default ('wurstmeister/kafka:' ~ kafka.version) }}\"\n  docker_container:\n    name: kafka{{ groups['kafkas'].index(inventory_hostname) }}\n    image: \"{{ kafka_image }}\"\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    env: \"{{ kafka_env_vars }}\"\n    ports:\n      - \"{{ kafka.port + groups['kafkas'].index(inventory_hostname) }}:{{ kafka.port + groups['kafkas'].index(inventory_hostname) }}\"\n      - \"{{ kafka.advertisedPort + groups['kafkas'].index(inventory_hostname) }}:{{ kafka.advertisedPort + groups['kafkas'].index(inventory_hostname) }}\"\n    volumes:\n      - \"{{ config_root_dir }}/kafka/certs:/config\"\n    pull: \"{{ kafka.pull_kafka | default(true) }}\"\n\n- name: wait until the kafka server started up\n  shell:\n    (echo dump; sleep 1) |\n    nc {{hostvars[groups['zookeepers']|first].ansible_host}} {{zookeeper.port}} |\n    grep /brokers/ids/{{ groups['kafkas'].index(inventory_hostname) }}\n  register: result\n  until: (result.rc == 0)\n  retries: 10\n  delay: 5\n"
  },
  {
    "path": "ansible/roles/kafka/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install kafka in group 'kafka' in the environment inventory\n# In deploy mode it will deploy kafka including zookeeper.\n# In clean mode it will remove kafka and zookeeper containers.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/mongodb/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove MongoDB server\n\n- name: remove MongoDB\n  docker_container:\n    name: mongodb\n    state: absent\n    keep_volumes: False\n"
  },
  {
    "path": "ansible/roles/mongodb/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will run a MongoDB server on the db group, this is only for test, please use\n# shared cluster for production env\n\n- name: (re)start mongodb\n  vars:\n    mongodb_image: \"{{ mongodb.docker_image | default('mongo:' ~ mongodb.version ) }}\"\n  docker_container:\n    name: mongodb\n    image: \"{{ mongodb_image }}\"\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    hostname: \"mongodb\"\n    user: \"mongodb\"\n    volumes:\n      - \"{{ db.mongodb.data_volume }}:/data/db\"\n    ports:\n      - \"27017:27017\"\n\n- name: wait until the MongoDB in this host is up and running\n  local_action: wait_for host={{ ansible_host }} port=27017 state=started delay=5 timeout=60\n"
  },
  {
    "path": "ansible/roles/mongodb/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will deploy a database server. Use the role if you want to use CouchCB locally.\n# In deploy mode it will start the MongoDB container.\n# In clean mode it will remove the MongoDB container.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n\n"
  },
  {
    "path": "ansible/roles/nginx/files/openwhisk-server-key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1Xk16caqAdcf3NITVcFsqXHceQf10lPjlKT5Dg+bbJPwS+9p\nqAsYOm0vLP4F72S7/A9IfgMskyUbRsumJlxSzR5Je4obMVQQCBB5fz90RzKdaqIs\nrZdwhmoBfr9VdUqOJjcUPe0TO2FW7QHO0pP1WwIQBfv29n1itkiLlMZOJgNSWcmF\nY7Hgj/7WDM+wM/XWw9lKCF3J9GsXs9nA70Alf/+bvlbPfSVgK3T4sIcBZl1b+OvK\ns+34zr2xGjW7Zbv1S+MWlG/342bElFoTq+N1KgRLH1rTPdHZfY/qh3hYeBN416xB\noQ73wV76Jm+U+k1DfVt44Rq8E/RVwLjZiByylQIDAQABAoIBAQCbZwLNbXdDobSr\nTy8OJaIR9DaY0Set9q3c/v/jsY7myweKb/5Ne84mcmd+bGorrPyAcSvHuE3RzJh/\nwC7zDBCnC95YleBX16dYB447CRl/3yPjha3arT0YTMFL4MO4gA7dWQleT2DJwIHE\nRaWnfFiH6Qd7I8LUC9E5e7RJGNe5KSphJ4bq58WHzEQXc9iqYLzc3AcS3TRENv8d\nu5GAWs7OUOc5mmktlNLptUIxwZBAkW5KHVWMxi8lrMDKqatL2Z/ydCcDezw7IZPd\n7ezQFt80k1I3/Ijxl5knPtn93N9iOk9A9LFmhYnnaBDD2uFFfZGac9A5U6f4Lz5m\ninWBwwo9AoGBAP2ruIB3HSV1ciUcsNPASo4PlAGpndaVGuKsTD1/T/GMR42jVbWP\nisP2HbDt3W9nd3lk3wSFVmtIq+vVHzpW9Q5F/aRBsdU/ofvWGZrlkO9Vo++eUABW\n3MB0gqJGapF+eDLde6IE2IsECckLEB+peLU5K2xSeAkBD/85d1nXCpobAoGBANdv\nAIIR4tmjRdtjimXVNSR53SGy7DEHemLs2Tw40Cg6GWTzA7R0Tv9FJHoSFtXYqZxw\nLP4avV8yEp2q5bfJaFHGxu8LQIGBmDL+yydmuy6vgTVXJD4H7YBH4Xlb0ckaz+ju\ninpzUHaIVvTORy6y8Mu3U39oSU7Yfn2iy1Ic47EPAoGAHH7n0PaQfZ693dFlQ8Q8\nG81AMReetXY2ePQl9FqS3m2FtDF+9VBUpELHfxKZZ2RWFXrxWo6n8JFPTsS4J1OR\nX7MZFRSUJ4JobePVKINVTq1uJwK/teoMDkqISjZizklIs14R/1dQA/3GI6FshEID\nX0g2yopRFaHa7C7Ga38un7UCgYB2wcL28KMrtByLLtkY/6oW3HKw4+/dqzClHck2\nsF7W/ggHpQrSzBbMEzJjdFtQMOp2yUOUI+tmcbTfY1jUslsmUTxSg9JgUa8z1U7p\n/nCK8MZ6P/pDk50xzO4XNy1y/avEzNJbY/vkC45bzuZgcNXahsmpfzSCGUfJPBd2\nwWQmswKBgQCbfjCFq0M1qUhesWDgJnmx0tav1Gn/KkWWe7GYVibj0nuW6QYR8exQ\nziiMW06DJDD2n2h9mrJ6Rin1dMDRS4nlmzOCqDwRNthtOpr1VDa/U/QRMEr0YiwI\n3OspdVR5BjIw9lbFrM2Idm+fdQtDnj7lHZntN6Env2nNr3DsIAkPZA==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "ansible/roles/nginx/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove nginx containers.\n\n- name: remove nginx\n  docker_container:\n    name: nginx\n    image: nginx\n    state: absent\n  ignore_errors: True\n\n- name: remove nginx config directory\n  file:\n    path: \"{{ nginx.confdir }}\"\n    state: absent\n  become: \"{{ nginx.dir.become }}\"\n\n- name: remove nginx log directory\n  file:\n    path: \"{{ whisk_logs_dir }}/nginx\"\n    state: absent\n  become: \"{{ logs.dir.become }}\"\n"
  },
  {
    "path": "ansible/roles/nginx/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role starts a nginx component\n\n- name: ensure nginx config directory exists\n  file:\n    path: \"{{ nginx.confdir }}\"\n    state: directory\n  become: \"{{ nginx.dir.become }}\"\n\n- name: copy template from local to remote in nginx config directory\n  template:\n    src: nginx.conf.j2\n    dest: \"{{ nginx.confdir }}/nginx.conf\"\n\n- name: copy cert files from local to remote in nginx config directory\n  copy:\n    src: \"{{ nginx.ssl.path }}/{{ item }}\"\n    dest: \"{{ nginx.confdir }}\"\n  with_items:\n        - \"{{ nginx.ssl.cert }}\"\n        - \"{{ nginx.ssl.key }}\"\n        - \"{{ nginx.ssl.client_ca_cert }}\"\n\n- name: copy password files for cert from local to remote in nginx config directory\n  copy:\n    src: \"{{ nginx.ssl.path }}/{{ nginx.ssl.password_file }}\"\n    dest: \"{{ nginx.confdir }}\"\n  when: nginx.ssl.password_file\n\n- name: copy controller cert for authentication\n  copy:\n    src: \"{{ openwhisk_home }}/ansible/roles/controller/files/{{ item }}\"\n    dest: \"{{ nginx.confdir }}\"\n  with_items:\n        - \"{{ controller.ssl.cert }}\"\n        - \"{{ controller.ssl.key }}\"\n  when: controller.protocol == 'https'\n\n- name: ensure nginx log directory is created with permissions\n  file:\n    path: \"{{ whisk_logs_dir }}/nginx\"\n    state: directory\n    mode: 0777\n  become: \"{{ logs.dir.become }}\"\n\n- name: \"pull the nginx:{{ nginx.version }} image\"\n  shell: \"docker pull nginx:{{ nginx.version }}\"\n  register: result\n  until: (result.rc == 0)\n  retries: \"{{ docker.pull.retries }}\"\n  delay: \"{{ docker.pull.delay }}\"\n\n- name: ensure html directory exists\n  stat:\n    path: \"{{ nginx.htmldir }}\"\n  register: nginx_html_dir_exists\n  when: nginx.htmldir\n\n- name: check if html directory exists\n  fail:\n    msg: html directory does not exist '{{ nginx.htmldir }}'\n  when: nginx.htmldir and not (nginx_html_dir_exists.stat.exists and nginx_html_dir_exists.stat.isdir)\n\n- name: configuration volumes to mount\n  set_fact:\n    volumes:\n      - \"{{ whisk_logs_dir }}/nginx:/logs\"\n      - \"{{ nginx.confdir }}:/etc/nginx\"\n\n- name: \"optional html volume to mount\"\n  set_fact:\n    volumes: \"{{ volumes }} + [ '{{ nginx.htmldir }}:/usr/share/nginx/html' ]\"\n  when: nginx.htmldir\n\n- name: (re)start nginx\n  docker_container:\n    name: nginx\n    image: nginx:{{ nginx.version }}\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    hostname: \"nginx\"\n    volumes: \"{{ volumes }}\"\n    ports:\n      - \"{{ nginx.port.http }}:80\"\n      - \"{{ nginx.port.https }}:443\"\n    env:\n      TZ: \"{{ docker.timezone }}\"\n"
  },
  {
    "path": "ansible/roles/nginx/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install nginx.\n# In deploy mode it will deploy an nginx server.\n# In clean mode it will remove the nginx server as well as nginx.confdir\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/nginx/templates/nginx.conf.j2",
    "content": "{# this template is used to generate a nginx.conf for booting a nginx server based on the given environment inventory #}\n\nworker_processes {{ nginx.wpn.router }};\nworker_rlimit_nofile 4096;\n\nevents {\n{# default: 1024 #}\n    worker_connections 4096;\n}\n\nhttp {\n{# allow large uploads, need to thread proper limit into here #}\n    client_max_body_size 65M;\n\n    rewrite_log on;\n{# change log format to display the upstream information #}\n    log_format combined-upstream '$remote_addr - $remote_user [$time_local] '\n        '[#tid_$request_id] $request $status $body_bytes_sent '\n        '$http_referer $http_user_agent $upstream_addr';\n    access_log /logs/nginx_access.log combined-upstream;\n\n{# needed to enable keepalive to upstream controllers #}\n    proxy_http_version 1.1;\n    proxy_set_header Connection \"\";\n    proxy_buffers 16 4k;\n\n{% if controller.protocol == 'https' %}\n    proxy_ssl_session_reuse on;\n    proxy_ssl_name {{ controller.ssl.cn }};\n    proxy_ssl_verify on;\n    proxy_ssl_trusted_certificate /etc/nginx/{{ controller.ssl.cert }};\n    proxy_ssl_protocols TLSv1.1 TLSv1.2;\n    proxy_ssl_certificate /etc/nginx/{{ controller.ssl.cert }};\n    proxy_ssl_certificate_key /etc/nginx/{{ controller.ssl.key }};\n{% endif %}\n\n    gzip_static on;\n    types {\n        text/html                                        html htm shtml;\n        text/css                                         css;\n        text/xml                                         xml;\n        image/gif                                        gif;\n        image/jpeg                                       jpeg jpg;\n        image/png                                        png;\n        image/svg+xml                                    svg svgz;\n        image/x-icon                                     ico;\n        image/x-jng                                      jng;\n        image/x-ms-bmp                                   bmp;\n        application/javascript                           js;\n        application/json                                 json;\n        application/java-archive                         jar war ear;\n        application/pdf                                  pdf;\n    }\n\n    upstream controllers {\n        # Mark the controller as unavailable after fail_timeout seconds, to not get any requests during restart.\n        # Otherwise, nginx would dispatch requests when the container is up, but the backend in the container not.\n        # From the docs:\n        #  \"normally, requests with a non-idempotent method (POST, LOCK, PATCH) are not passed to\n        #   the next server if a request has been sent to an upstream server\"\n{% for c in groups['controllers'] %}\n        server {{ hostvars[c].ansible_host }}:{{ controller.basePort + groups['controllers'].index(c) }} fail_timeout=60s;\n{% endfor %}\n        keepalive 512;\n    }\n\n{# determine \"special users\" to send the header accordingly #}\n    map \"$remote_user\" $special_user {\n      default not_special;\n{% for user in nginx.special_users %}\n      \"{{ user }}\" special;\n{% endfor %}\n    }\n\n    map \"$special_user:$http_x_ow_extra_logging\" $extra_logging {\n      default \"off\";\n      \"special:off\" off;\n      \"special:on\" on;\n      \"special:\" on;\n    }\n\n    proxy_set_header X-OW-EXTRA-LOGGING $extra_logging;\n    # Set the request id generated by nginx as tid-header to the upstream.\n    # This tid is either the request-id generated by nginx or a tid, sent by the caller.\n    proxy_set_header {{ transactions.header }} $request_id;\n\n    # Send the tid always back as header.\n    add_header {{ transactions.header }} $request_id always;\n\n{# Turn off sending information about the server to the client #}\n    server_tokens off;\n\n    # Redirect all http to https.\n    server {\n        listen 80 default_server;\n        server_name _;\n        return 308 https://$host$request_uri;\n    }\n\n    server {\n        listen 443 default ssl;\n\n        # Match namespace, note while OpenWhisk allows a richer character set for a\n        # namespace, not all those characters are permitted in the (sub)domain name.\n        # If namespace does not match, no vanity URL rewriting takes place.\n        server_name ~^(?<namespace>[0-9a-zA-Z-]+)\\.{{ whisk_api_localhost_name | default(whisk_api_host_name) | default(whisk_api_localhost_name_default) }}$;\n\n        # Recommended TLS settings from: https://wiki.mozilla.org/Security/Server_Side_TLS\n        ssl_session_cache    shared:SSL:1m;\n        ssl_session_timeout  10m;\n        ssl_certificate      /etc/nginx/{{ nginx.ssl.cert }};\n        ssl_certificate_key  /etc/nginx/{{ nginx.ssl.key }};\n        {% if nginx.ssl.password_file %}\n        ssl_password_file   \"/etc/nginx/{{ nginx.ssl.password_file }}\";\n        {% endif %}\n        ssl_client_certificate /etc/nginx/{{ nginx.ssl.client_ca_cert }};\n        ssl_verify_client {{ nginx.ssl.verify_client }};\n        ssl_protocols        TLSv1.2;\n        ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256;\n        ssl_prefer_server_ciphers on;\n        proxy_ssl_session_reuse on;\n\n        # proxy to the web action path\n        location / {\n            if ($namespace) {\n              rewrite    /(.*) /api/v1/web/${namespace}/$1 break;\n            }\n            proxy_pass {{ controller.protocol }}://controllers;\n            proxy_read_timeout 75s; # 70+5 additional seconds to allow controller to terminate request\n        }\n\n        # proxy to 'public/html' web action by convention\n        location = / {\n            if ($namespace) {\n              rewrite    ^ /api/v1/web/${namespace}/public/index.html break;\n            }\n            proxy_pass {{ controller.protocol }}://controllers;\n            proxy_read_timeout 75s; # 70+5 additional seconds to allow controller to terminate request\n        }\n\n        location /blackbox.tar.gz {\n            return 301 https://github.com/apache/openwhisk-runtime-docker/releases/download/sdk%400.1.0/blackbox-0.1.0.tar.gz;\n        }\n\n        location /cli {\n            autoindex on;\n            alias /etc/nginx/cli/go/download;\n        }\n\n        # this is here for backward compatibility\n        location /cli/go/download {\n            rewrite /cli/go/download(.*) /cli$1 permanent;\n        }\n\n        location /metrics {\n            deny all;\n        }\n\n{% if nginx.htmldir %}\n        location /ui {\n            alias /usr/share/nginx/html;\n        }\n{% endif %}\n    }\n}\n"
  },
  {
    "path": "ansible/roles/prereq/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove prereq packages\n\n- name: remove requests\n  pip:\n    name: requests\n    state: absent\n  become: true\n\n- name: remove docker\n  pip:\n    name: docker\n    state: absent\n  become: true\n\n- name: remove httplib2\n  pip:\n    name: httplib2\n    state: absent\n  become: true\n"
  },
  {
    "path": "ansible/roles/prereq/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install all necessary packages for Openwhisk.\n\n- name: check for pip\n  shell: \"pip\"\n  register: pip_result\n  ignore_errors: true\n\n- name: install pip\n  shell: \"curl -k https://bootstrap.pypa.io/get-pip.py | python\"\n  become: true\n  when: pip_result.rc != 0\n\n- name: install requests\n  pip:\n    name: requests\n    version: 2.31.0\n  become: true\n\n- name: install docker for python\n  pip:\n    name: docker\n    version: 4.0.2\n  become: true\n\n- name: install httplib2\n  pip:\n    name: httplib2\n    version: 0.9.2\n  become: true\n"
  },
  {
    "path": "ansible/roles/prereq/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will prepare target VMs for a whisk deployment using ansible.\n# In deploy mode it will install all necessary packages and programs.\n# In clean mode it will uninstall packages from deploy mode.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/redis/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove redis container.\n\n- name: remove redis\n  docker_container:\n    name: redis\n    state: absent\n  ignore_errors: True\n"
  },
  {
    "path": "ansible/roles/redis/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install redis\n\n- name: \"pull the redis:{{ redis.version }} image\"\n  shell: \"docker pull redis:{{ redis.version }}\"\n  register: result\n  until: (result.rc == 0)\n  retries: \"{{ docker.pull.retries }}\"\n  delay: \"{{ docker.pull.delay }}\"\n\n- name: (re)start redis\n  docker_container:\n    name: redis\n    image: redis:{{ redis.version }}\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    ports:\n      - \"{{ redis.port }}:6379\"\n    env:\n      TZ: \"{{ docker.timezone }}\"\n    command:\n      /bin/sh -c\n      \"docker-entrypoint.sh --requirepass {{ redis.password }}\"\n\n- name: wait until redis is up and running\n  shell: \"docker run --link redis:redis --rm redis:{{ redis.version }} redis-cli -h redis -p 6379 -a {{ redis.password }} ping\"\n  register: result\n  until: (result.rc == 0) and (result.stdout == 'PONG')\n  retries: 12\n  delay: 5\n"
  },
  {
    "path": "ansible/roles/redis/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install redis in group 'redis' in the environment inventory\n# In deploy mode it will deploy redis.\n# In clean mode it will remove the redis containers.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/routemgmt/files/installRouteMgmt.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n########\n#\n# use the command line interface to install standard actions deployed\n# automatically\n#\n# To run this command\n# ./installRouteMgmt.sh  <AUTH> <APIHOST> <NAMESPACE> <WSK_CLI>\n# AUTH, APIHOST and NAMESPACE are found in $HOME/.wskprops\n# WSK_CLI=\"$OPENWHISK_HOME/bin/wsk\"\n\nset -e\n\nif [ $# -eq 0 ]\nthen\necho \"Usage: ./installRouteMgmt.sh AUTHKEY APIHOST NAMESPACE PATH_TO_WSK_CLI\"\nfi\n\nAUTH=\"$1\"\nAPIHOST=\"$2\"\nNAMESPACE=\"$3\"\nWSK_CLI=\"$4\"\n\nWHISKPROPS_FILE=\"$OPENWHISK_HOME/whisk.properties\"\nif [ -z \"$GW_USER\" ]; then\n   GW_USER=`fgrep apigw.auth.user= $WHISKPROPS_FILE | cut -d'=' -f2`\nfi\nif [ -z \"$GW_PWD\" ]; then\n    GW_PWD=`fgrep apigw.auth.pwd= $WHISKPROPS_FILE | cut -d'=' -f2-`\nfi\nif [ -z \"$GW_HOST_V2\" ]; then\n    GW_HOST_V2=`fgrep apigw.host.v2= $WHISKPROPS_FILE | cut -d'=' -f2`\nfi\n\n# If the auth key file exists, read the key in the file. Otherwise, take the\n# first argument as the key itself.\nif [ -f \"$AUTH\" ]; then\n    AUTH=`cat $AUTH`\nfi\n\nif [ ! -f $WSK_CLI ]; then\n    echo $WSK_CLI is missing\n    exit 1\nfi\n\nexport WSK_CONFIG_FILE= # override local property file to avoid namespace clashes\n\necho Installing apimgmt package\n$WSK_CLI -i --apihost \"$APIHOST\" package update --auth \"$AUTH\"  --shared no \"$NAMESPACE/apimgmt\" \\\n-a description \"This package manages the gateway API configuration.\" \\\n-p gwUser \"$GW_USER\" \\\n-p gwPwd \"$GW_PWD\" \\\n-p gwUrlV2 \"$GW_HOST_V2\"\n\necho Creating NPM module .zip files\ncd \"$OPENWHISK_HOME/core/routemgmt/getApi\"\ncp \"$OPENWHISK_HOME/core/routemgmt/common\"/*.js .\nnpm install\nzip -r getApi.zip *\n\ncd \"$OPENWHISK_HOME/core/routemgmt/createApi\"\ncp \"$OPENWHISK_HOME/core/routemgmt/common\"/*.js .\nnpm install\nzip -r createApi.zip *\n\ncd \"$OPENWHISK_HOME/core/routemgmt/deleteApi\"\ncp \"$OPENWHISK_HOME/core/routemgmt/common\"/*.js .\nnpm install\nzip -r deleteApi.zip *\n\necho Installing apimgmt actions\n$WSK_CLI -i --apihost \"$APIHOST\" action update --auth \"$AUTH\" \"$NAMESPACE/apimgmt/getApi\" \"$OPENWHISK_HOME/core/routemgmt/getApi/getApi.zip\" \\\n-a description 'Retrieve the specified API configuration (in JSON format)' \\\n--kind nodejs:default \\\n-a web-export true -a final true\n\n$WSK_CLI -i --apihost \"$APIHOST\" action update --auth \"$AUTH\" \"$NAMESPACE/apimgmt/createApi\" \"$OPENWHISK_HOME/core/routemgmt/createApi/createApi.zip\" \\\n-a description 'Create an API' \\\n--kind nodejs:default \\\n-a web-export true -a final true\n\n$WSK_CLI -i --apihost \"$APIHOST\" action update --auth \"$AUTH\" \"$NAMESPACE/apimgmt/deleteApi\" \"$OPENWHISK_HOME/core/routemgmt/deleteApi/deleteApi.zip\" \\\n-a description 'Delete the API' \\\n--kind nodejs:default \\\n-a web-export true -a final true\n"
  },
  {
    "path": "ansible/roles/routemgmt/files/uninstallRouteMgmt.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n########\n#\n# use the command line interface to install standard actions deployed\n# automatically\n#\n# To run this command\n# ./uninstallRouteMgmt.sh  <AUTH> <APIHOST> <NAMESPACE> <WSK_CLI>\n# AUTH, APIHOST and NAMESPACE are found in $HOME/.wskprops\n# WSK_CLI=\"$OPENWHISK_HOME/bin/wsk\"\n\nset -e\n\nif [ $# -eq 0 ]\nthen\necho \"Usage: ./uninstallRouteMgmt.sh AUTHKEY APIHOST NAMESPACE PATH_TO_WSK_CLI\"\nfi\n\nAUTH=\"$1\"\nAPIHOST=\"$2\"\nNAMESPACE=\"$3\"\nWSK_CLI=\"$4\"\n\n# If the auth key file exists, read the key in the file. Otherwise, take the\n# first argument as the key itself.\nif [ -f \"$AUTH\" ]; then\n    AUTH=`cat $AUTH`\nfi\n\nif [ ! -f $WSK_CLI ]; then\n    echo $WSK_CLI is missing\n    exit 1\nfi\n\nexport WSK_CONFIG_FILE= # override local property file to avoid namespace clashes\n\nfunction deleteAction\n{\n  # The \"get\" command will fail if the resource does not exist, so use \"set +e\" to avoid exiting the script\n  set +e\n  $WSK_CLI -i --apihost \"$APIHOST\" action get --auth \"$AUTH\" \"$1\"\n  RC=$?\n  if [ $RC -eq 0 ]\n  then\n    set -e\n    $WSK_CLI -i --apihost \"$APIHOST\" action delete --auth \"$AUTH\" \"$1\"\n  fi\n  set -e\n}\n\nfunction deletePackage\n{\n  # The \"get\" command will fail if the resource does not exist, so use \"set +e\" to avoid exiting the script\n  set +e\n  $WSK_CLI -i --apihost \"$APIHOST\" package get --auth \"$AUTH\" \"$1\" -s\n  RC=$?\n  if [ $RC -eq 0 ]\n  then\n    set -e\n    $WSK_CLI -i --apihost \"$APIHOST\" package delete --auth \"$AUTH\" \"$1\"\n  fi\n}\n\n# Delete actions, then the package.  The order is important (can't delete a package that contains an action)!\n\necho Deleting routemgmt actions\ndeleteAction $NAMESPACE/routemgmt/getApi\ndeleteAction $NAMESPACE/routemgmt/createApi\ndeleteAction $NAMESPACE/routemgmt/deleteApi\n\necho Deleting routemgmt package - but only if it exists\ndeletePackage $NAMESPACE/routemgmt\n\necho Deleting apimgmt actions\ndeleteAction $NAMESPACE/apimgmt/getApi\ndeleteAction $NAMESPACE/apimgmt/createApi\ndeleteAction $NAMESPACE/apimgmt/deleteApi\n\necho Deleting apimgmt package - but only if it exists\ndeletePackage $NAMESPACE/apimgmt\n"
  },
  {
    "path": "ansible/roles/routemgmt/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove API Gateway route management actions.\n\n- name: remove route management actions\n  shell: ./uninstallRouteMgmt.sh {{ catalog_auth_key }} {{ groups['edge'] | first }} {{ catalog_namespace }} {{ cli.path }} chdir=\"{{ openwhisk_home }}/ansible/roles/routemgmt/files\"\n  environment:\n    OPENWHISK_HOME: \"{{ openwhisk_home }}\"\n"
  },
  {
    "path": "ansible/roles/routemgmt/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Install the API Gateway route management actions.\n- name: install route management actions\n  shell: ./installRouteMgmt.sh {{ catalog_auth_key }} {{ whisk_api_host_name | default(groups['edge'] | first) }} {{ catalog_namespace }} {{ cli.path }} chdir=\"{{ openwhisk_home }}/ansible/roles/routemgmt/files\"\n  environment:\n    OPENWHISK_HOME: \"{{ openwhisk_home }}\"\n"
  },
  {
    "path": "ansible/roles/routemgmt/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install the API Gateway route management actions\n# In deploy mode it will deploy the API Gateway route management actions.\n# In clean mode it will remove the API Gateway route management actions.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/schedulers/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove scheduler containers.\n\n- name: get scheduler name\n  set_fact:\n    scheduler_name: \"{{ name_prefix ~ host_group.index(inventory_hostname) }}\"\n\n- name: remove scheduler\n  docker_container:\n    name: \"{{ scheduler_name }}\"\n    state: absent\n  ignore_errors: \"True\"\n\n- name: remove scheduler log directory\n  file:\n    path: \"{{ whisk_logs_dir }}/{{ scheduler_name }}\"\n    state: absent\n  become: \"{{ logs.dir.become }}\"\n\n- name: remove scheduler conf directory\n  file:\n    path: \"{{ scheduler.confdir }}/{{ scheduler_name }}\"\n    state: absent\n  become: \"{{ scheduler.dir.become }}\"\n"
  },
  {
    "path": "ansible/roles/schedulers/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install Scheduler in group 'schedulers' in the environment\n# inventory\n\n- import_tasks: docker_login.yml\n\n- name: get scheduler name and index\n  set_fact:\n    scheduler_name: \"{{ name_prefix ~ host_group.index(inventory_hostname) }}\"\n    scheduler_index:\n      \"{{ (scheduler_index_base|int) + host_group.index(inventory_hostname) }}\"\n\n- name: \"pull the {{ docker.image.tag }} image of scheduler\"\n  shell: \"docker pull {{docker_registry}}{{ docker.image.prefix }}/scheduler:{{docker.image.tag}}\"\n  when: docker_registry != \"\"\n  register: result\n  until: (result.rc == 0)\n  retries: \"{{ docker.pull.retries }}\"\n  delay: \"{{ docker.pull.delay }}\"\n\n- name: ensure scheduler log directory is created with permissions\n  file:\n    path: \"{{ whisk_logs_dir }}/{{ scheduler_name }}\"\n    state: directory\n    mode: 0777\n  become: \"{{ logs.dir.become }}\"\n\n# We need to create the file with proper permissions because the dir creation above\n# does not result in a dir with full permissions in docker machine especially with macos mounts\n- name: ensure scheduler log file is created with permissions\n  file:\n    path: \"{{ whisk_logs_dir }}/{{ scheduler_name }}/{{ scheduler_name }}_logs.log\"\n    state: touch\n    mode: 0777\n  when: environment_type is defined and environment_type == \"docker-machine\"\n\n- name: ensure scheduler config directory is created with permissions\n  file:\n    path: \"{{ scheduler.confdir }}/{{ scheduler_name }}\"\n    state: directory\n    mode: 0777\n  become: \"{{ scheduler.dir.become }}\"\n\n- name: check, that required databases exist\n  include_tasks: \"{{ openwhisk_home }}/ansible/tasks/db/checkDb.yml\"\n  vars:\n    dbName: \"{{ item }}\"\n    dbUser: \"{{ db.credentials.scheduler.user }}\"\n    dbPass: \"{{ db.credentials.scheduler.pass }}\"\n  with_items:\n  - \"{{ db.whisk.auth }}\"\n\n- name: copy jmxremote password file\n  when: jmx.enabled\n  template:\n    src: \"jmxremote.password.j2\"\n    dest: \"{{ scheduler.confdir }}/{{ scheduler_name }}/jmxremote.password\"\n    mode: 0777\n\n- name: copy jmxremote access file\n  when: jmx.enabled\n  template:\n    src: \"jmxremote.access.j2\"\n    dest: \"{{ scheduler.confdir }}/{{ scheduler_name }}/jmxremote.access\"\n    mode: 0777\n\n- name: prepare scheduler port\n  set_fact:\n    scheduler_port: \"{{ scheduler.basePort + (scheduler_index | int) }}\"\n    ports_to_expose:\n      - \"{{ scheduler.grpc.basePort + (scheduler_index | int) }}:{{ scheduler.grpc.basePort + (scheduler_index | int) }}\"\n      - \"{{ scheduler.basePort + (scheduler_index | int) }}:8080\"\n\n- name: expose additional ports if jmxremote is enabled\n  when: jmx.enabled\n  vars:\n    jmx_remote_port: \"{{ jmx.basePortScheduler + (scheduler_index|int) }}\"\n    jmx_remote_rmi_port:\n      \"{{ jmx.rmiBasePortScheduler + (scheduler_index|int) }}\"\n  set_fact:\n    ports_to_expose: >-\n      {{ ports_to_expose }} +\n      [ '{{ jmx_remote_port }}:{{ jmx_remote_port }}' ] +\n      [ '{{ jmx_remote_rmi_port }}:{{ jmx_remote_rmi_port }}' ]\n    scheduler_args: >-\n      {{ scheduler.arguments }}\n      {{ jmx.jvmCommonArgs }}\n      -Djava.rmi.server.hostname={{ ansible_host }}\n      -Dcom.sun.management.jmxremote.rmi.port={{ jmx_remote_rmi_port }}\n      -Dcom.sun.management.jmxremote.port={{ jmx_remote_port }}\n\n- name: populate environment variables for scheduler\n  set_fact:\n    env:\n      \"JAVA_OPTS\":\n        -Xmx{{ scheduler.heap }}\n        -XX:+CrashOnOutOfMemoryError\n        -XX:+UseGCOverheadLimit\n        -XX:ErrorFile=/logs/java_error.log\n        -XX:+HeapDumpOnOutOfMemoryError\n        -XX:HeapDumpPath=/logs\n      \"SCHEDULER_OPTS\": \"{{ scheduler_args | default(scheduler.arguments) }}\"\n      \"SCHEDULER_INSTANCES\": \"{{ scheduler.instances }}\"\n      \"JMX_REMOTE\": \"{{ jmx.enabled }}\"\n      \"PORT\": \"8080\"\n\n      \"WHISK_SCHEDULER_ENDPOINTS_HOST\": \"{{ ansible_host }}\"\n      \"WHISK_SCHEDULER_ENDPOINTS_RPCPORT\": \"{{ scheduler.grpc.basePort + (scheduler_index | int)}}\"\n      \"WHISK_SCHEDULER_ENDPOINTS_PEKKOPORT\": \"{{ scheduler.pekko.cluster.basePort + (scheduler_index | int) }}\"\n      \"CONFIG_whisk_scheduler_protocol\": \"{{ scheduler.protocol }}\"\n      \"CONFIG_whisk_scheduler_maxPeek\": \"{{ scheduler.maxPeek }}\"\n      \"CONFIG_whisk_scheduler_dataManagementService_retryInterval\": \"{{ scheduler.dataManagementService.retryInterval }}\"\n      \"CONFIG_whisk_scheduler_inProgressJobRetention\": \"{{ scheduler.inProgressJobRetention }}\"\n      \"CONFIG_whisk_scheduler_blackboxMultiple\": \"{{ scheduler.blackboxMultiple }}\"\n      \"CONFIG_whisk_scheduler_scheduling_staleThreshold\": \"{{ scheduler.scheduling.staleThreshold }}\"\n      \"CONFIG_whisk_scheduler_scheduling_checkInterval\": \"{{ scheduler.scheduling.checkInterval }}\"\n      \"CONFIG_whisk_scheduler_scheduling_dropInterval\": \"{{ scheduler.scheduling.dropInterval }}\"\n      \"CONFIG_whisk_scheduler_queueManager_maxSchedulingTime\": \"{{ scheduler.queueManager.maxSchedulingTime }}\"\n      \"CONFIG_whisk_scheduler_queueManager_maxRetriesToGetQueue\": \"{{ scheduler.queueManager.maxRetriesToGetQueue }}\"\n      \"CONFIG_whisk_scheduler_queue_idleGrace\": \"{{ scheduler.queue.idleGrace }}\"\n      \"CONFIG_whisk_scheduler_queue_stopGrace\": \"{{ scheduler.queue.stopGrace }}\"\n      \"CONFIG_whisk_scheduler_queue_flushGrace\": \"{{ scheduler.queue.flushGrace }}\"\n      \"CONFIG_whisk_scheduler_queue_gracefulShutdownTimeout\": \"{{ scheduler.queue.gracefulShutdownTimeout }}\"\n      \"CONFIG_whisk_scheduler_queue_maxRetentionSize\": \"{{ scheduler.queue.maxRetentionSize }}\"\n      \"CONFIG_whisk_scheduler_queue_maxRetentionMs\": \"{{ scheduler.queue.maxRetentionMs }}\"\n      \"CONFIG_whisk_scheduler_queue_maxBlackboxRetentionMs\": \"{{ scheduler.queue.maxBlackboxRetentionMs }}\"\n      \"CONFIG_whisk_scheduler_queue_throttlingFraction\": \"{{ scheduler.queue.throttlingFraction }}\"\n      \"CONFIG_whisk_scheduler_queue_durationBufferSize\": \"{{ scheduler.queue.durationBufferSize }}\"\n      \"CONFIG_whisk_durationChecker_timeWindow\": \"{{ durationChecker.timeWindow }}\"\n\n      \"TZ\": \"{{ docker.timezone }}\"\n\n      \"KAFKA_HOSTS\": \"{{ kafka_connect_string }}\"\n      \"CONFIG_whisk_kafka_replicationFactor\":\n        \"{{ kafka.replicationFactor | default() }}\"\n      \"CONFIG_whisk_kafka_topics_scheduler_retentionBytes\":\n        \"{{ kafka_topics_scheduler_retentionBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_scheduler_retentionMs\":\n        \"{{ kafka_topics_scheduler_retentionMS | default() }}\"\n      \"CONFIG_whisk_kafka_topics_scheduler_segmentBytes\":\n        \"{{ kafka_topics_scheduler_segmentBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_creationAck_retentionBytes\":\n        \"{{ kafka_topics_creationAck_retentionBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_creationAck_retentionMs\":\n        \"{{ kafka_topics_creationAck_retentionMS | default() }}\"\n      \"CONFIG_whisk_kafka_topics_creationAck_segmentBytes\":\n        \"{{ kafka_topics_creationAck_segmentBytes | default() }}\"\n      \"CONFIG_whisk_kafka_topics_prefix\":\n        \"{{ kafka.topicsPrefix }}\"\n      \"CONFIG_whisk_kafka_topics_userEvent_prefix\":\n        \"{{ kafka.topicsUserEventPrefix }}\"\n      \"CONFIG_whisk_kafka_common_securityProtocol\":\n        \"{{ kafka.protocol }}\"\n      \"CONFIG_whisk_kafka_common_sslTruststoreLocation\":\n        \"/conf/{{ kafka.ssl.keystore.name }}\"\n      \"CONFIG_whisk_kafka_common_sslTruststorePassword\":\n        \"{{ kafka.ssl.keystore.password }}\"\n      \"CONFIG_whisk_kafka_common_sslKeystoreLocation\":\n        \"/conf/{{ kafka.ssl.keystore.name }}\"\n      \"CONFIG_whisk_kafka_common_sslKeystorePassword\":\n        \"{{ kafka.ssl.keystore.password }}\"\n      \"ZOOKEEPER_HOSTS\": \"{{ zookeeper_connect_string }}\"\n\n      \"LIMITS_ACTIONS_INVOKES_CONCURRENT\": \"{{ limits.concurrentInvocations }}\"\n\n      \"CONFIG_whisk_couchdb_protocol\": \"{{ db.protocol }}\"\n      \"CONFIG_whisk_couchdb_host\": \"{{ db.host }}\"\n      \"CONFIG_whisk_couchdb_port\": \"{{ db.port }}\"\n      \"CONFIG_whisk_couchdb_username\": \"{{ db.credentials.scheduler.user }}\"\n      \"CONFIG_whisk_couchdb_password\": \"{{ db.credentials.scheduler.pass }}\"\n      \"CONFIG_whisk_couchdb_provider\": \"{{ db.provider }}\"\n      \"CONFIG_whisk_couchdb_databases_WhiskAuth\": \"{{ db.whisk.auth }}\"\n      \"CONFIG_whisk_couchdb_databases_WhiskEntity\": \"{{ db.whisk.actions }}\"\n      \"CONFIG_whisk_couchdb_databases_WhiskActivation\": \"{{ db.whisk.activations }}\"\n      \"CONFIG_whisk_db_actionsDdoc\": \"{{ db_whisk_actions_ddoc | default() }}\"\n      \"CONFIG_whisk_db_activationsDdoc\": \"{{ db_whisk_activations_ddoc | default() }}\"\n      \"CONFIG_whisk_db_activationsFilterDdoc\": \"{{ db_whisk_activations_filter_ddoc | default() }}\"\n      \"CONFIG_whisk_userEvents_enabled\": \"{{ user_events | default(false) | lower }}\"\n\n      \"CONFIG_whisk_memory_min\": \"{{ limit_action_memory_min | default() }}\"\n      \"CONFIG_whisk_memory_max\": \"{{ limit_action_memory_max | default() }}\"\n      \"CONFIG_whisk_memory_std\": \"{{ limit_action_memory_std | default() }}\"\n\n      \"CONFIG_whisk_timeLimit_min\": \"{{ limit_action_time_min | default() }}\"\n      \"CONFIG_whisk_timeLimit_max\": \"{{ limit_action_time_max | default() }}\"\n      \"CONFIG_whisk_timeLimit_std\": \"{{ limit_action_time_std | default() }}\"\n\n      \"CONFIG_whisk_concurrencyLimit_min\": \"{{ limit_action_concurrency_min | default() }}\"\n      \"CONFIG_whisk_concurrencyLimit_max\": \"{{ limit_action_concurrency_max | default() }}\"\n      \"CONFIG_whisk_concurrencyLimit_std\": \"{{ limit_action_concurrency_std | default() }}\"\n\n      \"CONFIG_whisk_namespaceDefaultLimit_memory_min\": \"{{ namespace_default_limit_action_memory_min | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_memory_max\": \"{{ namespace_default_limit_action_memory_max | default() }}\"\n\n      \"CONFIG_whisk_namespaceDefaultLimit_timeLimit_min\": \"{{ namespace_default_limit_action_time_min | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_timeLimit_max\": \"{{ namespace_default_limit_action_time_max | default() }}\"\n\n      \"CONFIG_whisk_namespaceDefaultLimit_concurrencyLimit_min\": \"{{ namespace_default_limit_action_concurrency_min | default() }}\"\n      \"CONFIG_whisk_namespaceDefaultLimit_concurrencyLimit_max\": \"{{ namespace_default_limit_action_concurrency_max | default() }}\"\n\n      \"RUNTIMES_MANIFEST\": \"{{ runtimesManifest | to_json }}\"\n      \"CONFIG_whisk_runtimes_defaultImagePrefix\":\n        \"{{ runtimes_default_image_prefix | default() }}\"\n      \"CONFIG_whisk_runtimes_defaultImageTag\":\n        \"{{ runtimes_default_image_tag | default() }}\"\n      \"CONFIG_whisk_runtimes_bypassPullForLocalImages\":\n        \"{{ runtimes_bypass_pull_for_local_images | default() | lower }}\"\n      \"CONFIG_whisk_runtimes_localImagePrefix\":\n        \"{{ runtimes_local_image_prefix | default() }}\"\n\n      \"METRICS_KAMON\": \"{{ metrics.kamon.enabled | default(false) | lower }}\"\n      \"METRICS_KAMON_TAGS\": \"{{ metrics.kamon.tags | default() | lower }}\"\n      \"METRICS_LOG\": \"{{ metrics.log.enabled | default(false) | lower }}\"\n\n      \"CONFIG_kamon_statsd_hostname\": \"{{ metrics.kamon.host }}\"\n      \"CONFIG_kamon_statsd_port\": \"{{ metrics.kamon.port }}\"\n\n      \"CONFIG_whisk_fraction_managedFraction\":\n        \"{{ scheduler.managedFraction }}\"\n      \"CONFIG_whisk_fraction_blackboxFraction\":\n        \"{{ scheduler.blackboxFraction }}\"\n\n      \"CONFIG_logback_log_level\": \"{{ scheduler.loglevel }}\"\n\n      \"CONFIG_whisk_transactions_header\": \"{{ transactions.header }}\"\n\n      \"CONFIG_whisk_etcd_hosts\": \"{{ etcd_connect_string }}\"\n      \"CONFIG_whisk_etcd_lease_timeout\": \"{{ etcd.lease.timeout }}\"\n      \"CONFIG_whisk_etcd_pool_threads\": \"{{ etcd.pool_threads }}\"\n      \"CONFIG_whisk_cluster_name\": \"{{ whisk.cluster_name | lower }}\"\n\n      \"CONFIG_whisk_scheduler_username\": \"{{ scheduler.username }}\"\n      \"CONFIG_whisk_scheduler_password\": \"{{ scheduler.password }}\"\n\n      \"CONFIG_whisk_spi_DurationCheckerProvider\": \"{{ durationChecker.spi }}\"\n\n\n- name: merge extra env variables\n  set_fact:\n    env: \"{{ env | combine(scheduler.extraEnv) }}\"\n\n- name: populate volumes for scheduler\n  set_fact:\n    scheduler_volumes:\n       - \"{{ whisk_logs_dir }}/{{ scheduler_name }}:/logs\"\n       - \"{{ scheduler.confdir }}/{{ scheduler_name }}:/conf\"\n\n- name: setup elasticsearch activation store env\n  set_fact:\n    elastic_env:\n      \"CONFIG_whisk_activationStore_elasticsearch_protocol\": \"{{ db.elasticsearch.protocol}}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_hosts\": \"{{ elasticsearch_connect_string }}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_indexPattern\": \"{{ db.elasticsearch.index_pattern }}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_username\": \"{{ db.elasticsearch.auth.admin.username }}\"\n      \"CONFIG_whisk_activationStore_elasticsearch_password\": \"{{ db.elasticsearch.auth.admin.password }}\"\n      \"CONFIG_whisk_spi_ActivationStoreProvider\": \"org.apache.openwhisk.core.database.elasticsearch.ElasticSearchActivationStoreProvider\"\n      \"CONFIG_whisk_spi_DurationCheckerProvider\": \"org.apache.openwhisk.core.scheduler.queue.ElasticSearchDurationCheckerProvider\"\n  when: db.activation_store.backend == \"ElasticSearch\"\n\n- name: merge elasticsearch activation store env\n  set_fact:\n    env: \"{{ env | combine(elastic_env) }}\"\n  when: db.activation_store.backend == \"ElasticSearch\"\n\n- name: check if coverage collection is enabled\n  set_fact:\n    coverage_enabled: false\n  when: coverage_enabled is undefined\n\n- name: ensure scheduler coverage directory is created with permissions\n  file:\n    path: \"{{ coverage_logs_dir }}/scheduler/{{ item }}\"\n    state: directory\n    mode: 0777\n  with_items:\n    - scheduler\n    - common\n  become: \"{{ logs.dir.become }}\"\n  when: coverage_enabled\n\n- name: extend scheduler volume for coverage\n  set_fact:\n    scheduler_volumes: \"{{ scheduler_volumes|default({}) + [coverage_logs_dir+'/scheduler:/coverage']  }}\"\n  when: coverage_enabled\n\n- name: include plugins\n  include_tasks: \"{{ item }}.yml\"\n  with_items: \"{{ scheduler_plugins | default([]) }}\"\n\n- name: disable scheduler{{ groups['schedulers'].index(inventory_hostname) }} before redeploy scheduler\n  uri:\n    url: \"{{ scheduler.protocol }}://{{ ansible_host }}:{{ scheduler_port }}/disable\"\n    validate_certs: no\n    method: POST\n    status_code: 200\n    user: \"{{ scheduler.username }}\"\n    password: \"{{ scheduler.password }}\"\n    force_basic_auth: yes\n  ignore_errors: \"{{ scheduler.deployment_ignore_error }}\"\n  when: zeroDowntimeDeployment.enabled == true\n\n- name: wait until all activation is finished before redeploy scheduler\n  uri:\n    url: \"{{ scheduler.protocol }}://{{ ansible_host }}:{{ scheduler_port }}/activation/count\"\n    validate_certs: no\n    return_content: yes\n    user: \"{{ scheduler.username }}\"\n    password: \"{{ scheduler.password }}\"\n    force_basic_auth: yes\n  register: result\n  until: result.content == \"0\"\n  retries: 180\n  delay: 5\n  when: zeroDowntimeDeployment.enabled == true\n  ignore_errors: \"{{ scheduler.deployment_ignore_error }}\"\n\n- name: (re)start scheduler\n  docker_container:\n    name: \"{{ scheduler_name }}\"\n    image:\n      \"{{docker_registry~docker.image.prefix}}/scheduler:{{ 'cov' if (coverage_enabled) else docker.image.tag }}\"\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    hostname: \"{{ scheduler_name }}\"\n    env: \"{{ env }}\"\n    volumes: \"{{ scheduler_volumes }}\"\n    ports: \"{{ ports_to_expose }}\"\n    command:\n      /bin/sh -c\n      \"exec /init.sh {{ scheduler_index }}\n      >> /logs/{{ scheduler_name }}_logs.log 2>&1\"\n\n- name: wait until the Scheduler in this host is up and running\n  block:\n    - name: ping scheduler health endpoint\n      uri:\n        url:\n          \"{{scheduler.protocol}}://{{ansible_host}}:{{scheduler_port}}/ping\"\n        validate_certs: \"no\"\n      register: result\n      until: result.status == 200\n      retries: 12\n      delay: 5\n  rescue:\n    - name: dump scheduler docker logs\n      shell: |\n        docker logs {{ scheduler_name }} >/tmp/scheduler-docker.log 2>&1 || true\n        cat /tmp/scheduler-docker.log\n      register: scheduler_logs\n      failed_when: false\n    - name: output scheduler logs for debugging\n      debug:\n        var: scheduler_logs.stdout_lines\n    - name: dump scheduler file logs\n      shell: |\n        cat /var/tmp/wsklogs/{{ scheduler_name }}/{{ scheduler_name }}_logs.log\n      register: scheduler_file_logs\n      failed_when: false\n    - name: output scheduler file logs for debugging\n      debug:\n        var: scheduler_file_logs.stdout_lines\n    - fail:\n        msg: \"Scheduler failed to start; logs emitted above\"\n\n- name: create scheduler jmx.yml\n  template:\n    src: \"{{ openwhisk_home }}/ansible/roles/schedulers/templates/jmx.yml.j2\"\n    dest: \"{{ scheduler.confdir }}/jmx.yml\"\n  ignore_errors: True\n  when: scheduler_index | int + 1 == groups['schedulers'] | length or ansible_host != hostvars[groups['schedulers'][scheduler_index | int + 1 ]]['ansible_host']\n"
  },
  {
    "path": "ansible/roles/schedulers/tasks/join_pekko_cluster.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n#\n#  Scheduler 'plugin' that will add the items necessary to the scheduler\n#  environment to cause the scheduler to join a specified pekko cluster\n#\n\n- name: add pekko port to ports_to_expose\n  set_fact:\n    ports_to_expose: >-\n      {{ ports_to_expose }} +\n      [ \"{{ (scheduler.pekko.cluster.basePort + (scheduler_index | int)) }}:\"\n      + \"{{ scheduler.pekko.cluster.bindPort }}\" ]\n\n- name: add seed nodes to scheduler environment\n  set_fact:\n    env: >-\n      {{ env | combine({\n        'CONFIG_pekko_cluster_seedNodes_' ~ seedNode.0:\n          'pekko://scheduler-actor-system@'~seedNode.1~':'~(scheduler.pekko.cluster.basePort+seedNode.0)\n      }) }}\n  with_indexed_items: \"{{ scheduler.pekko.cluster.seedNodes }}\"\n  loop_control:\n    loop_var: seedNode\n\n- name: Add pekko environment to scheduler environment\n  vars:\n    pekko_env:\n      \"CONFIG_pekko_actor_provider\": \"{{ scheduler.pekko.provider }}\"\n      \"CONFIG_pekko_remote_artery_canonical_hostname\":\n        \"{{ scheduler.pekko.cluster.host[(scheduler_index | int)] }}\"\n      \"CONFIG_pekko_remote_artery_canonical_port\":\n        \"{{ scheduler.pekko.cluster.basePort + (scheduler_index | int) }}\"\n      \"CONFIG_pekko_remote_artery_bind_hostname\": \"0.0.0.0\"\n      \"CONFIG_pekko_remote_artery_bind_port\":\n        \"{{ scheduler.pekko.cluster.bindPort }}\"\n  set_fact:\n    env: \"{{ env | combine(pekko_env) }}\"\n"
  },
  {
    "path": "ansible/roles/schedulers/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install scheduler in group 'schedulers' in the environment inventory\n# In deploy mode it will deploy schedulers.\n# In clean mode it will remove the scheduler containers.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/roles/schedulers/templates/jmx.yml.j2",
    "content": "collects:\n{% set index = groups['schedulers'].index(inventory_hostname) %}\n{% set ip = hostvars[groups['schedulers'][groups['schedulers'].index(inventory_hostname) | int]]['ansible_host'] %}\n{% for i in range(0,index+1)|reverse if hostvars[groups['schedulers'][i]]['ansible_host'] == ip %}\n  - {{ hostvars[groups['schedulers'][i]]['inventory_hostname'] }}\n{% endfor %}\n\nrules:\n{% for i in range(0,index+1)|reverse if hostvars[groups['schedulers'][i]]['ansible_host'] == ip %}\n  - name: {{ hostvars[groups['schedulers'][i]]['inventory_hostname'] }}\n    metrics:\n    - kafka.producer:type=producer-metrics,client-id=* request-latency-avg,request-latency-max,request-rate,response-rate,incoming-byte-rate,outgoing-byte-rate,connection-count,connection-creation-rate,connection-close-rate,io-ratio,io-time-ns-avg,io-wait-ratio,select-rate,io-wait-time-ns-avg client-id\n    - kafka.producer:type=producer-node-metrics,client-id=*,node-id=* request-rate,response-rate,request-latency-max,request-latency-avg,incoming-byte-rate,request-size-avg,outgoing-byte-rate,request-size-max client-id\n    - kafka.producer:type=producer-topic-metrics,client-id=*,topic=* record-retry-rate,record-send-rate,compression-rate,byte-rate,record-error-rate client-id\n    - kafka.consumer:type=consumer-metrics,client-id=* connection-creation-rate,response-rate,select-rate,connection-count,network-io-rate,io-ratio,io-wait-time-ns-avg,io-wait-ratio,outgoing-byte-rate,request-size-max,io-time-ns-avg,request-rate,incoming-byte-rate,connection-close-rate,request-size-avg client-id\n    - kafka.consumer:type=consumer-fetch-manager-metrics,client-id=*,topic=* bytes-consumed-rate,records-consumed-rate,fetch-size-max,fetch-size-avg,records-per-request-avg client-id\n    - kafka.consumer:type=consumer-node-metrics,client-id=*,node-id=* request-rate,response-rate,request-latency-max,request-latency-avg,incoming-byte-rate,request-size-avg,outgoing-byte-rate,request-size-max client-id\n    - kafka.consumer:type=consumer-coordinator-metrics,client-id=* join-time-max,commit-latency-avg,sync-time-avg,join-rate,assigned-partitions,sync-rate,commit-rate,last-heartbeat-seconds-ago,heartbeat-rate,commit-latency-max,join-time-avg,sync-time-max,heartbeat-response-time-max client-id\n    jvmPrefix: kafka.jvm\n    jmxUrl: \"service:jmx:rmi:///jndi/rmi://{{ ip }}:{{ jmx.basePortScheduler + i }}/jmxrmi\"\n    jmxUsername: \"{{ jmx.user }}\"\n    jmxPassword: \"{{ jmx.pass }}\"\n\n{% endfor %}\nintervalSec: 10\n"
  },
  {
    "path": "ansible/roles/zookeeper/tasks/clean.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Remove kafka and zookeeper containers.\n\n- name: remove old zookeeper\n  docker_container:\n    name: zookeeper\n    image: \"{{ docker_registry }}{{ docker.image.prefix }}/zookeeper:{{ docker.image.tag }}\"\n    keep_volumes: False\n    state: absent\n  ignore_errors: True\n\n- name: remove zookeeper\n  docker_container:\n    name: zookeeper{{ groups['zookeepers'].index(inventory_hostname) }}\n    image: \"{{ docker_registry }}{{ docker.image.prefix }}/zookeeper:{{ docker.image.tag }}\"\n    keep_volumes: False\n    state: absent\n  ignore_errors: True\n"
  },
  {
    "path": "ansible/roles/zookeeper/tasks/deploy.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install Kafka with Zookeeper in group 'kafka' in the environment inventory\n\n- name: set zookeeper image\n  set_fact:\n    zookeeper_image: \"{{ zookeeper.image.arm64 }}\"\n  when: ansible_facts['os_family'] == \"Darwin\" and ansible_facts['architecture'] == \"arm64\"\n\n- name: set zookeeper image\n  set_fact:\n    zookeeper_image: \"{{ zookeeper.image.amd64 }}\"\n  when: ansible_facts['os_family'] != \"Darwin\" or ansible_facts['architecture'] != \"arm64\"\n\n- name: (re)start zookeeper\n  docker_container:\n    name: zookeeper{{ groups['zookeepers'].index(inventory_hostname) }}\n    image: \"{{ zookeeper_image }}\"\n    state: started\n    recreate: true\n    restart_policy: \"{{ docker.restart.policy }}\"\n    env:\n        TZ: \"{{ docker.timezone }}\"\n        ZOO_MY_ID: \"{{ groups['zookeepers'].index(inventory_hostname) + 1 }}\"\n        ZOO_SERVERS: \"{% set zhosts = [] %}\n                      {% for host in groups['zookeepers'] %}\n                        {% if host == inventory_hostname %}\n                          {{ zhosts.append('server.' + (loop.index|string) + '=' + '0.0.0.0:2888:3888') }}\n                        {% else %}\n                          {{ zhosts.append('server.' + (loop.index|string) + '=' + hostvars[host].ansible_host + ':' + ((2888+loop.index-1)|string) + ':' + ((3888+loop.index-1)|string) ) }}\n                        {% endif %}\n                      {% endfor %}\n                      {{ zhosts | join(' ') }}\"\n    ports:\n      - \"{{ zookeeper.port + groups['zookeepers'].index(inventory_hostname) }}:2181\"\n      - \"{{ 2888 + groups['zookeepers'].index(inventory_hostname) }}:2888\"\n      - \"{{ 3888 + groups['zookeepers'].index(inventory_hostname) }}:3888\"\n    pull: \"{{ zookeeper.pull_zookeeper | default(true) }}\"\n\n- name: wait until the Zookeeper in this host is up and running\n  action: shell (echo ruok; sleep 1) | nc {{ ansible_host }} {{ zookeeper.port + groups['zookeepers'].index(inventory_hostname) }}\n  register: result\n  until: (result.rc == 0) and (result.stdout == 'imok')\n  retries: 36\n  delay: 5\n"
  },
  {
    "path": "ansible/roles/zookeeper/tasks/main.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This role will install kafka in group 'kafka' in the environment inventory\n# In deploy mode it will deploy kafka including zookeeper.\n# In clean mode it will remove kafka and zookeeper containers.\n\n- import_tasks: deploy.yml\n  when: mode == \"deploy\"\n\n- import_tasks: clean.yml\n  when: mode == \"clean\"\n"
  },
  {
    "path": "ansible/routemgmt.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys the Openwhisk API Gateway route management actions.\n\n- hosts: ansible\n  roles:\n  - routemgmt\n"
  },
  {
    "path": "ansible/scheduler.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook deploys Openwhisk Controllers.\n\n- hosts: schedulers\n  vars:\n    #\n    # host_group - usually \"{{ groups['...'] }}\" where '...' is what was used\n    #   for 'hosts' above.  The hostname of each host will be looked up in this\n    #   group to assign a zero-based index.  That index will be used in concert\n    #   with 'name_prefix' below to assign a host/container name.\n    host_group: \"{{ groups['schedulers'] }}\"\n    #\n    # name_prefix - a unique prefix for this set of controllers.  The prefix\n    #   will be used in combination with an index (determined using\n    #   'host_group' above) to name host/controllers.\n    name_prefix: \"scheduler\"\n    #\n    # controller_index_base - the deployment process allocates host docker\n    #   ports to individual controllers based on their indices.  This is an\n    #   additional offset to prevent collisions between different controller\n    #   groups. Usually 0 if only one group is being deployed, otherwise\n    #   something like \"{{ groups['firstcontrollergroup']|length }}\"\n    scheduler_index_base: 0\n    #\n    # select which additional capabilities (from the controller role) need\n    #   to be added to the controller.  Plugin will override default\n    #   configuration settings.  (Plugins are found in the\n    #   'roles/controller/tasks' directory for now.)\n    scheduler_plugins:\n      # Join an pekko cluster rather than running standalone pekko\n      - \"join_pekko_cluster\"\n\n  serial: '1'\n  roles:\n    - schedulers\n"
  },
  {
    "path": "ansible/setup.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook prepares ansible configuration\n\n- hosts: localhost\n  tasks:\n  # Generate hosts files\n  - name: gen hosts if 'local' env is used\n    local_action: template src=\"{{playbook_dir}}/environments/local/hosts.j2.ini\" dest=\"{{ playbook_dir }}/environments/local/hosts\"\n    when: \"'environments/local' in hosts_dir\"\n\n  - name: find the ip of docker-machine\n    local_action: shell \"docker-machine\" \"ip\" \"{{docker_machine_name | default('whisk')}}\"\n    register: result\n    when: \"'environments/docker-machine' in hosts_dir\"\n\n  - name: get the docker-machine ip\n    set_fact:\n      docker_machine_ip: \"{{ result.stdout }}\"\n    when: \"'environments/docker-machine' in hosts_dir\"\n\n  - name: gen hosts for docker-machine\n    local_action: template src=\"{{playbook_dir}}/environments/docker-machine/hosts.j2.ini\" dest=\"{{ playbook_dir }}/environments/docker-machine/hosts\"\n    when: \"'environments/docker-machine' in hosts_dir\"\n\n  - name: gen hosts for Jenkins\n    local_action: template src=\"{{playbook_dir}}/environments/jenkins/hosts.j2.ini\" dest=\"{{ playbook_dir }}/environments/jenkins/hosts\"\n    when: \"'environments/jenkins' in hosts_dir\"\n\n  - name: Refresh inventory to ensure generated hosts files are used\n    meta: refresh_inventory\n\n  # Generate db_local.ini\n  - name: check if db_local.ini exists?\n    tags: ini\n    stat: path=\"{{ playbook_dir }}/db_local.ini\"\n    register: db_check\n\n  - name: prepare db_local.ini\n    tags: ini\n    local_action: template src=\"db_local.ini.j2\" dest=\"{{ playbook_dir }}/db_local.ini\"\n    when: not db_check.stat.exists\n\n  # Generate nginx certificates\n  - name: gen untrusted server certificate for host\n    local_action: shell \"{{ playbook_dir }}/files/genssl.sh\" \"*.{{ whisk_api_localhost_name | default(whisk_api_host_name) | default(whisk_api_localhost_name_default) }}\" \"server\" \"{{ nginx.ssl.path }}\"\n    when: nginx.ssl.cert == \"openwhisk-server-cert.pem\"\n\n  - name: gen untrusted client certificate for host\n    local_action: shell \"{{ playbook_dir }}/files/genssl.sh\" \"*.{{ whisk_api_localhost_name | default(whisk_api_host_name) | default(whisk_api_localhost_name_default) }}\" \"client\" \"{{ nginx.ssl.path }}\"\n    when: nginx.ssl.client_ca_cert == \"openwhisk-client-ca-cert.pem\"\n\n  # Generate Kafka certificates\n  - name: clean up old kafka keystore\n    file:\n      path: \"{{ playbook_dir }}/roles/kafka/files\"\n      state: absent\n    become: \"{{ logs.dir.become }}\"\n    when: kafka_protocol_for_setup == 'SSL'\n\n  - name: ensure kafka files directory exists\n    file:\n      path: \"{{ playbook_dir }}/roles/kafka/files/\"\n      state: directory\n      mode: 0777\n    become: \"{{ logs.dir.become }}\"\n    when: kafka_protocol_for_setup == 'SSL'\n\n  - name: generate kafka certificates\n    local_action: shell \"{{ playbook_dir }}/files/genssl.sh\" \"openwhisk-kafka\" \"server_with_JKS_keystore\" \"{{ playbook_dir }}/roles/kafka/files\" openwhisk \"kafka-\" \"generateKey\"\n    when: kafka_protocol_for_setup == 'SSL'\n\n  # Generate Controller certificates\n  - name: ensure controller files directory exists\n    file:\n      path: \"{{ playbook_dir }}/roles/controller/files/\"\n      state: directory\n      mode: 0777\n    become: \"{{ logs.dir.become }}\"\n    when: controller.protocol == 'https'\n\n  - name: generate controller certificates\n    when: controller.protocol == 'https'\n    local_action: shell \"{{ playbook_dir }}/files/genssl.sh\" \"{{ controller.ssl.cn }}\" \"server\" \"{{ playbook_dir }}/roles/controller/files\" {{ controller.ssl.keystore.password }} {{ controller.ssl.keyPrefix }} \"generateKey\"\n\n  # Generate Invoker certificates\n  - name: ensure invoker files directory exists\n    file:\n      path: \"{{ playbook_dir }}/roles/invoker/files/\"\n      state: directory\n      mode: 0777\n    become: \"{{ logs.dir.become }}\"\n    when: invoker.protocol == 'https'\n\n  - name: generate invoker certificates\n    when: invoker.protocol == 'https'\n    local_action: shell \"{{ playbook_dir }}/files/genssl.sh\" \"{{ invoker.ssl.cn }}\" \"server\" \"{{ playbook_dir }}/roles/invoker/files\" {{ invoker.ssl.keystore.password }} {{ invoker.ssl.keyPrefix }} \"generateKey\"\n"
  },
  {
    "path": "ansible/tasks/db/checkDb.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Checks, that the Database exists\n# dbName - name of the database to check\n# dbUser - name of the user which should have access rights\n# dbPass - password of the user which should have access\n\n- name: check if {{ dbName }} with {{ db.provider }} exists\n  uri:\n    url: \"{{ db.protocol }}://{{ db.host }}:{{ db.port }}/{{ dbName }}\"\n    method: HEAD\n    status_code: 200\n    user: \"{{ dbUser }}\"\n    password: \"{{ dbPass }}\"\n    force_basic_auth: yes\n  when: db.artifact_store.backend == \"CouchDB\"\n\n# the collection in MongoDB doesn't need to be created in advance, so just skip it\n# - name: check if {{ dbName }} on MongoDB exists\n"
  },
  {
    "path": "ansible/tasks/db/createUsers.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Create all required users in _users-database\n# http://docs.couchdb.org/en/2.0.0/intro/security.html#users-documents\n\n- name: create _users DB if it doesn't exist yet\n  uri:\n    url: \"{{ db.protocol }}://{{ db.host }}:{{ db.port }}/_users\"\n    method: PUT\n    status_code: 200,201,412\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n\n- name: create required users\n  uri:\n    url: \"{{ db.protocol }}://{{ db.host }}:{{ db.port }}/_users/org.couchdb.user:{{ item.value.user }}\"\n    method: PUT\n    status_code: 201,409\n    body_format: json\n    body: |\n      {\n        \"name\": \"{{ item.value.user }}\",\n        \"password\": \"{{ item.value.pass }}\",\n        \"roles\": [],\n        \"type\": \"user\"\n      }\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  with_dict: \"{{ db.credentials }}\"\n  # Don't create the admin user again, if a component is using admin access.\n  when: item.value.user != db.credentials.admin.user\n"
  },
  {
    "path": "ansible/tasks/db/grantPermissions.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Grant the specified users permissions to the specified database.\n# dbName - name of the database\n# dbHostname - hostname of the database\n# dbAdminUser - admin user, which is able to grant permissions\n# dbAdminPassword - password of the admin user, which is able to grant permissions\n# admins - all users which should have admin access on this database afterwards\n# readers - all users which should have read access on this database afterwards\n# writers - all users which should have write access on this database afterwards\n\n- set_fact:\n    dbUser: \"{{ dbAdminUser | default(db.credentials.admin.user) }}\"\n    dbPassword: \"{{ dbAdminPassword | default(db.credentials.admin.pass) }}\"\n    dbHost: \"{{ dbHostname | default(db.host) }}\"\n\n# If a component uses admin credentials, the admin user will not be added to the list (as it already has all access rights).\n- set_fact:\n    readerList: \"{{ readers | default([]) | difference([dbUser]) }}\"\n    writerList: \"{{ writers | default([]) | difference([dbUser]) }}\"\n    adminList: \"{{ admins | default([]) | difference([dbUser]) }}\"\n\n# http://docs.couchdb.org/en/2.0.0/api/database/security.html\n- name: grant permissions for CouchDB\n  uri:\n    url: \"{{ db.protocol }}://{{ dbHost }}:{{ db.port }}/{{ dbName }}/_security\"\n    method: PUT\n    status_code: 200\n    body_format: json\n    body: |\n      {\n        \"admins\": {\n          \"names\": [ \"{{ adminList | join('\", \"') }}\" ],\n          \"roles\": []\n        },\n        \"members\": {\n          \"names\": [ \"{{ readerList | union(writerList) | join('\", \"') }}\" ],\n          \"roles\": []\n        }\n      }\n    user: \"{{ dbUser }}\"\n    password: \"{{ dbPassword }}\"\n    force_basic_auth: yes\n  when: db.provider == 'CouchDB'\n\n# https://cloud.ibm.com/docs/services/Cloudant/api/authorization.html#authorization\n- name: grant permissions for Cloudant\n  uri:\n    url: \"{{ db.protocol }}://{{ dbHost }}:{{ db.port }}/{{ dbName }}/_security\"\n    method: PUT\n    status_code: 200\n    body_format: json\n    body: |\n      {\n        \"cloudant\": {\n          {% for item in readerList | union(writerList) | union(adminList) %}\"{{ item }}\": [ {% if item in readerList %}\"_reader\"{% if item in writerList %}, \"_writer\"{% if item in adminList %}, \"_admin\"{% endif %}{% endif %}{% endif %} ], {% endfor %}\n        }\n      }\n    user: \"{{ dbUser }}\"\n    password: \"{{ dbPassword }}\"\n    force_basic_auth: yes\n  when: db.provider == 'Cloudant'\n"
  },
  {
    "path": "ansible/tasks/db/recreateDb.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# (re))create the specified database.\n# dbName - name of the database to (re)create\n# forceRecreation - if true, the databases will be deleted (if it exists) and recreated. If false, it will not be recreated.\n\n- name: check if {{ dbName }} with {{ db.provider }} exists\n  uri:\n    url: \"{{ db.protocol }}://{{ db.host }}:{{ db.port }}/{{ dbName }}\"\n    method: HEAD\n    status_code: 200,404\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  register: response\n\n- name: delete the {{ dbName }} with {{ db.provider }}\n  uri:\n    url: \"{{ db.protocol }}://{{ db.host }}:{{ db.port }}/{{ dbName }}\"\n    method: DELETE\n    status_code: 200,404\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  when: forceRecreation == True and response.status == 200\n\n- name: create {{ dbName }} with {{ db.provider }}\n  uri:\n    url: \"{{ db.protocol }}://{{ db.host }}:{{ db.port }}/{{ dbName }}\"\n    method: PUT\n    status_code: 200,201\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  when: forceRecreation == True or response.status == 404\n"
  },
  {
    "path": "ansible/tasks/db/recreateDoc.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Recreates a document in a database.\n# dbName - name of the database, where the view should be stored\n# doc - the new document for the db\n\n- set_fact:\n    create: False\n    docWithRev: {}\n    document: \"{{ doc }}\"\n\n# fetches the revision of previous view (to update it) if it exists\n- name: check for {{ doc['_id'] }} document in {{ dbName }} database\n  uri:\n    url: \"{{ db.protocol }}://{{ db.host }}:{{ db.port }}/{{ dbName }}/{{ doc['_id'] }}\"\n    return_content: yes\n    method: GET\n    status_code: 200, 404\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  register: response\n\n- set_fact:\n    existingDoc: \"{{ response['content']|from_json }}\"\n  when: response.status == 200\n\n- name: extract revision from previous document\n  vars:\n    revision: \"{{ existingDoc['_rev'] }}\"\n  set_fact:\n    docWithRev: \"{{ doc | combine({'_rev': revision}) }}\"\n  when: response.status == 200\n\n- name: check if a doc update is required\n  set_fact:\n    create: True\n  when: (response.status == 200 and existingDoc != docWithRev) or response.status == 404 # Create doc, if it did not exist before or if update is required.\n\n- set_fact:\n    document: \"{{ docWithRev }}\"\n  when: docWithRev['_id'] is defined and create == True\n\n- name: recreate or update the document on the {{ dbName }} database\n  uri:\n    url: \"{{ db.protocol }}://{{ db.host }}:{{ db.port }}/{{ dbName }}\"\n    method: POST\n    status_code: 200, 201\n    body_format: json\n    body: \"{{ document }}\"\n    user: \"{{ db.credentials.admin.user }}\"\n    password: \"{{ db.credentials.admin.pass }}\"\n    force_basic_auth: yes\n  when: create == True\n"
  },
  {
    "path": "ansible/tasks/docker_login.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n\n# Logs in to private registry if needed.\n\n- name: docker login\n  docker_login:\n    registry: \"{{ docker_registry }}\"\n    username: \"{{ docker_registry_username }}\"\n    password: \"{{ docker_registry_password }}\"\n  when: docker_registry != \"\" and docker_registry_password is defined\n"
  },
  {
    "path": "ansible/tasks/gen_erl_cookie.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n\n# generate erlang cookie for CouchDB\n\n- name: \"generate erlang cookie\"\n  local_action: command openssl rand -base64 32\n  register: random_stdout\n  run_once: true\n  when: erlang_cookie is not defined\n\n- set_fact:\n    erlang_cookie: \"{{ random_stdout.stdout }}\"\n  when: erlang_cookie is not defined\n\n- name: \"ensure config root dir exists\"\n  file:\n    path: \"{{ config_root_dir }}\"\n    state: directory\n\n# when enable uid namespace mode, couchdb container doesn't have permission to change the owner of files which mounted\n# from host, so the container will be failed to start. Use a temporary container here to create the file erlang.cookie\n# with the correct user 'couchdb' as its owner\n- name: \"create the erlang cookie file on remote\"\n  vars:\n    couchdb_image: \"{{ couchdb.docker_image | default('apache/couchdb:' ~ couchdb.version ) }}\"\n  command: \"docker run --rm -v /tmp:/tmp -u couchdb {{ couchdb_image }} sh -c 'echo {{ erlang_cookie }} >> /tmp/erlang.cookie'\"\n  become: true\n\n- name: \"move erlang.cookie from /tmp to {{ config_root_dir }}\"\n  shell: \"chmod 400 /tmp/erlang.cookie && mv /tmp/erlang.cookie {{ config_root_dir }}/erlang.cookie\"\n  args:\n    warn: false\n  become: true\n"
  },
  {
    "path": "ansible/tasks/initdb.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This task will initialize the immortal DBs in the database account.\n# This step is usually done only once per deployment.\n\n- include_tasks: db/recreateDb.yml\n  vars:\n    dbName: \"{{ db.whisk.auth }}\"\n    forceRecreation: False\n\n- include_tasks: db/grantPermissions.yml\n  vars:\n    dbName: \"{{ db.whisk.auth }}\"\n    readers:\n    - \"{{ db.credentials.controller.user }}\"\n    - \"{{ db.credentials.invoker.user }}\"\n    - \"{{ db.credentials.scheduler.user }}\"\n\n- include_tasks: db/recreateDoc.yml\n  vars:\n    dbName: \"{{ db.whisk.auth }}\"\n    doc: \"{{ lookup('file', '{{ item }}') }}\"\n  with_items:\n    - \"{{ openwhisk_home }}/ansible/files/auth_design_document_for_subjects_db_v2.0.0.json\"\n    - \"{{ openwhisk_home }}/ansible/files/filter_design_document.json\"\n    - \"{{ openwhisk_home }}/ansible/files/namespace_throttlings_design_document_for_subjects_db.json\"\n\n- name: create necessary \"auth\" keys\n  include_tasks: db/recreateDoc.yml\n  vars:\n    key: \"{{ lookup('file', 'files/auth.{{ item }}') }}\"\n    dbName: \"{{ db.whisk.auth }}\"\n    doc: >\n      {\n        \"_id\": \"{{ item }}\",\n        \"subject\": \"{{ item }}\",\n        \"namespaces\": [\n        {% if 'extraNamespaces' in db and item in db.extraNamespaces %}\n          {% for ns in db.extraNamespaces[item] %}\n          {\n            \"name\": \"{{ item }}{{ ns.postfix }}\",\n            \"uuid\": \"{{ ns.uuid }}\",\n            \"key\": \"{{ ns.key }}\"\n          },\n          {% endfor %}\n        {% endif %}\n          {\n            \"name\": \"{{ item }}\",\n            \"uuid\": \"{{ key.split(\":\")[0] }}\",\n            \"key\": \"{{ key.split(\":\")[1] }}\"\n          }]\n      }\n  with_items: \"{{ db.authkeys }}\"\n"
  },
  {
    "path": "ansible/tasks/installOpenwhiskCatalog.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This task will install the standard actions and packages available in openwhisk-catalog repos.\n\n- set_fact:\n    catalog_location={{ item.value.location }}\n    catalog_repo_url={{ item.value.url }}\n    api_host={{ whisk_api_host_name | default(groups['edge'] | first) }}\n    version=\"HEAD\"\n    repo_update=\"yes\"\n\n- set_fact:\n    version={{ item.value.version }}\n  when: item.value.version is defined\n\n- set_fact:\n    repo_update={{ item.value.repo_update }}\n  when: item.value.repo_update is defined\n\n- set_fact:\n    skip_catalog_install=\"{{ item.value.skip | default(false) }}\"\n\n- set_fact:\n    environment_catalog:\n      OPENWHISK_HOME: \"{{ openwhisk_home }}\"\n\n- set_fact:\n    environment_catalog: \"{{ environment_catalog | combine( { the_item.key: the_item.value } ) }}\"\n  when: item.value.environment is defined\n  with_dict: \"{{ item.value.environment }}\"\n  loop_control:\n    loop_var: the_item\n\n- name: \"ensure catalog_location directory exists\"\n  file:\n    path: \"{{ catalog_location }}\"\n    state: directory\n\n- name: download the catalog repository to the catalog location if necessary\n  git:\n    repo: \"{{ catalog_repo_url }}\"\n    dest: \"{{ catalog_location }}\"\n    update: \"{{ repo_update }}\"\n    version: \"{{ version }}\"\n\n- name: install the catalog from the catalog location\n  shell: ./installCatalogUsingWskdeploy.sh {{ catalog_auth_key }} {{ api_host }} {{ cli.path }} chdir=\"{{ catalog_location }}/packages\"\n  environment: \"{{ environment_catalog }}\"\n  when: skip_catalog_install == false\n"
  },
  {
    "path": "ansible/tasks/recreateViews.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Recreates all views in all databases.\n\n- include_tasks: db/recreateDoc.yml\n  vars:\n    dbName: \"{{ db.whisk.actions }}\"\n    doc: \"{{ lookup('file', '{{ item }}') }}\"\n  with_items:\n    - \"{{ openwhisk_home }}/ansible/files/whisks_design_document_for_entities_db_v2.1.0.json\"\n    - \"{{ openwhisk_home }}/ansible/files/filter_design_document.json\"\n\n- include_tasks: db/recreateDoc.yml\n  vars:\n    dbName: \"{{ db.whisk.activations }}\"\n    doc: \"{{ lookup('file', '{{ item }}') }}\"\n  with_items:\n    - \"{{ openwhisk_home }}/ansible/files/whisks_design_document_for_activations_db_v2.1.0.json\"\n    - \"{{ openwhisk_home }}/ansible/files/whisks_design_document_for_activations_db_filters_v2.1.1.json\"\n    - \"{{ openwhisk_home }}/ansible/files/filter_design_document.json\"\n    - \"{{ openwhisk_home }}/ansible/files/activations_design_document_for_activations_db.json\"\n    - \"{{ openwhisk_home }}/ansible/files/logCleanup_design_document_for_activations_db.json\"\n"
  },
  {
    "path": "ansible/tasks/wipeDatabase.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# Wipe transient databases. You should know what you are doing here.\n# withViews: True or False. Says, if the views have to be recreated.\n\n- include_tasks: db/createUsers.yml\n\n- include_tasks: db/recreateDb.yml\n  vars:\n    dbName: \"{{ db.whisk.actions }}\"\n    forceRecreation: True\n- include_tasks: db/grantPermissions.yml\n  vars:\n    dbName: \"{{ db.whisk.actions }}\"\n    readers:\n    - \"{{ db.credentials.controller.user }}\"\n    - \"{{ db.credentials.invoker.user }}\"\n    - \"{{ db.credentials.scheduler.user }}\"\n    writers:\n    - \"{{ db.credentials.controller.user }}\"\n\n- include_tasks: db/recreateDb.yml\n  vars:\n    dbName: \"{{ db.whisk.activations }}\"\n    forceRecreation: True\n- include_tasks: db/grantPermissions.yml\n  vars:\n    dbName: \"{{ db.whisk.activations }}\"\n    readers:\n    - \"{{ db.credentials.controller.user }}\"\n    - \"{{ db.credentials.invoker.user }}\"\n    writers:\n    - \"{{ db.credentials.controller.user }}\"\n    - \"{{ db.credentials.invoker.user }}\"\n    - \"{{ db.credentials.scheduler.user }}\"\n\n- include_tasks: recreateViews.yml\n  when: withViews == True\n"
  },
  {
    "path": "ansible/tasks/writeWhiskProperties.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This task will write whisk.properties to the openwhisk_home.\n# Currently whisk.properties is still needed for tests.\n\n- name: write whisk.properties template to openwhisk_home\n  template:\n    src: whisk.properties.j2\n    dest: \"{{ openwhisk_home }}/whisk.properties\"\n\n- name: check if application.conf.j2 exists\n  tags: application\n  stat: path=\"{{ openwhisk_home }}/tests/src/test/resources/application.conf.j2\"\n  register: application\n\n- name: write test's application conf overrides\n  tags: application\n  template:\n    src: \"{{ openwhisk_home }}/tests/src/test/resources/application.conf.j2\"\n    dest: \"{{ openwhisk_home }}/tests/src/test/resources/application.conf\"\n  when: application.stat.exists\n\n- name: write whisk.conf template for wskadmin to openwhisk_home\n  template:\n    src: whisk.conf.j2\n    dest: \"{{ openwhisk_home }}/whisk.conf\"\n"
  },
  {
    "path": "ansible/teardown.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# This playbook cleans all docker containers\n\n- hosts: all:!ansible\n  tasks:\n    - name: kill all docker containers\n      shell: \"{{ item }}\"\n      with_items:\n      - \"RUNNING=$(docker ps -aq); if [ -n \\\"$RUNNING\\\" ]; then docker unpause $RUNNING > /dev/null; docker kill $RUNNING > /dev/null; docker rm -f -v $(docker ps -aq); fi\"\n      - \"DANGLING=$(docker images -q -f dangling=true); if [ -n \\\"$DANGLING\\\" ]; then docker rmi -f $DANGLING; fi\"\n"
  },
  {
    "path": "ansible/templates/db_local.ini.j2",
    "content": "[db_creds]\ndb_provider={{ lookup('env', 'OW_DB')|default('CouchDB', true) }}\ndb_username={{ lookup('env', 'OW_DB_USERNAME')|default('whisk_admin', true) }}\ndb_password={{ lookup('env', 'OW_DB_PASSWORD')|default('some_passw0rd', true) }}\ndb_protocol={{ lookup('env', 'OW_DB_PROTOCOL')|default('http', true) }}\ndb_host={{ lookup('env', 'OW_DB_HOST')|default(groups['db']|first, true) }}\ndb_port={{ lookup('env', 'OW_DB_PORT')|default('5984', true) }}\n\n[controller]\ndb_username={{ lookup('env', 'OW_DB_CONTROLLER_USERNAME') | default(db_prefix + 'controller0', true) }}\ndb_password={{ lookup('env', 'OW_DB_CONTROLLER_PASSWORD') | default('some_controller_passw0rd', true) }}\n\n[invoker]\ndb_username={{ lookup('env', 'OW_DB_INVOKER_USERNAME') | default(db_prefix + 'invoker0', true) }}\ndb_password={{ lookup('env', 'OW_DB_INVOKER_PASSWORD') | default('some_invoker_passw0rd', true) }}\n\n[scheduler]\ndb_username={{ lookup('env', 'OW_DB_SCHEDULER_USERNAME') | default(db_prefix + 'scheduler0', true) }}\ndb_password={{ lookup('env', 'OW_DB_SCHEDULER_PASSWORD') | default('some_scheduler_passw0rd', true) }}\n"
  },
  {
    "path": "ansible/templates/jmxremote.access.j2",
    "content": "{{ jmx.user }} readwrite\n"
  },
  {
    "path": "ansible/templates/jmxremote.password.j2",
    "content": "{{ jmx.user }} {{ jmx.pass }}\n"
  },
  {
    "path": "ansible/templates/whisk.conf.j2",
    "content": "include classpath(\"application.conf\")\n\nwhisk {\n  couchdb {\n    protocol = \"{{ db.protocol }}\"\n    host     = \"{{ db.host }}\"\n    port     = \"{{ db.port }}\"\n    username = \"{{ db.credentials.admin.user }}\"\n    password = \"{{ db.credentials.admin.pass }}\"\n    provider = \"{{ db.provider }}\"\n    databases {\n      WhiskAuth       = \"{{ db.whisk.auth }}\"\n      WhiskEntity     = \"{{ db.whisk.actions }}\"\n      WhiskActivation = \"{{ db.whisk.activations }}\"\n    }\n  }\n  {% if db.artifact_store.backend == 'MongoDB' %}\n  mongodb {\n    uri         = \"{{ db.mongodb.connect_string }}\"\n    database    = \"{{ db.mongodb.database }}\"\n  }\n\n  spi {\n    ArtifactStoreProvider = org.apache.openwhisk.core.database.mongodb.MongoDBArtifactStoreProvider\n  }\n  {% endif %}\n}\n"
  },
  {
    "path": "ansible/templates/whisk.properties.j2",
    "content": "openwhisk.home={{ openwhisk_home }}\n\npython.27=python\nnginx.conf.dir={{ nginx.confdir }}\ntesting.auth={{ openwhisk_home }}/ansible/files/auth.guest\nvcap.services.file=\n\nwhisk.logs.dir={{ whisk_logs_dir }}\nwhisk.coverage.logs.dir={{ coverage_logs_dir | default('') }}\nenvironment.type={{ environmentInformation.type }}\nwhisk.ssl.client.verification={{ nginx.ssl.verify_client }}\nwhisk.ssl.cert={{ nginx.ssl.path }}/{{ nginx.ssl.cert }}\nwhisk.ssl.key={{ nginx.ssl.path }}/{{ nginx.ssl.key }}\nwhisk.ssl.challenge=openwhisk\n\n{#\n # the whisk.api.host.name must be a name that can resolve form inside an action container,\n # or an ip address reachable from inside the action container.\n #\n # the whisk.api.localhost.name must be a name that resolves from the client; it is either the\n # whisk_api_host_name if it is defined, an environment specific localhost name, or the default\n # localhost name.\n #\n # the whisk.api.vanity.subdomain.parts indicates how many conforming parts the router is configured to\n # match in the subdomain, which it rewrites into a namespace; each part must match ([a-zA-Z0-9]+)\n # with parts separated by a single dash.\n #}\nwhisk.api.host.proto={{ whisk_api_host_proto | default('https') }}\nwhisk.api.host.port={{ whisk_api_host_port | default('443') }}\nwhisk.api.host.name={{ whisk_api_host_name | default(groups['edge'] | first) }}\nwhisk.api.localhost.name={{ whisk_api_localhost_name | default(whisk_api_host_name) | default(whisk_api_localhost_name_default) }}\nwhisk.api.vanity.subdomain.parts=1\n\nwhisk.action.concurrency={{ runtimes_enable_concurrency | default(false) }}\nwhisk.feature.requireApiKeyAnnotation={{ whisk.feature_flags.require_api_key_annotation | default(true) }}\nwhisk.feature.requireResponsePayload={{ whisk.feature_flags.require_response_payload | default(true) }}\n\nruntimes.manifest={{ runtimesManifest | to_json }}\n\nlimits.actions.invokes.perMinute={{ limits.invocationsPerMinute }}\nlimits.actions.invokes.concurrent={{ limits.concurrentInvocations }}\nlimits.triggers.fires.perMinute={{ limits.firesPerMinute }}\nlimits.actions.sequence.maxLength={{ limits.sequenceMaxLength }}\n\nedge.host={{ groups[\"edge\"]|first }}\nkafka.hosts={{ kafka_connect_string }}\nredis.host={{ groups[\"redis\"]|default([\"\"])|first }}\nrouter.host={{ groups[\"edge\"]|first }}\nzookeeper.hosts={{ zookeeper_connect_string }}\ninvoker.hosts={{ groups[\"invokers\"] | map('extract', hostvars, 'ansible_host') | list | join(\",\") }}\n\nedge.host.apiport=443\nkafkaras.host.port={{ kafka.ras.port }}\nredis.host.port={{ redis.port }}\ninvoker.hosts.basePort={{ invoker.port }}\ninvoker.username={{ invoker.username }}\ninvoker.password={{ invoker.password }}\n\ncontroller.hosts={{ groups[\"controllers\"] | map('extract', hostvars, 'ansible_host') | list | join(\",\") }}\ncontroller.host.basePort={{ controller.basePort }}\ncontroller.instances={{ controller.instances }}\ncontroller.protocol={{ controller.protocol }}\ncontroller.username={{ controller.username }}\ncontroller.password={{ controller.password }}\n\ninvoker.container.network=bridge\ninvoker.container.policy={{ invoker_container_policy_name | default()}}\ninvoker.container.dns={{ invoker_container_network_dns_servers | default()}}\ninvoker.useRunc={{ invoker.useRunc }}\n\nmain.docker.endpoint={{ hostvars[groups[\"controllers\"]|first].ansible_host }}:{{ docker.port }}\n\ndocker.registry={{ docker_registry }}\ndocker.image.prefix={{ docker.image.prefix }}\n#use.docker.registry=false\ndocker.port={{ docker.port }}\ndocker.timezone.mount=\ndocker.image.tag={{ docker.image.tag }}\ndocker.tls.cmd=\ndocker.addHost.cmd=\ndocker.dns.cmd={{ docker_dns }}\ndocker.restart.opts={{ docker.restart.policy }}\n\ndb.provider={{ db.provider }}\ndb.protocol={{ db.protocol }}\ndb.host={{ db.host }}\ndb.port={{ db.port }}\ndb.username={{ db.credentials.admin.user }}\ndb.password={{ db.credentials.admin.pass }}\ndb.prefix={{ db_prefix }}\ndb.whisk.auths={{ db.whisk.auth }}\ndb.whisk.actions={{ db.whisk.actions }}\ndb.whisk.activations={{ db.whisk.activations }}\ndb.hostsList={{ groups[\"db\"] | map('extract', hostvars, 'ansible_host') | list | join(\",\") }}\ndb.instances={{ db.instances }}\n\napigw.auth.user={{apigw_auth_user}}\napigw.auth.pwd={{apigw_auth_pwd}}\napigw.host.v2={{apigw_host_v2}}\n"
  },
  {
    "path": "ansible/wipe.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\n# WARNING: This playbook wipes the database. This action is not reversible. Be very careful and know what you are doing.\n\n- hosts: ansible\n  tasks:\n    - import_tasks: tasks/wipeDatabase.yml\n      vars:\n        withViews: True\n      when: mode == \"deploy\"\n"
  },
  {
    "path": "ansible/yamllint.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nextends: default\n\nrules:\n  #  Needs to be more than 80 because of the standard\n  #  apache header overflowing in a couple places\n  line-length:\n    max: 90\n"
  },
  {
    "path": "build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nbuildscript {\n    repositories {\n        gradlePluginPortal()\n        mavenCentral()\n    }\n    dependencies {\n        classpath \"gradle.plugin.cz.alenkacz:gradle-scalafmt:${gradle.scalafmt.version}\"\n    }\n}\n\nplugins {\n    id \"org.scoverage\" version \"7.0.0\" apply false\n    id \"cz.alenkacz.gradle.scalafmt\" version \"1.16.2\" apply false\n}\n\nsubprojects {\n    apply plugin: 'scalafmt'\n    scalafmt.configFilePath = gradle.scalafmt.config\n\n    group 'org.apache.openwhisk'\n    version '1.0.1-SNAPSHOT'\n\n    pluginManager.withPlugin('scala') {\n        // Constraint all transitive pekko-* dependencies to the one we want to use to avoid issues.\n        def cons = project.getDependencies().getConstraints()\n        def pekko = ['pekko-actor', 'pekko-actor-typed', 'pekko-cluster', 'pekko-cluster-metrics', 'pekko-cluster-tools', 'pekko-coordination',\n                    'pekko-discovery', 'discovery-kubernetes-api', 'discovery-marathon-api', 'pekko-distributed-data','grpc-runtime','pekko-protobuf', 'pekko-remote', 'pekko-slf4j',\n                    'pekko-stream', 'pekko-stream-testkit', 'pekko-testkit', 'pekko-persistence', 'pekko-cluster-sharding','pekko-protobuf-v3','pekko-pki','pekko-serialization-jackson']\n        def pekkoHttp = ['pekko-http', 'pekko-http-core', 'pekko-http-spray-json', 'pekko-http-testkit', 'pekko-http-xml',\n                        'pekko-http2-support']\n        def pekkoKafka = ['pekko-stream-kafka-testkit','pekko-connectors-kafka','pekko-connectors-s3','pekko-connectors-file']\n        def pekkoManagement = ['pekko-management-cluster-bootstrap','pekko-management','pekko-discovery-kubernetes-api','pekko-discovery-marathon-api']\n\n        pekko.forEach {\n            cons.add('implementation', \"org.apache.pekko:${it}_${gradle.scala.depVersion}:${gradle.pekko.version}\")\n        }\n        pekkoHttp.forEach {\n            cons.add('implementation', \"org.apache.pekko:${it}_${gradle.scala.depVersion}:${gradle.pekko_http.version}\")\n        }\n        pekkoKafka.forEach{\n            cons.add('implementation', \"org.apache.pekko:${it}_${gradle.scala.depVersion}:${gradle.pekko_kafka.version}\")\n        }\n        pekkoManagement.forEach{\n            cons.add('implementation', \"org.apache.pekko:${it}_${gradle.scala.depVersion}:${gradle.pekko_management.version}\")\n        }\n    }\n\n    afterEvaluate {\n        if (project.plugins.hasPlugin('scala')) {\n            repositories {\n                mavenCentral()\n            }\n\n            tasks.withType(ScalaCompile) {\n                scalaCompileOptions.additionalParameters = gradle.scala.compileFlags\n                scalaCompileOptions.forkOptions.jvmArgs = [\"-Xss2m\"]\n            }\n\n            if (project.plugins.hasPlugin('application')) {\n                startScripts {\n                    doLast {\n                        unixScript.text = configureUnixClasspath(unixScript)\n                    }\n                }\n            }\n\n            configurations {\n                implementationResolvable {\n                    canBeResolved = true\n                    canBeConsumed = false\n                    extendsFrom configurations.implementation\n                }\n            }\n        }\n\n        if (project.plugins.hasPlugin('maven-publish')) {\n            task sourcesJar(type: Jar, dependsOn: classes) {\n                classifier = 'sources'\n                from sourceSets.main.allSource\n            }\n\n            task testSourcesJar(type: Jar, dependsOn: testClasses) {\n                classifier = 'test-sources'\n                from sourceSets.test.allSource\n                exclude(\"logback-test.xml\")\n            }\n\n            task testClassesJar(type: Jar, dependsOn: testClasses) {\n                classifier = 'tests'\n                from sourceSets.test.output\n            }\n        }\n\n        if (project.plugins.hasPlugin('application')) {\n            //Ensure that dist archive name does not contain version\n            distTar {\n                archiveFileName = \"${project.name}.tar\"\n            }\n\n            //Avoid generating the zip files from maven installations\n            distZip {\n                enabled false\n            }\n\n            configurations.archives.artifacts.removeAll {it.file =~ 'zip'}\n        }\n    }\n}\n\ndef configureUnixClasspath(File script) {\n    script\n        .readLines()\n        .collect { line ->\n            // Looking for the line that starts with CLASSPATH=\n            line = line.replaceAll(~/^CLASSPATH=.*$/) { original ->\n\n                // Get original line and append it\n                // with the configuration directory.\n                original += ':$APP_HOME/ext-lib/*:$APP_HOME/config'\n                //Ensure classes comes first. Used to refer to instrumented classes for code coverage\n                original = original.replace('CLASSPATH=', 'CLASSPATH=$APP_HOME/classes:')\n            }\n        }\n        .join('\\n')\n}\n"
  },
  {
    "path": "common/scala/.dockerignore",
    "content": "*\n!transformEnvironment.sh\n!copyJMXFiles.sh\n!build/distributions"
  },
  {
    "path": "common/scala/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# if you change version of openjdk, also update tools/github/setup.sh to download the corresponding jdk\n# NOTE:\n# OpenWhisk will use a 21-jre multi arch image, compilation will be done with a jdk 17 temurin based image.\n# as wsk CLI is compiled against glibc we need touse a GLIBC based JRE Image (alpine it is not GLIBC based)\nFROM eclipse-temurin:21-jre\n\nENV LANG=en_US.UTF-8\nENV LANGUAGE=en_US:en\nENV LC_ALL=en_US.UTF-8\n\n# Install curl, bash, sed\nRUN apt-get update && \\\n    apt-get install -y curl bash sed openssl && \\\n    apt-get clean && \\\n    rm -rf /var/lib/apt/lists/*\n\nRUN mkdir /logs\n\nCOPY transformEnvironment.sh /\nRUN chmod +x transformEnvironment.sh\n\nCOPY copyJMXFiles.sh /\nRUN chmod +x copyJMXFiles.sh\n"
  },
  {
    "path": "common/scala/Dockerfile-debian",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nFROM adoptopenjdk/openjdk11-openj9:x86_64-debian-jdk-11.0.8_10_openj9-0.21.0-slim\n\nENV LANG en_US.UTF-8\nENV LANGUAGE en_US:en\nENV LC_ALL en_US.UTF-8\n\n#use cloudfront + https for packages\nRUN echo 'deb https://cloudfront.debian.net/debian/ stable main\\n\\\ndeb https://cloudfront.debian.net/debian-security/ stable/updates main'\\\n> /etc/apt/sources.list\n\nRUN apt-get -y install --no-install-recommends \\\n      sed curl bash && apt-get -y update && apt-get -y upgrade\n\nRUN mkdir /logs\n\nCOPY transformEnvironment.sh /\nRUN chmod +x transformEnvironment.sh\n\nCOPY copyJMXFiles.sh /\nRUN chmod +x copyJMXFiles.sh\n"
  },
  {
    "path": "common/scala/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'eclipse'\n    id 'maven-publish'\n    id 'org.scoverage'\n    id 'scala'\n    id 'java-library'\n}\n\next.dockerImageName = 'scala'\n\n// Using a multiarch base image should make this supefluous\n//if(System.getProperty(\"os.arch\").toLowerCase(Locale.ENGLISH).startsWith(\"aarch\")) {\n//    ext.dockerDockerfileSuffix = \".arm\"\n//}\n\napply from: '../../gradle/docker.gradle'\n\nproject.archivesBaseName = \"openwhisk-common\"\n\nscoverage {\n    scoverageVersion.set(\"${gradle.scala.scoverageVersion}\")\n    scoverageScalaVersion.set(\"${gradle.scala.scoverageScalaVersion}\")\n}\n\ndependencies {\n    api \"org.scala-lang:scala-library:${gradle.scala.version}\"\n\n    api (\"com.github.pureconfig:pureconfig_${gradle.scala.depVersion}:0.11.1\") {\n        exclude group: 'org.scala-lang', module: 'scala-compiler'\n        exclude group: 'org.scala-lang', module: 'scala-reflect'\n    }\n    api \"io.spray:spray-json_${gradle.scala.depVersion}:1.3.5\"\n    api \"com.lihaoyi:fastparse_${gradle.scala.depVersion}:2.3.0\"\n    api \"org.apache.pekko:pekko-actor_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    api \"org.apache.pekko:pekko-actor-typed_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    api \"org.apache.pekko:pekko-cluster-typed_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    api \"org.apache.pekko:pekko-stream_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    api \"org.apache.pekko:pekko-slf4j_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    api \"org.apache.pekko:pekko-cluster_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    api \"org.apache.pekko:pekko-cluster-metrics_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    api \"org.apache.pekko:pekko-cluster-tools_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    api \"org.apache.pekko:pekko-distributed-data_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n\n    api \"org.apache.pekko:pekko-http-core_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n    api \"org.apache.pekko:pekko-http-spray-json_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n\n    api \"org.apache.pekko:pekko-connectors-file_${gradle.scala.depVersion}:${gradle.pekko_kafka.version}\"\n\n    api \"ch.qos.logback:logback-classic:1.3.15\"\n    api \"org.slf4j:slf4j-api:2.0.9\"\n    api \"org.slf4j:jcl-over-slf4j:2.0.17\"\n    api \"org.slf4j:log4j-over-slf4j:2.0.17\"\n    api \"commons-codec:commons-codec:1.9\"\n    api \"commons-io:commons-io:2.14.0\"\n    api \"commons-collections:commons-collections:3.2.2\"\n    api \"org.apache.kafka:kafka-clients:3.9.1\"\n    api \"org.apache.httpcomponents:httpclient:4.5.5\"\n    api \"com.fasterxml.uuid:java-uuid-generator:3.1.3\"\n    api \"com.github.ben-manes.caffeine:caffeine:2.6.2\"\n    api \"com.google.code.findbugs:jsr305:3.0.2\"\n    api \"io.fabric8:kubernetes-client:${gradle.kube_client.version}\"\n\n    //metrics\n    api (\"io.kamon:kamon-core_${gradle.scala.depVersion}:2.1.12\") {\n        exclude group: 'com.lihaoyi'\n    }\n    api \"io.kamon:kamon-statsd_${gradle.scala.depVersion}:2.1.12\"\n    api (\"io.kamon:kamon-system-metrics_${gradle.scala.depVersion}:2.1.12\") {\n        exclude group: 'io.kamon', module: 'sigar-loader'\n    }\n    api \"io.kamon:kamon-prometheus_${gradle.scala.depVersion}:2.1.12\"\n    api \"io.kamon:kamon-datadog_${gradle.scala.depVersion}:2.1.12\"\n\n    // for etcd - aligned with Pekko gRPC\n    api \"com.ibm.etcd:etcd-java:0.0.24\"\n\n    //tracing support\n    api \"io.opentracing:opentracing-api:0.31.0\"\n    api \"io.opentracing:opentracing-util:0.31.0\"\n    api (\"io.opentracing.brave:brave-opentracing:0.31.0\") {\n        exclude group: 'io.zipkin.brave', module:'brave-tests'\n    }\n    api \"io.zipkin.reporter2:zipkin-sender-okhttp3:2.6.1\"\n    api \"io.zipkin.reporter2:zipkin-reporter:2.6.1\"\n\n    api \"io.reactivex:rxjava:1.3.8\"\n    api \"io.reactivex:rxjava-reactive-streams:1.2.1\"\n\n\n    api (\"org.apache.pekko:pekko-connectors-s3_${gradle.scala.depVersion}:${gradle.pekko_kafka.version}\") {\n        exclude group: 'org.apache.httpcomponents' //Not used as pekko-connectors uses pekko-http\n        exclude group: 'com.fasterxml.jackson.core'\n        exclude group: 'com.fasterxml.jackson.dataformat'\n    }\n    api \"com.amazonaws:aws-java-sdk-cloudfront:1.12.792\" // Upgraded to remove ion-java dependency (CVE-2024-21634)\n\n    api (\"com.azure:azure-storage-blob:12.18.0\") {\n        exclude group: \"com.azure\", module: \"azure-core-test\"\n    }\n\n    api \"com.microsoft.azure:azure-cosmosdb:2.6.2\"\n    constraints {\n        api(\"com.microsoft.azure:azure-cosmosdb:2.6.2\")\n        api(\"com.fasterxml.jackson.core:jackson-core:2.15.4\") {\n            because \"cannot upgrade azure-cosmosdb to new major version to remediate vulns w/o breaking change\"\n        }\n    }\n\n    api \"com.sksamuel.elastic4s:elastic4s-http_${gradle.scala.depVersion}:6.7.8\"\n    constraints {\n        api(\"com.sksamuel.elastic4s:elastic4s-http_${gradle.scala.depVersion}:6.7.8\")\n        api(\"org.elasticsearch.client:elasticsearch-rest-client:6.8.23\") {\n            because \"cannot upgrade elastic4s to remediate vuln without performing major version rest client upgrade\"\n        }\n    }\n    //for mongo\n    api \"org.mongodb.scala:mongo-scala-driver_${gradle.scala.depVersion}:2.7.0\"\n    constraints {\n        api(\"org.mongodb.scala:mongo-scala-driver_${gradle.scala.depVersion}:2.7.0\")\n        api(\"org.mongodb:mongodb-driver-async:3.12.1\") {\n            because \"cannot upgrade major mongo scala driver to remediate vuln w/o code changes\"\n        }\n    }\n\n    api \"io.netty:netty-buffer:${gradle.netty.version}\"\n    api \"io.netty:netty-handler:${gradle.netty.version}\"\n    api \"io.netty:netty-handler-proxy:${gradle.netty.version}\"\n    api \"io.netty:netty-codec-socks:${gradle.netty.version}\"\n    api \"io.netty:netty-codec-http:${gradle.netty.version}\"\n    api \"io.netty:netty-codec-http2:${gradle.netty.version}\"\n    api \"io.netty:netty-transport-native-epoll:${gradle.netty.version}\"\n    api \"io.netty:netty-transport-native-unix-common:${gradle.netty.version}\"\n\n    api \"org.apache.pekko:pekko-grpc-runtime_${gradle.scala.depVersion}:${gradle.pekko_grpc.version}\"\n    api \"org.apache.pekko:pekko-stream_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n\n    // Constraints for transitive dependencies to address security vulnerabilities\n    constraints {\n        api(\"com.squareup.okhttp3:okhttp:4.9.2\")\n        api(\"com.squareup.okhttp3:mockwebserver:4.9.2\")\n        api(\"com.squareup.okio:okio:3.9.1\")\n\n        api(\"org.apache.commons:commons-lang3:3.18.0\")\n\n        api(\"io.projectreactor.netty:reactor-netty-core:1.2.8\")\n        api(\"io.projectreactor.netty:reactor-netty-http:1.2.8\")\n\n        api(\"io.grpc:grpc-api:${gradle.grpc.version}\") {\n            version { strictly gradle.grpc.version }\n            because \"Align all gRPC artifacts with Pekko gRPC runtime ${gradle.pekko_grpc.version}\"\n        }\n        api(\"io.grpc:grpc-core:${gradle.grpc.version}\") {\n            version { strictly gradle.grpc.version }\n            because \"Align all gRPC artifacts with Pekko gRPC runtime ${gradle.pekko_grpc.version}\"\n        }\n        api(\"io.grpc:grpc-netty:${gradle.grpc.version}\") {\n            version { strictly gradle.grpc.version }\n            because \"Align all gRPC artifacts with Pekko gRPC runtime ${gradle.pekko_grpc.version}\"\n        }\n        api(\"io.grpc:grpc-netty-shaded:${gradle.grpc.version}\") {\n            version { strictly gradle.grpc.version }\n            because \"Align all gRPC artifacts with Pekko gRPC runtime ${gradle.pekko_grpc.version}\"\n        }\n        api(\"io.grpc:grpc-protobuf:${gradle.grpc.version}\") {\n            version { strictly gradle.grpc.version }\n            because \"Align all gRPC artifacts with Pekko gRPC runtime ${gradle.pekko_grpc.version}\"\n        }\n        api(\"io.grpc:grpc-protobuf-lite:${gradle.grpc.version}\") {\n            version { strictly gradle.grpc.version }\n            because \"Align all gRPC artifacts with Pekko gRPC runtime ${gradle.pekko_grpc.version}\"\n        }\n        api(\"io.grpc:grpc-stub:${gradle.grpc.version}\") {\n            version { strictly gradle.grpc.version }\n            because \"Align all gRPC artifacts with Pekko gRPC runtime ${gradle.pekko_grpc.version}\"\n        }\n        api(\"org.apache.commons:commons-text:1.10.0\")\n        api(\"com.google.code.gson:gson:2.8.9\")\n\n        api(\"com.google.protobuf:protobuf-java:3.25.5\")\n        api(\"org.xerial.snappy:snappy-java:1.1.10.4\")\n        api(\"ch.qos.logback:logback-core:1.2.13\")\n        api(\"io.netty:netty-codec:${gradle.netty.version}\")\n        api(\"io.netty:netty-common:${gradle.netty.version}\")\n    }\n}\n\nconfigurations {\n    api {\n        exclude group: 'commons-logging'\n        exclude group: 'log4j'\n    }\n    all {\n        resolutionStrategy.dependencySubstitution {\n            // CVE-2025-12183, CVE-2025-66566: org.lz4:lz4-java relocated to at.yawk.lz4 transitive dependency of kafka-clients\n            substitute module('org.lz4:lz4-java') using module('at.yawk.lz4:lz4-java:1.10.3')\n        }\n    }\n}\n"
  },
  {
    "path": "common/scala/copyJMXFiles.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nif [[ $( ls /conf/jmxremote.* 2> /dev/null ) ]]\nthen\n  # JMX auth files would be mounted as a symbolic link (read-only mode)\n  # with `root` privileges by the k8s secret.\n  cp -rL /conf/jmxremote.* /home/owuser\n  rm -f /conf/jmxremote.* 2>/dev/null || true\n\n  # The owner must be `owuser` and the file only have read permission.\n  chmod 600 /home/owuser/jmxremote.*\nfi\n"
  },
  {
    "path": "common/scala/src/main/resources/application.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# default application configuration file for pekko\ninclude \"logging\"\n\npekko {\n    java-flight-recorder.enabled = false\n}\n\npekko.http {\n    client {\n        parsing.illegal-header-warnings = off\n        parsing.max-chunk-size = 50m\n        parsing.max-content-length = 50m\n    }\n    parsing {\n        max-to-strict-bytes = 50m\n    }\n\n    host-connection-pool {\n        max-connections = 128\n        max-open-requests = 1024\n    }\n\n    server {\n        preview.enable-http2 = on\n        parsing.illegal-header-warnings = off\n    }\n}\n\n#kamon related configuration\nkamon {\n    modules {\n        # Only statsd is enabled by default.\n        statsd-reporter {\n            enabled = true\n        }\n        datadog-agent {\n            enabled = false\n        }\n        datadog-trace-agent {\n            enabled = false\n        }\n        # This should never be set to true as we register Prometheus reporters manually and surface them via pekko-http.\n        prometheus-reporter {\n            enabled = false\n        }\n\n        host-metrics {\n            enabled = false\n        }\n    }\n\n    environment {\n      # Identifier for this service. For keeping it backward compatible setting to natch previous\n      # statsd name\n      service = \"openwhisk-statsd\"\n    }\n    metric {\n        tick-interval = 1 second\n    }\n\n    statsd {\n        # Interval between metrics data flushes to StatsD. It's value must be equal or greater than the\n        # kamon.metrics.tick-interval setting.\n        flush-interval = 1 second\n\n        # Max packet size for UDP metrics data sent to StatsD.\n        max-packet-size = 1024 bytes\n\n        # Subscription patterns used to select which metrics will be pushed to StatsD. Note that first, metrics\n        # collection for your desired entities must be activated under the kamon.metrics.filters settings.\n        includes {\n            actor      =  [ \"*\" ]\n            trace      =  [ \"*\" ]\n            dispatcher =  [ \"*\" ]\n        }\n\n        metric-key-generator = org.apache.openwhisk.common.WhiskStatsDMetricKeyGenerator\n    }\n    prometheus {\n        # We expose the metrics endpoint over pekko http. So default server is disabled\n        start-embedded-http-server = no\n\n        buckets {\n            custom {\n                //By default retry are configured upto 9. However for certain setups we may increase\n                //it to higher values\n                \"histogram.cosmosdb_retry_success\" = [1, 2, 3, 5, 7, 10, 12, 15, 20]\n            }\n        }\n    }\n}\n\nwhisk {\n    shared-packages-execute-only = false\n    metrics {\n        # Enable/disable Prometheus support. If enabled then metrics would be exposed at `/metrics` endpoint\n        # If Prometheus is enabled then please review `kamon.metric.tick-interval` (set to 1 sec by default above).\n        # It can then be set to scrape interval value which is generally 60 secs\n        prometheus-enabled = false\n\n        # Enable/disable whether metric information is sent to the configured reporters.\n        kamon-enabled      = false\n        kamon-enabled      = ${?METRICS_KAMON}\n\n        # Enable/disable whether to use the Kamon tags when sending metrics.\n        kamon-tags-enabled = false\n        kamon-tags-enabled = ${?METRICS_KAMON_TAGS}\n\n        # Enable/disable whether the metric information is written out to the log files in logmarker format.\n        logs-enabled       = true\n        logs-enabled       = ${?METRICS_LOG}\n    }\n\n    # kafka related configuration, the complete list of parameters is here:\n    # https://kafka.apache.org/documentation/#brokerconfigs\n    kafka {\n        replication-factor = 1\n\n        // Used to control the cadence of the consumer lag check interval\n        consumer-lag-check-interval = 60 seconds\n\n        // The following settings are passed \"raw\" to the respective Kafka client. Dashes are replaced by dots.\n        common {\n            security-protocol = PLAINTEXT\n            ssl-endpoint-identification-algorithm = \"\" // restores pre-kafka 2.0.0 default\n\n            //Enable this for reporting Kafka client metrics\n            //metric-reporters = \"org.apache.openwhisk.connector.kafka.KamonMetricsReporter\"\n\n        }\n        producer {\n            acks = 1\n            request-timeout-ms = 30000\n            metadata-max-age-ms = 15000\n            # max-request-size is defined programmatically for producers related to the \"completed\" and \"invoker\" topics\n            # as ${whisk.activation.kafka.payload.max} + ${whisk.activation.kafka.serdes-overhead}. All other topics use\n            # the default of 1 MB.\n        }\n        consumer {\n            session-timeout-ms = 30000\n            heartbeat-interval-ms = 10000\n            enable-auto-commit = false\n            auto-offset-reset = earliest\n            request-timeout-ms = 30000\n\n            max-poll-interval-ms = 1800000 // 30 minutes\n\n            // The maximum amount of time the server will block before answering\n            // the fetch request if there isn't sufficient data to immediately\n            // satisfy the requirement given by fetch.min.bytes.\n            // (default is 500, default of fetch.min.bytes is 1)\n            // On changing fetch.min.bytes, a high value for fetch.max.wait.ms,\n            // could increase latency of activations.\n            // A low value will cause excessive busy-waiting.\n            fetch-max-wait-ms = 500\n        }\n\n        topics {\n            cache-invalidation {\n                segment-bytes   =  536870912\n                retention-bytes = 1073741824\n                retention-ms    = 300000\n            }\n            completed {\n                segment-bytes   =  536870912\n                retention-bytes = 1073741824\n                retention-ms    = 3600000\n                # max-message-bytes is defined programmatically as ${whisk.activation.kafka.payload.max} +\n                # ${whisk.activation.kafka.serdes-overhead}.\n            }\n            creationAck {\n                segment-bytes   =  536870912\n                retention-bytes = 1073741824\n                retention-ms    = 3600000\n                # max-message-bytes is defined programmatically as ${whisk.activation.kafka.payload.max} +\n                # ${whisk.activation.kafka.serdes-overhead}.\n            }\n            health {\n                segment-bytes   =  536870912\n                retention-bytes = 1073741824\n                retention-ms    = 3600000\n            }\n            invoker {\n                segment-bytes     =  536870912\n                retention-bytes   = 1073741824\n                retention-ms      =  172800000\n                # max-message-bytes is defined programmatically as ${whisk.activation.kafka.payload.max} +\n                # ${whisk.activation.kafka.serdes-overhead}.\n            }\n            events {\n                segment-bytes   =  536870912\n                retention-bytes = 1073741824\n                retention-ms    = 3600000\n            }\n            scheduler {\n                segment-bytes     =  536870912\n                retention-bytes   = 1073741824\n                retention-ms      =  86400000\n            }\n            prefix = \"\"\n            user-event {\n                prefix = \"\"\n            }\n        }\n\n        metrics {\n            // Name of metrics which should be tracked via Kamon\n            // https://docs.confluent.io/current/kafka/monitoring.html\n            names = [\n                // consumer-fetch-manager-metrics\n                \"records-lag-max\", // The maximum lag in terms of number of records for any partition in this window\n                \"records-consumed-total\", // The total number of records consumed\n\n                //producer-topic-metrics\n                \"record-send-total\",\n                \"byte-total\",\n\n                //producer-metrics\n                \"request-total\",\n                \"request-size-avg\"\n            ]\n\n            report-interval = 10 seconds\n        }\n    }\n    # db related configuration\n    db {\n        subjects-ddoc = \"subjects.v2.0.0\"\n        actions-ddoc = \"whisks.v2.1.0\"\n        activations-ddoc = \"whisks.v2.1.0\"\n        activations-filter-ddoc = \"whisks-filters.v2.1.1\"\n\n        # Size limit for inlined attachments. Attachments having size less than this would\n        # be inlined with there content encoded in attachmentName\n        max-inline-size = 16 k\n    }\n\n    # CouchDB related configuration\n    # For example:\n    # couchdb {\n    #     protocol = http          # One of \"https\" or \"http\"\n    #     host     = servername    # DB Host\n    #     port     = 5984          # DB Port\n    #     username =\n    #     password =\n    #     provider =               # Either \"Cloudant\" or \"CouchDB\"\n    #     databases {              # Database names used for various entity classes\n    #        WhiskAuth       =\n    #        WhiskEntity     =\n    #        WhiskActivation =\n    #     }\n    #}\n\n    # CosmosDB related configuration\n    # For example:\n    cosmosdb {\n        # endpoint          =               # Endpoint URL like https://<account>.documents.azure.com:443/\n        # key               =               # Access key\n        # db                =               # Database name\n        # Throughput configured for each collection within this db\n        # This is configured only if collection is created fresh. If collection\n        # already exists then existing throughput would be used\n        throughput        = 1000\n        # Select from one of the supported\n        # https://azure.github.io/azure-cosmosdb-java/1.0.0/com/microsoft/azure/cosmosdb/ConsistencyLevel.html\n        consistency-level = \"Session\"\n\n        # TTL duration. By default no TTL is set unless explicitly configured\n        # ENABLING THIS VALUE MEANS YOUR DATA WILL NOT BE PERMANENTLY STORED\n        # Can only be used for `WhiskActivation` for now\n        # time-to-live      = 60 s\n\n        # Specifies the current clusterId whose value is recorded with document upon any update\n        # to indicate which cluster made the change. By default no such value is recorded\n        # cluster-id        =\n\n        # Enables soft delete mode where by the document would not be actually deleted. Instead\n        # it would be marked deleted by setting `_deleted` property to true and then actual delete\n        # happens via TTL.\n        # soft-delete-ttl   = 10 h\n\n        # Frequency at which collection resource usage info like collection size, document count etc is recorded\n        # and exposed as metrics. If any reindexing is in progress then its progress would be logged with this frequency\n        record-usage-frequency = 10 m\n\n        # Flag to enable collection of retry stats. This feature works by registering with Logback to intercept\n        # log messages and based on that collect stats\n        retry-stats-enabled = true\n\n        connection-policy {\n            max-pool-size = 1000\n            # When the value of this property is true, the SDK will direct write operations to\n            # available writable locations of geo-replicated database account\n            using-multiple-write-locations = false\n\n            # Select from one of the supported connection mode\n            # https://github.com/Azure/azure-cosmosdb-java/blob/master/commons/src/main/java/com/microsoft/azure/cosmosdb/ConnectionMode.java\n            connection-mode = \"Gateway\"\n\n            # Sets the preferred locations for geo-replicated database accounts e.g. \"East US\"\n            # See names at https://azure.microsoft.com/en-in/global-infrastructure/locations/\n            preferred-locations = []\n            retry-options {\n                # Sets the maximum number of retries in the case where the request fails\n                # because the service has applied rate limiting on the client.\n\n                # If this value is changed then adjust the buckets under `kamon.prometehus`\n                max-retry-attempts-on-throttled-requests = 9\n\n                # Sets the maximum retry time\n                # If the cumulative wait time exceeds this SDK will stop retrying and return the\n                # error to the application.\n                max-retry-wait-time                      = 30 s\n            }\n        }\n\n        # Specify entity specific overrides below. By default all config values would be picked from top level. To override\n        # any config option for specific entity specify them below. Following names can be used\n        #   - WhiskAuth\n        #   - WhiskEntity\n        #   - WhiskActivation\n        # For example if multiple writes need to be enabled for activations then\n        # collections {\n        #   WhiskActivation {            # Add entity specific overrides here\n        #     connection-policy {\n        #        using-multiple-write-locations = true\n        #     }\n        #   }\n        # }\n    }\n\n    # ActivationStore related configuration\n    # For example:\n    # activation-store {\n    #   elasticsearch {\n    #      protocol         =       # \"http\" or \"https\"\n    #      hosts            =       # the hosts address of ES, can be multi hosts combined with commas, like \"172.17.0.1:9200,172.17.0.2:9200,172.17.0.3:9200\"\n    #      index-pattern    =       # the index pattern used to tell which index an activation should be stored to, will be calculated with activation namespace,\n    #                                 for example, if the index-pattern is \"openwhisk-%s\", then activations under \"whisk.system\" will be saved in to index\n    #                                 \"openwhisk-whisk.system\", you can also save all namespaces activations into one index by set index-pattern to a raw string\n    #                                 like \"openwhisk\"\n    #      username         =       # username of the provide ES\n    #      password         =       # password of the provide ES\n    #   }\n    # }\n\n    activation-store {\n        retry-config {\n            max-tries = 3\n        }\n        elasticsearch {\n            keep-alive = 13 minutes\n        }\n    }\n\n    azure-blob {\n        # Config property when using AzureBlobAttachmentStore\n        # whisk {\n        #  spi {\n        #    AttachmentStoreProvider = org.apache.openwhisk.core.database.azblob.AzureBlobAttachmentStoreProvider\n        #  }\n        #}\n\n        # Blob container endpoint like https://foostore.blob.core.windows.net/test-ow-travis\n        # It is of format https://<account-name>.blob.core.windows.net/<container-name>\n        # endpoint =\n\n        # Storage account name\n        # account-name =\n\n        # Container name within storage account used to store the blobs\n        # container-name =\n\n        # Shared key credentials\n        # https://github.com/Azure/azure-sdk-for-java/tree/master/sdk/storage/azure-storage-blob#shared-key-credential\n        # account-key\n\n        # Folder path within the container (optional)\n        # prefix\n\n        retry-config {\n            retry-policy-type = FIXED\n            max-tries = 3\n            try-timeout = 5 seconds\n            retry-delay = 10 milliseconds\n            #secondary-host = \"\"\n        }\n        #azure-cdn-config {\n        #    domain-name = \"<your azure cdn domain>\"\n        #}\n    }\n\n    # MongoDB related configuration\n    # For example:\n    # mongodb {\n    #    uri         = mongodb://localhost:27017  # DB Uri\n    #    database    =             # Database name\n    #}\n\n    # transaction ID related configuration\n    transactions {\n        header = \"X-Request-ID\"\n    }\n    # action runtimes configuration\n    runtimes {\n        bypass-pull-for-local-images = false\n        local-image-prefix = \"whisk\"\n    }\n\n    # cluster name related etcd configuration\n    cluster {\n        name = \"whisk\"\n    }\n\n    user-events {\n        enabled = false\n    }\n\n    activation {\n        payload {\n            max = 1 m\n            truncation = 1 m\n        }\n        # Action responses sent through Kafka can contain up to 3018 bytes of metadata\n        #   CompletionMessage\n        #       base                            71\n        #       TransactionId\n        #           id                          32\n        #           start                       13\n        #           extraLogging                5\n        #       WhiskActivation\n        #           base                        368\n        #           activationId                32\n        #           subject                     256\n        #           namespace                   256\n        #           entity name                 256\n        #           path                        3x256+2=770\n        #           initTime                    64\n        #           waitTime                    64\n        #           duration                    64\n        #           kind                        64\n        #           limits                      64*3=192\n        #           version                     64x3+2=194\n        #           size                        16\n        #       InvokerInstanceId\n        #           instance                    64\n        #           uniqueName + displayName    253 (max pod name length in Kube)\n        serdes-overhead = 6068 // 3034 bytes of metadata * 2 for extra headroom\n\n        # DEPRECATED, use store-blocking-result-level\n        # Disables database store for blocking + successful activations\n        # invocations made with `X-OW-EXTRA-LOGGING: on` header, will force the activation to be stored\n        disable-store-result = false\n\n        # Result level to store in db for blocking activations (STORE_ALWAYS, STORE_FAILURES, STORE_FAILURES_NOT_APPLICATION_ERRORS)\n        # invocations made with `X-OW-EXTRA-LOGGING: on` header, will force the activation to be stored\n        store-blocking-result-level = \"STORE_ALWAYS\"\n\n        # Result level to store in db for non-blocking activations (STORE_ALWAYS, STORE_FAILURES, STORE_FAILURES_NOT_APPLICATION_ERRORS)\n        # invocations made with `X-OW-EXTRA-LOGGING: on` header, will force the activation to be stored\n        store-non-blocking-result-level = \"STORE_ALWAYS\"\n\n        # Enable metadata logging of activations not stored in the database\n        unstored-logs-enabled = false\n    }\n\n    # action timelimit configuration\n    time-limit {\n        min = 100 ms\n        max = 5 m\n        std = 1 m\n    }\n\n    # action memory configuration\n    memory {\n        min = 128 m\n        max = 512 m\n        std = 256 m\n    }\n\n    # action log-limit configuration\n    log-limit {\n        min = 0 m\n        max = 10 m\n        std = 10 m\n    }\n\n    # action concurrency-limit configuration\n    concurrency-limit {\n        min = 1\n        max = 1\n        std = 1\n    }\n\n    # maximum size of the action parameter\n    parameter-size-limit = 1 m\n\n    # maximum size of the action code\n    exec-size-limit = 48 m\n\n    query-limit {\n        max-list-limit     = 200  # max number of entities that can be requested from a collection on a list operation\n        default-list-limit = 30   # default limit on number of entities returned from a collection on a list operation\n    }\n\n    # default namespace limit settings\n    # Disabled for backwards compatibility. If you want to use it, either uncomment it or add the setting at deployment time.\n    # namespace-default-limit {\n    #     memory {\n    #         min = 128 m\n    #         max = 512 m\n    #     }\n    #     time-limit {\n    #         min = 100 ms\n    #         max = 5 m\n    #     }\n    #     log-limit {\n    #         min = 0 m\n    #         max = 10 m\n    #     }\n    #     concurrency-limit {\n    #         min = 1\n    #         max = 1\n    #     }\n    #     parameter-size-limit = 1 m\n    #     activation {\n    #         payload {\n    #             max = 1 m\n    #             truncation = 1 m\n    #         }\n    #     }\n    # }\n\n    yarn {\n        master-url=\"http://localhost:8088\" //YARN Resource Manager endpoint to be accessed from the invoker\n        yarn-link-log-message=true //If true, display a link to YARN in the static log message, otherwise do not include a link to YARN.\n        service-name=\"openwhisk-action-service\" //Name of the YARN Service created by the invoker. The invoker number will be appended.\n        auth-type=\"simple\" //Authentication type for YARN (simple or kerberos)\n        kerberos-principal=\"\" //Kerberos principal to use for the YARN service. Note: must include a hostname\n        kerberos-keytab=\"\" //Location of keytab accessible by all node managers\n        queue=\"default\" //Name of the YARN queue where the service will be created\n        memory=256 //Memory used by each YARN container\n        cpus=1 //CPUs used by each YARN container\n    }\n\n    logstore {\n        #SplunkLogStore configuration\n        #splunk {\n        #    host = \"splunkhost\"                   #splunk api hostname\n        #    port = 8089                           #splunk api port\n        #    username = \"splunkapiusername\"        #splunk api username\n        #    password = \"splunkapipassword\"        #splunk api password\n        #    index = \"splunkindex\"                 #splunk index name\n        #    log-timestamp-field = \"log_timestamp\" #splunk field where timestamp is stored (to reflect log event generated time, not splunk's _time)\n        #    log-stream-field = \"log_stream\"       #splunk field where stream is stored (stdout/stderr)\n        #    log-message-field = \"log_message\"     #splunk field where log message is stored\n        #    namespace-field = \"namespace\"         #splunk field where namespace is stored\n        #    activation-id-field = \"activation_id\" #splunk field where activation id is stored\n        #    query-constraints = \"\"                #additional constraints for splunk queries\n        #    finalize-max-time = 10.seconds        #splunk api max_time The number of seconds to run this search before finalizing. Specify 0 to never finalize.\n        #    earliest-time-offset = 7.days         #splunk query will search for records no older than the offset defined; e.g. \"earliest_time=now() - offset\"\n        #    query-timestamp-offset = 2.seconds    #splunk query will be broadened by this 2*<offset value>; e.g. \"earliest_time=activation.start - offset\" and \"latest_time=activation.end + offset\"\n        #    disable-sni = false                    #if true, disables hostname validation and cert validation (in case splunk api endpoint is using a self signed cert)\n        #}\n    }\n\n    # tracing configuration\n    tracing {\n        cache-expiry = 30 seconds #how long to keep spans in cache. Set to appropriate value to trace long running requests\n        #Zipkin configuration. Uncomment following to enable zipkin based tracing\n        #zipkin {\n        #   url = \"http://localhost:9411\" //URL to connect to zipkin server\n             //sample-rate to decide a request is sampled or not.\n             //sample-rate 0.5 equals to sampling 50% of the requests\n             //sample-rate of 1 means 100% sampling.\n             //sample-rate of 0 means no sampling\n        #   sample-rate = \"0.01\" // sample 1% of requests by default\n        #}\n    }\n\n    controller {\n        activation {\n            polling-from-db = true\n            max-wait-for-blocking-activation = 60 seconds\n        }\n    }\n\n    feature-flags {\n        # Enables support for `provide-api-key` annotation.\n        # See https://github.com/apache/openwhisk/pull/4284\n        # for details\n        require-api-key-annotation = true\n\n        # Enables the support to receive the response payload\n        # for POST and DELETE APIs\n        # See: https://github.com/apache/openwhisk/issues/3274\n        require-response-payload = true\n    }\n\n    apache-client {\n        # By default Apache HTTP Client would not retry NoHttpResponseException cases\n        # For some setups like Standalone mode this setting may need to be enabled\n        # to work around some Docker network issue\n        # In general this setting should be left to its default disabled state\n        retry-no-http-response-exception = false\n    }\n    # Enabling this will start to encrypt all default parameters for actions and packages. Be careful using this as\n    # it will slowly migrate all the actions that have been 'updated' to use encrypted parameters but going back would\n    # require a currently non-existing migration step.\n    parameter-storage {\n        # The current algorithm to use for parameter encryption, this can be changed but you have to leave all the keys\n        # configured for any algorithm you used previously.\n        # Allowed values:\n        #   \"off|noop\" -> no op/no encryption\n        #   \"aes-128\"  -> AES with 128 bit key (given as base64 encoded string)\n        #   \"aes-256\"  -> AES with 256 bit key (given as base64 encoded string)\n        current = \"off\"\n        # Base64 encoded 128 bit key\n        #aes-128 = \"\"\n        # Base64 encoded 256 bit key\n        #aes-256 = \"\"\n    }\n}\n#placeholder for test overrides so that tests can override defaults in application.conf (todo: move all defaults to reference.conf)\ntest {\n}\n"
  },
  {
    "path": "common/scala/src/main/resources/logback.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <jmxConfigurator></jmxConfigurator>\n  <include optional=\"true\" resource=\"whisk-logback.xml\"/>\n  <appender name=\"console\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>[%d{yyyy-MM-dd'T'HH:mm:ss.SSS'Z'}] [%p] %msg%n</pattern>\n    </encoder>\n  </appender>\n\n  <!-- Apache HttpClient -->\n  <logger name=\"org.apache.http\" level=\"ERROR\" />\n\n  <!-- Kafka -->\n  <logger name=\"org.apache.kafka\" level=\"ERROR\" />\n\n  <root level=\"${logback.log.level:-INFO}\">\n    <appender-ref ref=\"console\" />\n  </root>\n</configuration>"
  },
  {
    "path": "common/scala/src/main/resources/logging.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\npekko {\n  loglevel = \"DEBUG\"\n  loggers = [\"org.apache.pekko.event.slf4j.Slf4jLogger\"]\n  logging-filter = \"org.apache.pekko.event.slf4j.Slf4jLoggingFilter\"\n}\n"
  },
  {
    "path": "common/scala/src/main/resources/reference.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ninclude \"s3-reference.conf\"\n\nwhisk.spi {\n  ArtifactStoreProvider = org.apache.openwhisk.core.database.CouchDbStoreProvider\n  ActivationStoreProvider = org.apache.openwhisk.core.database.ArtifactActivationStoreProvider\n  MessagingProvider = org.apache.openwhisk.connector.kafka.KafkaMessagingProvider\n  ContainerFactoryProvider = org.apache.openwhisk.core.containerpool.docker.DockerContainerFactoryProvider\n  LogStoreProvider = org.apache.openwhisk.core.containerpool.logging.DockerToActivationLogStoreProvider\n  LoadBalancerProvider = org.apache.openwhisk.core.loadBalancer.FPCPoolBalancer\n  EntitlementSpiProvider = org.apache.openwhisk.core.entitlement.FPCEntitlementProvider\n  AuthenticationDirectiveProvider = org.apache.openwhisk.core.controller.BasicAuthenticationDirective\n  InvokerProvider = org.apache.openwhisk.core.invoker.FPCInvokerReactive\n  InvokerServerProvider = org.apache.openwhisk.core.invoker.FPCInvokerServer\n  DurationCheckerProvider = org.apache.openwhisk.core.scheduler.queue.ElasticSearchDurationCheckerProvider\n}\n\ndispatchers {\n  # Custom dispatcher for CouchDB Client. Tune as needed.\n  couch-dispatcher {\n    type = Dispatcher\n    executor = \"thread-pool-executor\"\n\n    # Underlying thread pool implementation is java.util.concurrent.ThreadPoolExecutor\n    thread-pool-executor {\n      # Min number of threads to cap factor-based corePoolSize number to\n      core-pool-size-min = 2\n\n      # The core-pool-size-factor is used to determine corePoolSize of the\n      # ThreadPoolExecutor using the following formula:\n      # ceil(available processors * factor).\n      # Resulting size is then bounded by the core-pool-size-min and\n      # core-pool-size-max values.\n      core-pool-size-factor = 2.0\n\n      # Max number of threads to cap factor-based corePoolSize number to\n      core-pool-size-max = 32\n    }\n    # Throughput defines the number of messages that are processed in a batch\n    # before the thread is returned to the pool. Set to 1 for as fair as possible.\n    throughput = 5\n  }\n\n  # Custom dispatcher for Kafka client. Tune as needed.\n  kafka-dispatcher {\n    type = Dispatcher\n    executor = \"thread-pool-executor\"\n\n    # Underlying thread pool implementation is java.util.concurrent.ThreadPoolExecutor\n    thread-pool-executor {\n      # Min number of threads to cap factor-based corePoolSize number to\n      core-pool-size-min = 2\n\n      # The core-pool-size-factor is used to determine corePoolSize of the\n      # ThreadPoolExecutor using the following formula:\n      # ceil(available processors * factor).\n      # Resulting size is then bounded by the core-pool-size-min and\n      # core-pool-size-max values.\n      core-pool-size-factor = 2.0\n\n      # Max number of threads to cap factor-based corePoolSize number to\n      core-pool-size-max = 32\n    }\n\n    # Throughput defines the number of messages that are processed in a batch\n    # before the thread is returned to the pool. Set to 1 for as fair as possible.\n    throughput = 5\n  }\n  lease-service-dispatcher {\n    type = PinnedDispatcher\n    executor = \"thread-pool-executor\"\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/resources/s3-reference.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nwhisk {\n  s3 {\n\n    # Name of S3 bucket\n    # bucket =\n\n    # Folder path within the bucket (optional)\n    # prefix =\n\n    # CloudFront configuration - Enable this if the attachments are to be read from the CloudFront CDN\n    # cloud-front-config {\n        # CloudFront domain name\n        # domain-name = xxx.cloudfront.net\n\n        # Signing key pair id\n        # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs\n        # key-pair-id = AAXXXX\n\n        # Private key content in PEM format\n        # private-key = \"\"\"-----BEGIN RSA PRIVATE KEY-----xxx-----END RSA PRIVATE KEY-----\"\"\"\n\n        # Timeout for generated signed url\n        # timeout = 10 min\n    # }\n\n    # See https://pekko.apache.org/docs/pekko-connectors/current/s3.html\n    pekko-connectors {\n      # whether the buffer request chunks (up to 5MB each) to \"memory\" or \"disk\"\n      buffer = \"memory\"\n\n      # location for temporary files, if buffer is set to \"disk\". If empty, uses the standard java temp path.\n      disk-buffer-path = \"\"\n\n      proxy {\n        # hostname of the proxy. If undefined (\"\") proxy is not enabled.\n        host = \"\"\n        port = 8000\n\n        # if \"secure\" is set to \"true\" then HTTPS will be used for all requests to S3, otherwise HTTP will be used\n        secure = true\n      }\n\n      # default values for AWS configuration. If credentials and/or region are not specified when creating S3Client,\n      # these values will be used.\n      aws {\n        # If this section is absent, the fallback behavior is to use the\n        # com.amazonaws.auth.DefaultAWSCredentialsProviderChain instance to resolve credentials\n        credentials {\n          # supported providers:\n          # anon - anonymous requests (\"no auth\")\n          # static - static credentials,\n          #   required params:\n          #     access-key-id\n          #     secret-access-key\n          #   optional:\n          #     token\n          # default: as described in com.amazonaws.auth.DefaultAWSCredentialsProviderChain docs,\n          # attempts to get the credentials from either:\n          #   - environment variables\n          #   - system properties\n          #   - credentials file\n          #   - EC2 credentials service\n          #   - IAM / metadata\n          provider = default\n        }\n\n        # If this section is absent, the fallback behavior is to use the\n        # com.amazonaws.regions.AwsRegionProvider.DefaultAwsRegionProviderChain instance to resolve region\n        region {\n          # supported providers:\n          # static - static credentials,\n          #   required params:\n          #     default-region\n          # default: as described in com.amazonaws.regions.AwsRegionProvider.DefaultAwsRegionProviderChain docs,\n          # attempts to get the region from either:\n          #   - environment variables\n          #   - system properties\n          #   - progile file\n          #   - EC2 metadata\n          provider = default\n        }\n      }\n\n      # Enable path style access to s3, i.e. \"https://s3-eu-west-1.amazonaws.com/my.bucket/myobject\"\n      # Default is virtual-hosted style.\n      # When using virtual hosted–style buckets with SSL, the S3 wild card certificate only matches buckets that do not contain periods.\n      # Buckets containing periods will lead to certificate errors. In those cases it's useful to enable path-style access.\n      path-style-access = true\n\n      # Custom endpoint url, used for alternate s3 implementations\n      # endpoint-url = null\n\n      # Which version of the list bucket api to use. Set to 1 to use the old style version 1 API.\n      # By default the newer version 2 api is used.\n      list-bucket-api-version = 2\n\n      # Validate the object key before making requests to S3\n      validate-object-key = true\n\n      # Whether to sign anonymous requests\n      sign-anonymous-requests = true\n\n      # Default settings corresponding to automatic retry of requests in an S3 stream.\n      retry-settings {\n        # The maximum number of additional attempts (following transient errors) that will be made to process a given request before giving up.\n        max-retries = 3\n        # The minimum delay between request retries.\n        min-backoff = 200ms\n        # The maximum delay between request retries.\n        max-backoff = 10s\n        # Random jitter factor applied to retry delay calculation.\n        random-factor = 0.0\n      }\n\n      # Various settings for the multipart upload\n      multipart-upload {\n        retry-settings = ${whisk.s3.pekko-connectors.retry-settings}\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/AverageRingBuffer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nobject AverageRingBuffer {\n  def apply(maxSize: Int) = new AverageRingBuffer(maxSize)\n}\n\n/**\n * This buffer provides the average of the given elements.\n * The number of elements are limited and the first element is removed if the maximum size is reached.\n * Since it is based on the Vector, its operation takes effectively constant time.\n * For more details, please visit https://docs.scala-lang.org/overviews/collections/performance-characteristics.html\n *\n * @param maxSize the maximum size of the buffer\n */\nclass AverageRingBuffer(private val maxSize: Int) {\n  private var elements = Vector.empty[Double]\n  private var sum = 0.0\n\n  def nonEmpty: Boolean = elements.nonEmpty\n\n  def average: Double = {\n    val size = elements.size\n    sum / size\n  }\n\n  def add(el: Double): Unit = synchronized {\n    if (elements.size == maxSize) {\n      sum = sum + el - elements.head\n      elements = elements.tail :+ el\n    } else {\n      sum += el\n      elements = elements :+ el\n    }\n  }\n\n  def size(): Int = elements.size\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/CausedBy.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\n/**\n * Helper to match on exceptions caused by other exceptions.\n *\n * Use this like:\n *\n * ```\n * try {\n *   block()\n * } catch {\n *   case CausedBy(internalException: MyFancyException) => ...\n * }\n * ```\n */\nobject CausedBy {\n  def unapply(e: Throwable): Option[Throwable] = Option(e.getCause)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/Config.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport scala.util.Try\n\n/**\n * A set of properties which define a configuration.\n *\n * This class tries to populate the properties in the following order.  Each step may overwrite\n * properties defined earlier.\n *\n * <ol>\n * <li> first it uses default values specified in the requiredProperties map if any.\n * <li> first it looks in the system environment.  If the system environment defines a value for FOO_BAR, this\n * will be used as the value for properties foo.bar or FoO.BaR. By convention, we usually define variables as\n * all lowercase (foo.bar)\n * <li> next it reads the properties from the propertiesFile.   In this case it looks for an exact match for \"foo.bar\" in the\n * file\n * </ol>\n *\n * After loading the properties, this validates that all required properties are defined.\n *\n * @param requiredProperties a Map whose keys define properties that must be bound to\n * a value, and whose values are default values. A null value in the Map means there is\n * no default value specified.\n * @param optionalProperties a Set of optional properties which may or not be defined.\n * @param env an optional environment to read from (defaults to sys.env).\n */\nclass Config(requiredProperties: Map[String, String], optionalProperties: Set[String] = Set.empty)(\n  env: Map[String, String] = sys.env)(implicit logging: Logging) {\n\n  private val settings = getProperties().toMap.filter {\n    case (k, v) =>\n      requiredProperties.contains(k) ||\n        (optionalProperties.contains(k) && v != null)\n  }\n\n  lazy val isValid: Boolean = Config.validateProperties(requiredProperties, settings)\n\n  /**\n   * Gets value for key if it exists else the empty string.\n   * The value of the override key will instead be returned if its value is present in the map.\n   *\n   * @param key to lookup\n   * @param overrideKey the property whose value will be returned if the map contains the override key.\n   * @return value for the key or the empty string if the key does not have a value/does not exist\n   */\n  def apply(key: String, overrideKey: String = \"\"): String = {\n    Try(settings(overrideKey)).orElse(Try(settings(key))).getOrElse(\"\")\n  }\n\n  /**\n   * Returns the value of a given key.\n   *\n   * @param key the property that has to be returned.\n   */\n  def getProperty(key: String): String = {\n    this(key)\n  }\n\n  /**\n   * Returns the value of a given key parsed as a double.\n   * If parsing fails, return the default value.\n   *\n   * @param key the property that has to be returned.\n   */\n  def getAsDouble(key: String, defaultValue: Double): Double = {\n    Try { getProperty(key).toDouble } getOrElse { defaultValue }\n  }\n\n  /**\n   * Returns the value of a given key parsed as an integer.\n   * If parsing fails, return the default value.\n   *\n   * @param key the property that has to be returned.\n   */\n  def getAsInt(key: String, defaultValue: Int): Int = {\n    Try { getProperty(key).toInt } getOrElse { defaultValue }\n  }\n\n  /**\n   * Returns the value of a given key parsed as a boolean.\n   * If parsing fails, return the default value.\n   *\n   * @param key the property that has to be returned.\n   */\n  def getAsBoolean(key: String, defaultValue: Boolean): Boolean = {\n    Try(getProperty(key).toBoolean).getOrElse(defaultValue)\n  }\n\n  /**\n   * Converts the set of property to a string for debugging.\n   */\n  def mkString: String = settings.mkString(\"\\n\")\n\n  /**\n   * Loads the properties from the environment into a mutable map.\n   *\n   * @return a pair which is the Map defining the properties, and a boolean indicating whether validation succeeded.\n   */\n  protected def getProperties(): scala.collection.mutable.Map[String, String] = {\n    val required = scala.collection.mutable.Map[String, String]() ++= requiredProperties\n    Config.readPropertiesFromSystemAndEnv(required, env)\n\n    // for optional value, assign them a default from the required properties list\n    // to prevent loss of a default value on a required property that may not otherwise be defined\n    val optional = scala.collection.mutable.Map[String, String]() ++= optionalProperties.map { k =>\n      k -> required.getOrElse(k, null)\n    }\n    Config.readPropertiesFromSystemAndEnv(optional, env)\n\n    required ++ optional\n  }\n}\n\n/**\n * Singleton object which provides global methods to manage configuration.\n */\nobject Config {\n  val prefix = \"whisk-config.\"\n\n  /**\n   * Reads a Map of key-value pairs from the environment -- store them in the\n   * mutable properties object.\n   */\n  def readPropertiesFromSystemAndEnv(properties: scala.collection.mutable.Map[String, String],\n                                     env: Map[String, String])(implicit logging: Logging) = {\n    val keys: Seq[String] = properties.keys.toSeq\n    readPropertiesFromSystem(properties)\n    for (p <- keys) {\n      val envp = p.replace('.', '_').toUpperCase\n      val envv = env.get(envp)\n      if (envv.isDefined) {\n        logging.info(this, s\"environment set value for $p\")\n        properties += p -> envv.get.trim\n      }\n    }\n  }\n\n  def readPropertiesFromSystem(properties: scala.collection.mutable.Map[String, String])(implicit logging: Logging) = {\n    val keys: Seq[String] = properties.keys.toSeq\n    for (p <- keys) {\n      val sysv = Option(System.getProperty(prefix + p))\n      if (sysv.isDefined) {\n        logging.info(this, s\"system set value for $p\")\n        properties += p -> sysv.get.trim\n      }\n    }\n  }\n\n  /**\n   * Checks that the properties object defines all the required properties.\n   *\n   * @param required a key-value map where the keys are required properties\n   * @param properties a set of properties to check\n   */\n  def validateProperties(required: Map[String, String], properties: Map[String, String])(\n    implicit logging: Logging): Boolean = {\n    required.keys.forall { key =>\n      val value = properties(key)\n      if (value == null) logging.error(this, s\"required property $key still not set\")\n      value != null\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/ConfigMXBean.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\nimport java.lang.management.ManagementFactory\n\nimport com.typesafe.config.{ConfigFactory, ConfigRenderOptions}\nimport javax.management.ObjectName\n\ntrait ConfigMXBean {\n\n  /**\n   * Renders the config value to a string\n   *\n   * @param path root of subtree which needs to be rendered. Pass `.` for getting whole subtree rendered\n   * @param originComment {@link ConfigValue#origin} of that setting's value. For example these\n   *                            comments might tell you which file a setting comes from.\n   * @return rendered config\n   */\n  def getConfig(path: String, originComment: Boolean): String\n}\n\nobject ConfigMXBean extends ConfigMXBean {\n  val name = new ObjectName(\"org.apache.openwhisk:name=config\")\n  private val renderOptions =\n    ConfigRenderOptions.defaults().setComments(false).setOriginComments(true).setFormatted(true).setJson(false)\n\n  override def getConfig(path: String, originComment: Boolean): String = {\n    val config = ConfigFactory.load()\n    val co = if (path == \".\") config.root() else config.getConfig(path).root()\n    co.render(renderOptions.setOriginComments(originComment))\n  }\n\n  def register(): Unit = {\n    ManagementFactory.getPlatformMBeanServer.registerMBean(ConfigMXBean, name)\n  }\n\n  def unregister(): Unit = {\n    ManagementFactory.getPlatformMBeanServer.unregisterMBean(name)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/ConfigMapValue.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.io.File\nimport java.net.URI\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport org.apache.commons.io.FileUtils\nimport pureconfig.ConfigReader\nimport pureconfig.ConvertHelpers.catchReadError\n\nclass ConfigMapValue private (val value: String)\n\nobject ConfigMapValue {\n\n  /**\n   * Checks if the value is a file url like `file:/etc/config/foo.yaml` then treat it as a file reference\n   * and read its content otherwise consider it as a literal value\n   */\n  def apply(config: String): ConfigMapValue = {\n    val value = if (config.startsWith(\"file:\")) {\n      val uri = new URI(config)\n      val file = new File(uri)\n      FileUtils.readFileToString(file, UTF_8)\n    } else config\n    new ConfigMapValue(value)\n  }\n\n  implicit val reader: ConfigReader[ConfigMapValue] = ConfigReader.fromString[ConfigMapValue](catchReadError(apply))\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/Counter.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.util.concurrent.atomic.AtomicLong\n\n/**\n * A simple thread-safe counter.\n */\nclass Counter {\n  private val cnt = new AtomicLong(0L)\n  def cur = cnt.get()\n\n  /**\n   * Increments and gets the current value.\n   */\n  def next(): Long = {\n    cnt.incrementAndGet()\n  }\n\n  /**\n   * Decrements and gets the current value.\n   */\n  def prev(): Long = {\n    cnt.decrementAndGet()\n  }\n\n  /**\n   * Sets the value\n   */\n  def set(i: Long) = cnt.set(i)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/ExecutorCloser.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\nimport java.io.Closeable\nimport java.util.concurrent.{ExecutorService, TimeUnit}\n\nimport org.apache.pekko.event.slf4j.SLF4JLogging\n\nimport scala.concurrent.duration._\n\ncase class ExecutorCloser(service: ExecutorService, timeout: FiniteDuration = 5.seconds)\n    extends Closeable\n    with SLF4JLogging {\n  override def close(): Unit = {\n    try {\n      service.shutdown()\n      service.awaitTermination(timeout.toSeconds, TimeUnit.SECONDS)\n    } catch {\n      case e: InterruptedException =>\n        log.error(\"Error while shutting down the ExecutorService\", e)\n        Thread.currentThread.interrupt()\n    } finally {\n      if (!service.isShutdown) {\n        log.warn(s\"ExecutorService `$service` didn't shutdown property. Will be forced now.\")\n      }\n      service.shutdownNow()\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/ForcibleSemaphore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.util.concurrent.locks.AbstractQueuedSynchronizer\n\nimport scala.annotation.tailrec\n\n/**\n * A Semaphore, which in addition to the usual features has means to force more clients to get permits.\n *\n * Like any usual Semaphore, this implementation will give away at most `maxAllowed` permits when used the \"usual\" way.\n * In addition to that, it also has a `forceAcquire` method which will push the Semaphore's remaining permits into a\n * negative value. Getting permits using `tryAcquire` will only be possible once the permits value is in a positive\n * state again.\n *\n * As this is (now) only used for the loadbalancer's scheduling, this does not implement the \"whole\" Java Semaphore's\n * interface but only the methods needed.\n *\n * @param maxAllowed maximum number of permits given away by `tryAcquire`\n */\nclass ForcibleSemaphore(maxAllowed: Int) {\n  class Sync extends AbstractQueuedSynchronizer {\n    setState(maxAllowed)\n\n    def permits: Int = getState\n\n    /** Try to release a permit and return whether or not that operation was successful. */\n    @tailrec\n    override final def tryReleaseShared(releases: Int): Boolean = {\n      val current = getState\n      val next = current + releases\n      if (next < current) { // integer overflow\n        throw new Error(\"Maximum permit count exceeded, permit variable overflowed\")\n      }\n      if (compareAndSetState(current, next)) {\n        true\n      } else {\n        tryReleaseShared(releases)\n      }\n    }\n\n    /**\n     * Try to acquire a permit and return whether or not that operation was successful. Requests may not finish in FIFO\n     * order, hence this method is not necessarily fair.\n     */\n    @tailrec\n    final def nonFairTryAcquireShared(acquires: Int): Int = {\n      val available = getState\n      val remaining = available - acquires\n      if (remaining < 0 || compareAndSetState(available, remaining)) {\n        remaining\n      } else {\n        nonFairTryAcquireShared(acquires)\n      }\n    }\n\n    /**\n     * Basically the same as `nonFairTryAcquireShared`, but does bound to a minimal value of 0 so permits can get\n     * negative.\n     */\n    @tailrec\n    final def forceAquireShared(acquires: Int): Unit = {\n      val available = getState\n      val remaining = available - acquires\n      if (!compareAndSetState(available, remaining)) {\n        forceAquireShared(acquires)\n      }\n    }\n  }\n\n  private val sync = new Sync\n\n  /**\n   * Acquires the given numbers of permits.\n   *\n   * @param acquires the number of permits to get\n   * @return `true`, iff the internal semaphore's number of permits is positive, `false` if negative\n   */\n  def tryAcquire(acquires: Int = 1): Boolean = {\n    require(acquires > 0, \"cannot acquire negative or no permits\")\n    sync.nonFairTryAcquireShared(acquires) >= 0\n  }\n\n  /**\n   * Forces the amount of permits.\n   *\n   * This possibly pushes the internal number of available permits to a negative value.\n   *\n   * @param acquires the number of permits to get\n   */\n  def forceAcquire(acquires: Int = 1): Unit = {\n    require(acquires > 0, \"cannot force acquire negative or no permits\")\n    sync.forceAquireShared(acquires)\n  }\n\n  /**\n   * Releases the given amount of permits\n   *\n   * @param acquires the number of permits to release\n   */\n  def release(acquires: Int = 1): Unit = {\n    require(acquires > 0, \"cannot release negative or no permits\")\n    sync.releaseShared(acquires)\n  }\n\n  /** Returns the number of currently available permits. Possibly negative. */\n  def availablePermits: Int = sync.permits\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/Https.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.io.{FileInputStream, InputStream}\nimport java.security.{KeyStore, SecureRandom}\nimport javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}\nimport org.apache.pekko.http.scaladsl.{ConnectionContext, HttpsConnectionContext}\nimport org.apache.pekko.stream.TLSClientAuth\n\nobject Https {\n  case class HttpsConfig(keystorePassword: String, keystoreFlavor: String, keystorePath: String, clientAuth: String)\n\n  def getCertStore(password: Array[Char], flavor: String, path: String): KeyStore = {\n    val cs: KeyStore = KeyStore.getInstance(flavor)\n    val certStore: InputStream = new FileInputStream(path)\n    cs.load(certStore, password)\n    cs\n  }\n\n  def httpsInsecureClient(context: SSLContext): HttpsConnectionContext =\n    ConnectionContext.httpsClient((host, port) => {\n      val engine = context.createSSLEngine(host, port)\n      engine.setUseClientMode(true)\n      // WARNING: this creates an SSL Engine without enabling endpoint identification/verification procedures\n      // Disabling host name verification is a very bad idea, please don't unless you have a very good reason to.\n      engine\n    })\n\n  def applyHttpsConfig(httpsConfig: HttpsConfig, withDisableHostnameVerification: Boolean = false): SSLContext = {\n    val keyFactoryType = \"SunX509\"\n    val clientAuth = {\n      if (httpsConfig.clientAuth.toBoolean)\n        Some(TLSClientAuth.need)\n      else\n        Some(TLSClientAuth.none)\n    }\n\n    val keystorePassword = httpsConfig.keystorePassword.toCharArray\n\n    val keyStore: KeyStore = KeyStore.getInstance(httpsConfig.keystoreFlavor)\n    val keyStoreStream: InputStream = new FileInputStream(httpsConfig.keystorePath)\n    keyStore.load(keyStoreStream, keystorePassword)\n\n    val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(keyFactoryType)\n    keyManagerFactory.init(keyStore, keystorePassword)\n\n    // Currently, we are using the keystore as truststore as well, because the clients use the same keys as the\n    // server for client authentication (if enabled).\n    // So this code is guided by https://pekko.apache.org/docs/pekko-http/current/server-side/server-https-support.html\n    // This needs to be reworked, when we fix the keys and certificates.\n    val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance(keyFactoryType)\n    trustManagerFactory.init(keyStore)\n\n    val sslContext: SSLContext = SSLContext.getInstance(\"TLS\")\n    sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, new SecureRandom)\n    sslContext\n  }\n\n  def connectionContextClient(httpsConfig: HttpsConfig,\n                              withDisableHostnameVerification: Boolean = false): HttpsConnectionContext = {\n    val sslContext = applyHttpsConfig(httpsConfig, withDisableHostnameVerification)\n    connectionContextClient(sslContext, withDisableHostnameVerification)\n  }\n\n  def connectionContextClient(sslContext: SSLContext,\n                              withDisableHostnameVerification: Boolean): HttpsConnectionContext = {\n    if (withDisableHostnameVerification) {\n      httpsInsecureClient(sslContext)\n    } else {\n      ConnectionContext.httpsClient(sslContext)\n    }\n  }\n\n  def connectionContextServer(httpsConfig: HttpsConfig,\n                              withDisableHostnameVerification: Boolean = false): HttpsConnectionContext = {\n    val sslContext: SSLContext = applyHttpsConfig(httpsConfig, withDisableHostnameVerification)\n    ConnectionContext.httpsServer(sslContext)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/Logging.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.io.PrintStream\nimport java.time.{Clock, Instant, ZoneId}\nimport java.time.format.DateTimeFormatter\nimport org.apache.pekko.event.Logging._\nimport org.apache.pekko.event.LoggingAdapter\nimport kamon.Kamon\nimport kamon.metric.{MeasurementUnit, Counter => KCounter, Gauge => KGauge, Histogram => KHistogram}\nimport kamon.statsd.{MetricKeyGenerator, SimpleMetricKeyGenerator}\nimport kamon.tag.TagSet\nimport org.apache.openwhisk.core.entity.ControllerInstanceId\n\ntrait Logging {\n\n  /**\n   * Prints a message on DEBUG level\n   *\n   * @param from Reference, where the method was called from.\n   * @param message Message to write to the log if not empty\n   */\n  def debug(from: AnyRef, message: => String)(implicit id: TransactionId = TransactionId.unknown) = {\n    if (id.meta.extraLogging) {\n      emit(InfoLevel, id, from, message)\n    } else {\n      emit(DebugLevel, id, from, message)\n    }\n  }\n\n  /**\n   * Prints a message on INFO level\n   *\n   * @param from Reference, where the method was called from.\n   * @param message Message to write to the log if not empty\n   */\n  def info(from: AnyRef, message: => String)(implicit id: TransactionId = TransactionId.unknown) = {\n    emit(InfoLevel, id, from, message)\n  }\n\n  /**\n   * Prints a message on WARN level\n   *\n   * @param from Reference, where the method was called from.\n   * @param message Message to write to the log if not empty\n   */\n  def warn(from: AnyRef, message: => String)(implicit id: TransactionId = TransactionId.unknown) = {\n    emit(WarningLevel, id, from, message)\n  }\n\n  /**\n   * Prints a message on ERROR level\n   *\n   * @param from Reference, where the method was called from.\n   * @param message Message to write to the log if not empty\n   */\n  def error(from: AnyRef, message: => String)(implicit id: TransactionId = TransactionId.unknown) = {\n    emit(ErrorLevel, id, from, message)\n  }\n\n  /**\n   * Prints a message to the output.\n   *\n   * @param loglevel The level to log on\n   * @param id <code>TransactionId</code> to include in the log\n   * @param from Reference, where the method was called from.\n   * @param message Message to write to the log if not empty\n   */\n  protected[common] def emit(loglevel: LogLevel, id: TransactionId, from: AnyRef, message: => String): Unit\n}\n\n/**\n * Implementation of Logging, that uses Pekko logging.\n */\nclass PekkoLogging(loggingAdapter: LoggingAdapter) extends Logging {\n  def emit(loglevel: LogLevel, id: TransactionId, from: AnyRef, message: => String) = {\n    if (loggingAdapter.isEnabled(loglevel)) {\n      val logmsg: String = message // generates the message\n      if (logmsg.nonEmpty) { // log it only if its not empty\n        val name = if (from.isInstanceOf[String]) from else Logging.getCleanSimpleClassName(from.getClass)\n        loggingAdapter.log(loglevel, format(id, name.toString, logmsg))\n      }\n    }\n  }\n\n  protected def format(id: TransactionId, name: String, logmsg: String) = {\n    val currentId = if (id.hasParent) s\"[$id] \" else \"\"\n    s\"[${id.root}] $currentId[$name] $logmsg\"\n  }\n}\n\n/**\n * Implementation of Logging, that uses the output stream.\n */\nclass PrintStreamLogging(outputStream: PrintStream = Console.out) extends Logging {\n  override def emit(loglevel: LogLevel, id: TransactionId, from: AnyRef, message: => String) = {\n    val now = Instant.now(Clock.systemUTC)\n    val time = Emitter.timeFormat.format(now)\n    val name = if (from.isInstanceOf[String]) from else Logging.getCleanSimpleClassName(from.getClass)\n\n    val level = loglevel match {\n      case DebugLevel   => \"DEBUG\"\n      case InfoLevel    => \"INFO\"\n      case WarningLevel => \"WARN\"\n      case ErrorLevel   => \"ERROR\"\n      case LogLevel(_)  => \"UNKNOWN\"\n    }\n\n    val logMessage = Seq(message).collect {\n      case msg if msg.nonEmpty =>\n        msg.split('\\n').map(_.trim).mkString(\" \")\n    }\n    val currentId = if (id.hasParent) id else \"\"\n\n    val parts = Seq(s\"[$time]\", s\"[$level]\", s\"[${id.root}]\", s\"[$currentId]\") ++ Seq(s\"[$name]\") ++ logMessage\n    outputStream.println(parts.mkString(\" \"))\n  }\n}\n\n/**\n * A triple representing the timestamp relative to which the elapsed time was computed,\n * typically for a TransactionId, the elapsed time in milliseconds and a string containing\n * the given marker token.\n *\n * @param token the LogMarkerToken that should be defined in LoggingMarkers\n * @param deltaToTransactionStart the time difference between now and the start of the Transaction\n * @param deltaToMarkerStart if this is an end marker, this is the time difference to the start marker\n */\ncase class LogMarker(token: LogMarkerToken, deltaToTransactionStart: Long, deltaToMarkerStart: Option[Long] = None) {\n  override def toString() = {\n    val parts = Seq(LogMarker.keyword, token.toStringWithSubAction, deltaToTransactionStart) ++ deltaToMarkerStart\n    \"[\" + parts.mkString(\":\") + \"]\"\n  }\n}\n\nobject LogMarker {\n\n  val keyword = \"marker\"\n\n  /** Convenience method for parsing log markers in unit tests. */\n  def parse(s: String) = {\n    val logmarker = raw\"\\[${keyword}:([^\\s:]+):(\\d+)(?::(\\d+))?\\]\".r.unanchored\n    val logmarker(token, deltaToTransactionStart, deltaToMarkerStart) = s\n    LogMarker(LogMarkerToken.parse(token), deltaToTransactionStart.toLong, Option(deltaToMarkerStart).map(_.toLong))\n  }\n}\n\nprivate object Logging {\n\n  /**\n   * Given a class object, return its simple name less the trailing dollar sign.\n   */\n  def getCleanSimpleClassName(clz: Class[_]) = {\n    val simpleName = clz.getSimpleName\n    if (simpleName.endsWith(\"$\")) simpleName.dropRight(1)\n    else simpleName\n  }\n}\n\nprivate object Emitter {\n  val timeFormat = DateTimeFormatter.ofPattern(\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\").withZone(ZoneId.of(\"UTC\"))\n}\n\n/**\n * Used to record log message and make a metric name.\n *\n * @param component Component like invoker, controller, and docker. It is defined in LoggingMarkers.\n * @param action Action of the component.\n * @param state State of the action.\n * @param subAction more specific identifier for \"action\", like `runc.resume`\n * @param tags tags can be used for whatever granularity you might need.\n */\ncase class LogMarkerToken(\n  component: String,\n  action: String,\n  state: String,\n  subAction: Option[String] = None,\n  tags: Map[String, String] = Map.empty)(measurementUnit: MeasurementUnit = MeasurementUnit.none) {\n  private var finishToken: LogMarkerToken = _\n  private var errorToken: LogMarkerToken = _\n\n  // Using var is safe wrt thread-safety because Kamon makes sure the instances\n  // (given the same key) are always the same, so a missed update is not harmful\n  private var _counter: KCounter = _\n  private var _histogram: KHistogram = _\n  private var _gauge: KGauge = _\n\n  override val toString = component + \"_\" + action + \"_\" + state\n  val toStringWithSubAction: String =\n    subAction.map(sa => component + \"_\" + action + \".\" + sa + \"_\" + state).getOrElse(toString)\n\n  def asFinish: LogMarkerToken = {\n    if (finishToken == null) {\n      finishToken = copy(state = LoggingMarkers.finish)(measurementUnit)\n    }\n    finishToken\n  }\n\n  def asError: LogMarkerToken = {\n    if (errorToken == null) {\n      errorToken = copy(state = LoggingMarkers.error)(measurementUnit)\n    }\n    errorToken\n  }\n\n  def counter: KCounter = {\n    if (_counter == null) {\n      _counter = createCounter()\n    }\n    _counter\n  }\n\n  def histogram: KHistogram = {\n    if (_histogram == null) {\n      _histogram = createHistogram()\n    }\n    _histogram\n  }\n\n  def gauge: KGauge = {\n    if (_gauge == null) {\n      _gauge = createGauge()\n    }\n    _gauge\n  }\n\n  private def createCounter() = {\n    if (TransactionId.metricsKamonTags) {\n      Kamon.counter(createName(toString, \"counter\")).withTags(TagSet.from(tags))\n    } else {\n      Kamon.counter(createName(toStringWithSubAction, \"counter\")).withoutTags()\n    }\n  }\n\n  private def createHistogram() = {\n    if (TransactionId.metricsKamonTags) {\n      Kamon.histogram(createName(toString, \"histogram\"), measurementUnit).withTags(TagSet.from(tags))\n    } else {\n      Kamon.histogram(createName(toStringWithSubAction, \"histogram\"), measurementUnit).withoutTags()\n    }\n  }\n\n  private def createGauge() = {\n    if (TransactionId.metricsKamonTags) {\n      Kamon.gauge(createName(toString, \"gauge\"), measurementUnit).withTags(TagSet.from(tags))\n    } else {\n      Kamon.gauge(createName(toStringWithSubAction, \"gauge\"), measurementUnit).withoutTags()\n    }\n  }\n\n  /**\n   * Kamon 1.0 onwards does not include the metric type in the metric name which cause issue\n   * for us as we use same metric name for counter and histogram. So to be backward compatible we\n   * need to prefix the name with type\n   */\n  private def createName(name: String, metricType: String) = {\n    s\"$metricType.$name\"\n  }\n}\n\nobject LogMarkerToken {\n\n  def parse(string: String) = {\n    // Per convention the components are guaranteed to not contain '_'\n    // thus it's safe to split at '_' to get the components\n    val Array(component, action, state) = string.split('_')\n\n    val (generalAction, subAction) = action.split('.').toList match {\n      case Nil         => throw new IllegalArgumentException(\"LogMarkerToken malformed\")\n      case a :: Nil    => (a, None)\n      case a :: s :: _ => (a, Some(s))\n    }\n\n    LogMarkerToken(component, generalAction, state, subAction)(MeasurementUnit.none)\n  }\n\n}\n\nobject MetricEmitter {\n  def emitCounterMetric(token: LogMarkerToken, times: Long = 1): Unit = {\n    if (TransactionId.metricsKamon) {\n      token.counter.increment(times)\n    }\n  }\n\n  def emitHistogramMetric(token: LogMarkerToken, value: Long): Unit = {\n    if (TransactionId.metricsKamon) {\n      token.histogram.record(value)\n    }\n  }\n\n  def emitGaugeMetric(token: LogMarkerToken, value: Long): Unit = {\n    if (TransactionId.metricsKamon) {\n      token.gauge.update(value)\n    }\n  }\n}\n\n/**\n * Name generator to make names compatible to pre Kamon 1.0 logic. Statsd reporter \"normalizes\"\n * the key name by replacing all `.` with `_`. Pre 1.0 the metric category was added by Statsd\n * reporter itself. However now we pass it explicitly. So to retain the pre 1.0 name we need to replace\n * normalized name with one having category followed by `.` instead of `_`\n */\nclass WhiskStatsDMetricKeyGenerator(config: com.typesafe.config.Config) extends MetricKeyGenerator {\n  val simpleGen = new SimpleMetricKeyGenerator(config)\n  override def generateKey(name: String, tags: TagSet): String = {\n    val key = simpleGen.generateKey(name, tags)\n    if (key.contains(\".counter_\")) key.replace(\".counter_\", \".counter.\")\n    else if (key.contains(\".histogram_\")) key.replace(\".histogram_\", \".histogram.\")\n    else key\n  }\n}\n\nobject LoggingMarkers {\n\n  val start = \"start\"\n  val finish = \"finish\"\n  val error = \"error\"\n  val counter = \"counter\"\n  val timeout = \"timeout\"\n\n  private val controller = \"controller\"\n  private val scheduler = \"scheduler\"\n  private val invoker = \"invoker\"\n  private val database = \"database\"\n  private val activation = \"activation\"\n  private val kafka = \"kafka\"\n  private val loadbalancer = \"loadbalancer\"\n  private val containerClient = \"containerClient\"\n  private val containerPool = \"containerPool\"\n\n  /*\n   * The following markers are used to emit log messages as well as metrics. Add all LogMarkerTokens below to\n   * have a reference list of all metrics. The list below contains LogMarkerToken singletons (val) as well as\n   * LogMarkerToken creation functions (def). The LogMarkerToken creation functions allow to include variable\n   * information in metrics, such as the controller / invoker id or commands executed by a container factory.\n   *\n   * When using LogMarkerTokens for emitting metrics, you should use the convenience functions only once to\n   * create LogMarkerToken singletons instead of creating LogMarkerToken instances over and over again for each\n   * metric emit.\n   *\n   * Example:\n   * val MY_COUNTER_GREEN = LoggingMarkers.MY_COUNTER(GreenCounter)\n   * ...\n   * MetricEmitter.emitCounterMetric(MY_COUNTER_GREEN)\n   *\n   * instead of\n   *\n   * MetricEmitter.emitCounterMetric(LoggingMarkers.MY_COUNTER(GreenCounter))\n   */\n  def SCHEDULER_NAMESPACE_CONTAINER(namespace: String) =\n    LogMarkerToken(scheduler, \"namespaceContainer\", counter, Some(namespace), Map(\"namespace\" -> namespace))(\n      MeasurementUnit.none)\n  def SCHEDULER_NAMESPACE_INPROGRESS_CONTAINER(namespace: String) =\n    LogMarkerToken(scheduler, \"namespaceInProgressContainer\", counter, Some(namespace), Map(\"namespace\" -> namespace))(\n      MeasurementUnit.none)\n  def SCHEDULER_ACTION_CONTAINER(namespace: String, actionWithVersion: String, actionWithoutVersion: String) =\n    LogMarkerToken(\n      scheduler,\n      \"actionContainer\",\n      counter,\n      Some(actionWithoutVersion),\n      Map(\"namespace\" -> namespace, \"action\" -> actionWithVersion))(MeasurementUnit.none)\n  def SCHEDULER_ACTION_INPROGRESS_CONTAINER(namespace: String,\n                                            actionWithVersion: String,\n                                            actionWithoutVersion: String) =\n    LogMarkerToken(\n      scheduler,\n      \"actionInProgressContainer\",\n      counter,\n      Some(actionWithoutVersion),\n      Map(\"namespace\" -> namespace, \"action\" -> actionWithVersion))(MeasurementUnit.none)\n\n  /*\n   * Controller related markers\n   */\n  def CONTROLLER_STARTUP(id: String) =\n    if (TransactionId.metricsKamonTags)\n      LogMarkerToken(controller, s\"startup\", counter, None, Map(\"controller_id\" -> id))(MeasurementUnit.none)\n    else LogMarkerToken(controller, s\"startup$id\", counter)(MeasurementUnit.none)\n\n  // Time of the activation in controller until it is delivered to Kafka\n  val CONTROLLER_ACTIVATION =\n    LogMarkerToken(controller, activation, start)(MeasurementUnit.time.milliseconds)\n  val CONTROLLER_ACTIVATION_BLOCKING =\n    LogMarkerToken(controller, \"blockingActivation\", start)(MeasurementUnit.time.milliseconds)\n  val CONTROLLER_ACTIVATION_BLOCKING_DATABASE_RETRIEVAL =\n    LogMarkerToken(controller, \"blockingActivationDatabaseRetrieval\", counter)(MeasurementUnit.none)\n\n  // Time that is needed to load balance the activation\n  val CONTROLLER_LOADBALANCER = LogMarkerToken(controller, loadbalancer, start)(MeasurementUnit.none)\n\n  // Time that is needed to produce message in kafka\n  val CONTROLLER_KAFKA = LogMarkerToken(controller, kafka, start)(MeasurementUnit.time.milliseconds)\n  def INVOKER_SHAREDPACKAGE(path: String) =\n    LogMarkerToken(invoker, \"sharedPackage\", counter, None, Map(\"path\" -> path))(MeasurementUnit.none)\n  def INVOKER_CONTAINERPOOL_MEMORY(state: String) =\n    LogMarkerToken(invoker, \"containerPoolMemory\", counter, Some(state), Map(\"state\" -> state))(MeasurementUnit.none)\n  def INVOKER_CONTAINERPOOL_CONTAINER(state: String, tags: Option[Map[String, String]] = None) = {\n    val map = Map(\"state\" -> state) ++: tags.getOrElse(Map.empty)\n    LogMarkerToken(invoker, \"containerPoolContainer\", counter, Some(state), map)(MeasurementUnit.none)\n  }\n\n  // System overload and random invoker assignment\n  val MANAGED_SYSTEM_OVERLOAD =\n    LogMarkerToken(controller, \"managedInvokerSystemOverload\", counter)(MeasurementUnit.none)\n  val BLACKBOX_SYSTEM_OVERLOAD =\n    LogMarkerToken(controller, \"blackBoxInvokerSystemOverload\", counter)(MeasurementUnit.none)\n  /*\n   * Invoker related markers\n   */\n  def INVOKER_STARTUP(i: Int) =\n    if (TransactionId.metricsKamonTags)\n      LogMarkerToken(invoker, s\"startup\", counter, None, Map(\"invoker_id\" -> i.toString))(MeasurementUnit.none)\n    else LogMarkerToken(invoker, s\"startup$i\", counter)(MeasurementUnit.none)\n\n  // Check invoker healthy state from loadbalancer\n  def LOADBALANCER_INVOKER_STATUS_CHANGE(state: String) =\n    LogMarkerToken(loadbalancer, \"invokerState\", counter, Some(state), Map(\"state\" -> state))(MeasurementUnit.none)\n  val LOADBALANCER_ACTIVATION_START = LogMarkerToken(loadbalancer, \"activations\", counter)(MeasurementUnit.none)\n\n  def LOADBALANCER_ACTIVATIONS_INFLIGHT(controllerInstance: ControllerInstanceId) = {\n    if (TransactionId.metricsKamonTags)\n      LogMarkerToken(\n        loadbalancer,\n        \"activationsInflight\",\n        counter,\n        None,\n        Map(\"controller_id\" -> controllerInstance.asString))(MeasurementUnit.none)\n    else\n      LogMarkerToken(loadbalancer + controllerInstance.asString, \"activationsInflight\", counter)(MeasurementUnit.none)\n  }\n  def LOADBALANCER_MEMORY_INFLIGHT(controllerInstance: ControllerInstanceId, actionType: String) =\n    if (TransactionId.metricsKamonTags)\n      LogMarkerToken(\n        loadbalancer,\n        s\"memory${actionType}Inflight\",\n        counter,\n        None,\n        Map(\"controller_id\" -> controllerInstance.asString))(MeasurementUnit.none)\n    else\n      LogMarkerToken(loadbalancer + controllerInstance.asString, s\"memory${actionType}Inflight\", counter)(\n        MeasurementUnit.none)\n\n  // Counter metrics for completion acks in load balancer\n  sealed abstract class CompletionAckType(val name: String) { def asString: String = name }\n  case object RegularCompletionAck extends CompletionAckType(\"regular\")\n  case object ForcedCompletionAck extends CompletionAckType(\"forced\")\n  case object HealthcheckCompletionAck extends CompletionAckType(\"healthcheck\")\n  case object RegularAfterForcedCompletionAck extends CompletionAckType(\"regularAfterForced\")\n  case object ForcedAfterRegularCompletionAck extends CompletionAckType(\"forcedAfterRegular\")\n\n  // Convenience function to create log marker tokens used for emitting counter metrics related to completion acks.\n  def LOADBALANCER_COMPLETION_ACK(controllerInstance: ControllerInstanceId, completionAckType: CompletionAckType) =\n    if (TransactionId.metricsKamonTags)\n      LogMarkerToken(\n        loadbalancer,\n        \"completionAck\",\n        counter,\n        None,\n        Map(\"controller_id\" -> controllerInstance.asString, \"type\" -> completionAckType.asString))(MeasurementUnit.none)\n    else\n      LogMarkerToken(\n        loadbalancer + controllerInstance.asString,\n        \"completionAck_\" + completionAckType.asString,\n        counter)(MeasurementUnit.none)\n\n  // Time that is needed to execute the action\n  val INVOKER_ACTIVATION_RUN =\n    LogMarkerToken(invoker, \"activationRun\", start)(MeasurementUnit.time.milliseconds)\n\n  // Time that is needed to init the action\n  val INVOKER_ACTIVATION_INIT =\n    LogMarkerToken(invoker, \"activationInit\", start)(MeasurementUnit.time.milliseconds)\n\n  // Time needed to collect the logs\n  val INVOKER_COLLECT_LOGS =\n    LogMarkerToken(invoker, \"collectLogs\", start)(MeasurementUnit.time.milliseconds)\n\n  // Time in invoker\n  val INVOKER_ACTIVATION = LogMarkerToken(invoker, activation, start)(MeasurementUnit.none)\n  def INVOKER_DOCKER_CMD(cmd: String) =\n    LogMarkerToken(invoker, \"docker\", start, Some(cmd), Map(\"cmd\" -> cmd))(MeasurementUnit.time.milliseconds)\n  def INVOKER_RUNC_CMD(cmd: String) =\n    LogMarkerToken(invoker, \"runc\", start, Some(cmd), Map(\"cmd\" -> cmd))(MeasurementUnit.time.milliseconds)\n  def INVOKER_KUBEAPI_CMD(cmd: String) =\n    LogMarkerToken(invoker, \"kubeapi\", start, Some(cmd), Map(\"cmd\" -> cmd))(MeasurementUnit.time.milliseconds)\n  def INVOKER_CONTAINER_START(containerState: String, invocationNamespace: String, namespace: String, action: String) =\n    LogMarkerToken(\n      invoker,\n      \"containerStart\",\n      counter,\n      Some(containerState),\n      Map(\n        \"containerState\" -> containerState,\n        \"initiator\" -> invocationNamespace,\n        \"namespace\" -> namespace,\n        \"action\" -> action))(MeasurementUnit.none)\n  def INVOKER_CONTAINER_CREATE(action: String, state: String) =\n    LogMarkerToken(invoker, \"creation\", counter, None, Map(\"action\" -> action, \"state\" -> state))(MeasurementUnit.none)\n  val INVOKER_CONTAINER_HEALTH = LogMarkerToken(invoker, \"containerHealth\", start)(MeasurementUnit.time.milliseconds)\n  val INVOKER_CONTAINER_HEALTH_FAILED_WARM =\n    LogMarkerToken(invoker, \"containerHealthFailed\", counter, Some(\"warm\"), Map(\"containerState\" -> \"warm\"))(\n      MeasurementUnit.none)\n  val INVOKER_CONTAINER_HEALTH_FAILED_PREWARM =\n    LogMarkerToken(invoker, \"containerHealthFailed\", counter, Some(\"prewarm\"), Map(\"containerState\" -> \"prewarm\"))(\n      MeasurementUnit.none)\n  val CONTAINER_CLIENT_RETRIES =\n    LogMarkerToken(containerClient, \"retries\", counter)(MeasurementUnit.none)\n\n  val CONTAINER_POOL_RESCHEDULED_ACTIVATION =\n    LogMarkerToken(containerPool, \"rescheduledActivation\", counter)(MeasurementUnit.none)\n  val CONTAINER_POOL_RUNBUFFER_COUNT =\n    LogMarkerToken(containerPool, \"runBufferCount\", counter)(MeasurementUnit.none)\n  val CONTAINER_POOL_RUNBUFFER_SIZE =\n    LogMarkerToken(containerPool, \"runBufferSize\", counter)(MeasurementUnit.information.megabytes)\n  val CONTAINER_POOL_ACTIVE_COUNT =\n    LogMarkerToken(containerPool, \"activeCount\", counter)(MeasurementUnit.none)\n  val CONTAINER_POOL_ACTIVE_SIZE =\n    LogMarkerToken(containerPool, \"activeSize\", counter)(MeasurementUnit.information.megabytes)\n  val CONTAINER_POOL_PREWARM_COUNT =\n    LogMarkerToken(containerPool, \"prewarmCount\", counter)(MeasurementUnit.none)\n  val CONTAINER_POOL_PREWARM_SIZE =\n    LogMarkerToken(containerPool, \"prewarmSize\", counter)(MeasurementUnit.information.megabytes)\n  val CONTAINER_POOL_IDLES_COUNT =\n    LogMarkerToken(containerPool, \"idlesCount\", counter)(MeasurementUnit.none)\n  def CONTAINER_POOL_PREWARM_COLDSTART(memory: String, kind: String) =\n    LogMarkerToken(containerPool, \"prewarmColdstart\", counter, None, Map(\"memory\" -> memory, \"kind\" -> kind))(\n      MeasurementUnit.none)\n  def CONTAINER_POOL_PREWARM_EXPIRED(memory: String, kind: String) =\n    LogMarkerToken(containerPool, \"prewarmExpired\", counter, None, Map(\"memory\" -> memory, \"kind\" -> kind))(\n      MeasurementUnit.none)\n  val CONTAINER_POOL_IDLES_SIZE =\n    LogMarkerToken(containerPool, \"idlesSize\", counter)(MeasurementUnit.information.megabytes)\n\n  val INVOKER_TOTALMEM_BLACKBOX = LogMarkerToken(loadbalancer, \"totalCapacityBlackBox\", counter)(MeasurementUnit.none)\n  val INVOKER_TOTALMEM_MANAGED = LogMarkerToken(loadbalancer, \"totalCapacityManaged\", counter)(MeasurementUnit.none)\n\n  val HEALTHY_INVOKER_MANAGED =\n    LogMarkerToken(loadbalancer, \"totalHealthyInvokerManaged\", counter)(MeasurementUnit.none)\n  val UNHEALTHY_INVOKER_MANAGED =\n    LogMarkerToken(loadbalancer, \"totalUnhealthyInvokerManaged\", counter)(MeasurementUnit.none)\n  val UNRESPONSIVE_INVOKER_MANAGED =\n    LogMarkerToken(loadbalancer, \"totalUnresponsiveInvokerManaged\", counter)(MeasurementUnit.none)\n  val OFFLINE_INVOKER_MANAGED =\n    LogMarkerToken(loadbalancer, \"totalOfflineInvokerManaged\", counter)(MeasurementUnit.none)\n\n  val HEALTHY_INVOKER_BLACKBOX =\n    LogMarkerToken(loadbalancer, \"totalHealthyInvokerBlackBox\", counter)(MeasurementUnit.none)\n  val UNHEALTHY_INVOKER_BLACKBOX =\n    LogMarkerToken(loadbalancer, \"totalUnhealthyInvokerBlackBox\", counter)(MeasurementUnit.none)\n  val UNRESPONSIVE_INVOKER_BLACKBOX =\n    LogMarkerToken(loadbalancer, \"totalUnresponsiveInvokerBlackBox\", counter)(MeasurementUnit.none)\n  val OFFLINE_INVOKER_BLACKBOX =\n    LogMarkerToken(loadbalancer, \"totalOfflineInvokerBlackBox\", counter)(MeasurementUnit.none)\n\n  val HEALTHY_INVOKERS =\n    LogMarkerToken(loadbalancer, \"totalHealthyInvoker\", counter)(MeasurementUnit.none)\n  val UNHEALTHY_INVOKERS =\n    LogMarkerToken(loadbalancer, \"totalUnhealthyInvoker\", counter)(MeasurementUnit.none)\n  val OFFLINE_INVOKERS =\n    LogMarkerToken(loadbalancer, \"totalOfflineInvoker\", counter)(MeasurementUnit.none)\n\n  val INVOKER_TOTALMEM = LogMarkerToken(loadbalancer, \"totalCapacity\", counter)(MeasurementUnit.none)\n\n  // Kafka related markers\n  def KAFKA_QUEUE(topic: String) =\n    if (TransactionId.metricsKamonTags)\n      LogMarkerToken(kafka, \"topic\", counter, None, Map(\"topic\" -> topic))(MeasurementUnit.none)\n    else LogMarkerToken(kafka, topic, counter)(MeasurementUnit.none)\n  def KAFKA_MESSAGE_DELAY(topic: String) =\n    if (TransactionId.metricsKamonTags)\n      LogMarkerToken(kafka, \"topic\", start, Some(\"delay\"), Map(\"topic\" -> topic))(MeasurementUnit.time.milliseconds)\n    else LogMarkerToken(kafka, topic, start, Some(\"delay\"))(MeasurementUnit.time.milliseconds)\n\n  // Time that is needed to produce message in kafka\n  val SCHEDULER_KAFKA = LogMarkerToken(scheduler, kafka, start)(MeasurementUnit.time.milliseconds)\n  val SCHEDULER_KAFKA_WAIT_TIME =\n    LogMarkerToken(scheduler, \"kafkaWaitTime\", counter)(MeasurementUnit.time.milliseconds)\n  def SCHEDULER_WAIT_TIME(actionWithVersion: String, actionWithoutVersion: String) =\n    LogMarkerToken(scheduler, \"waitTime\", counter, Some(actionWithoutVersion), Map(\"action\" -> actionWithVersion))(\n      MeasurementUnit.time.milliseconds)\n\n  def SCHEDULER_KEEP_ALIVE(leaseId: Long) =\n    LogMarkerToken(scheduler, \"keepAlive\", counter, None, Map(\"leaseId\" -> leaseId.toString))(MeasurementUnit.none)\n  def SCHEDULER_QUEUE = LogMarkerToken(scheduler, \"queue\", counter)(MeasurementUnit.none)\n  def SCHEDULER_QUEUE_CREATE = LogMarkerToken(scheduler, \"queueCreate\", start)(MeasurementUnit.time.milliseconds)\n  def SCHEDULER_QUEUE_RECOVER = LogMarkerToken(scheduler, \"queueRecover\", start)(MeasurementUnit.time.milliseconds)\n  def SCHEDULER_QUEUE_UPDATE(reason: String) =\n    LogMarkerToken(scheduler, \"queueUpdate\", counter, None, Map(\"reason\" -> reason))(MeasurementUnit.none)\n  def SCHEDULER_QUEUE_WAITING_ACTIVATION(namespace: String, actionWithVersion: String, actionWithoutVersion: String) =\n    LogMarkerToken(\n      scheduler,\n      \"queueActivation\",\n      counter,\n      Some(actionWithoutVersion),\n      Map(\"namespace\" -> namespace, \"action\" -> actionWithVersion))(MeasurementUnit.none)\n  def SCHEDULER_QUEUE_NOT_PROCESSING(namespace: String, actionWithVersion: String, actionWithoutVersion: String) =\n    LogMarkerToken(\n      scheduler,\n      \"queueNotProcessing\",\n      counter,\n      Some(actionWithoutVersion),\n      Map(\"namespace\" -> namespace, \"action\" -> actionWithVersion))(MeasurementUnit.none)\n\n  /*\n   * General markers\n   */\n  val DATABASE_CACHE_HIT = LogMarkerToken(database, \"cacheHit\", counter)(MeasurementUnit.none)\n  val DATABASE_CACHE_MISS = LogMarkerToken(database, \"cacheMiss\", counter)(MeasurementUnit.none)\n  val DATABASE_SAVE =\n    LogMarkerToken(database, \"saveDocument\", start)(MeasurementUnit.time.milliseconds)\n  val DATABASE_BULK_SAVE =\n    LogMarkerToken(database, \"saveDocumentBulk\", start)(MeasurementUnit.time.milliseconds)\n  val DATABASE_DELETE =\n    LogMarkerToken(database, \"deleteDocument\", start)(MeasurementUnit.time.milliseconds)\n  val DATABASE_GET = LogMarkerToken(database, \"getDocument\", start)(MeasurementUnit.time.milliseconds)\n  val DATABASE_QUERY = LogMarkerToken(database, \"queryView\", start)(MeasurementUnit.time.milliseconds)\n  val DATABASE_ATT_GET =\n    LogMarkerToken(database, \"getDocumentAttachment\", start)(MeasurementUnit.time.milliseconds)\n  val DATABASE_ATT_SAVE =\n    LogMarkerToken(database, \"saveDocumentAttachment\", start)(MeasurementUnit.time.milliseconds)\n  val DATABASE_ATT_DELETE =\n    LogMarkerToken(database, \"deleteDocumentAttachment\", start)(MeasurementUnit.time.milliseconds)\n  val DATABASE_ATTS_DELETE =\n    LogMarkerToken(database, \"deleteDocumentAttachments\", start)(MeasurementUnit.time.milliseconds)\n  val DATABASE_BATCH_SIZE = LogMarkerToken(database, \"batchSize\", counter)(MeasurementUnit.none)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/Message.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport org.apache.openwhisk.core.entity.InvokerInstanceId\n\ncase object GracefulShutdown\ncase object Enable\n\n// States an Invoker can be in\nsealed trait InvokerState {\n  val asString: String\n  val isUsable: Boolean\n}\n\nobject InvokerState {\n  // Invokers in this state can be used to schedule workload to\n  sealed trait Usable extends InvokerState { val isUsable = true }\n  // No workload should be scheduled to invokers in this state\n  sealed trait Unusable extends InvokerState { val isUsable = false }\n\n  // A completely healthy invoker, pings arriving fine, no system errors\n  case object Healthy extends Usable { val asString = \"up\" }\n  // The invoker can not create a container\n  case object Unhealthy extends Unusable { val asString = \"unhealthy\" }\n  // Pings are arriving fine, the invoker does not respond with active-acks in the expected time though\n  case object Unresponsive extends Unusable { val asString = \"unresponsive\" }\n  // The invoker is down\n  case object Offline extends Unusable { val asString = \"down\" }\n}\n\n/**\n * Describes an abstract invoker. An invoker is a local container pool manager that\n * is in charge of the container life cycle management.\n *\n * @param id a unique instance identifier for the invoker\n * @param status it status (healthy, unhealthy, unresponsive, offline)\n */\ncase class InvokerHealth(id: InvokerInstanceId, status: InvokerState) {\n  override def equals(obj: scala.Any): Boolean = obj match {\n    case that: InvokerHealth => that.id == this.id && that.status == this.status\n    case _                   => false\n  }\n\n  override def toString = s\"InvokerHealth($id, $status)\"\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/NestedSemaphore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport scala.collection.concurrent.TrieMap\n\n/**\n * A Semaphore that coordinates the memory (ForcibleSemaphore) and concurrency (ResizableSemaphore) where\n * - for invocations when maxConcurrent == 1, delegate to super\n * - for invocations that cause acquire on memory slots, also acquire concurrency slots, and do it atomically\n * @param memoryPermits\n * @tparam T\n */\nclass NestedSemaphore[T](memoryPermits: Int) extends ForcibleSemaphore(memoryPermits) {\n  private val actionConcurrentSlotsMap = TrieMap.empty[T, ResizableSemaphore] //one key per action; resized per container\n\n  final def tryAcquireConcurrent(actionid: T, maxConcurrent: Int, memoryPermits: Int): Boolean = {\n\n    if (maxConcurrent == 1) {\n      super.tryAcquire(memoryPermits)\n    } else {\n      tryOrForceAcquireConcurrent(actionid, maxConcurrent, memoryPermits, false)\n    }\n  }\n\n  /**\n   * Coordinated permit acquisition:\n   * - first try to acquire concurrency slot\n   * - then try to acquire lock for this action\n   * - within the lock:\n   *     - try to acquire concurrency slot (double check)\n   *     - try to acquire memory slot\n   *     - if memory slot acquired, release concurrency slots\n   * - release the lock\n   * - if neither concurrency slot nor memory slot acquired, return false\n   * @param actionid\n   * @param maxConcurrent\n   * @param memoryPermits\n   * @param force\n   * @return\n   */\n  private def tryOrForceAcquireConcurrent(actionid: T,\n                                          maxConcurrent: Int,\n                                          memoryPermits: Int,\n                                          force: Boolean): Boolean = {\n    val concurrentSlots = actionConcurrentSlotsMap\n      .getOrElseUpdate(actionid, new ResizableSemaphore(0, maxConcurrent))\n    if (concurrentSlots.tryAcquire(1)) {\n      true\n    } else {\n      // with synchronized:\n      concurrentSlots.synchronized {\n        if (concurrentSlots.tryAcquire(1)) {\n          true\n        } else if (force) {\n          super.forceAcquire(memoryPermits)\n          concurrentSlots.release(maxConcurrent - 1, false)\n          true\n        } else if (super.tryAcquire(memoryPermits)) {\n          concurrentSlots.release(maxConcurrent - 1, false)\n          true\n        } else {\n          false\n        }\n      }\n    }\n  }\n\n  def forceAcquireConcurrent(actionid: T, maxConcurrent: Int, memoryPermits: Int): Unit = {\n    require(memoryPermits > 0, \"cannot force acquire negative or no permits\")\n    if (maxConcurrent == 1) {\n      super.forceAcquire(memoryPermits)\n    } else {\n      tryOrForceAcquireConcurrent(actionid, maxConcurrent, memoryPermits, true)\n    }\n  }\n\n  /**\n   * Releases the given amount of permits\n   *\n   * @param acquires the number of permits to release\n   */\n  def releaseConcurrent(actionid: T, maxConcurrent: Int, memoryPermits: Int): Unit = {\n    require(memoryPermits > 0, \"cannot release negative or no permits\")\n    if (maxConcurrent == 1) {\n      super.release(memoryPermits)\n    } else {\n      val concurrentSlots = actionConcurrentSlotsMap(actionid)\n      val (memoryRelease, actionRelease) = concurrentSlots.release(1, true)\n      //concurrent slots\n      if (memoryRelease) {\n        super.release(memoryPermits)\n      }\n      if (actionRelease) {\n        actionConcurrentSlotsMap.remove(actionid)\n      }\n    }\n  }\n  //for testing\n  def concurrentState = actionConcurrentSlotsMap.readOnlySnapshot()\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/Prometheus.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport org.apache.pekko.http.scaladsl.model.{ContentType, HttpCharsets, HttpEntity, MediaType}\nimport org.apache.pekko.http.scaladsl.server.Directives._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport kamon.Kamon\nimport kamon.prometheus.PrometheusReporter\n\nclass KamonPrometheus extends AutoCloseable {\n  private val reporter = new PrometheusReporter\n  private val v4: ContentType = ContentType.apply(\n    MediaType.textWithFixedCharset(\"plain\", HttpCharsets.`UTF-8`).withParams(Map(\"version\" -> \"0.0.4\")))\n  Kamon.registerModule(\"prometheus\", reporter)\n\n  def route: Route = path(\"metrics\") {\n    get {\n      encodeResponse {\n        complete(getReport())\n      }\n    }\n  }\n\n  private def getReport() = HttpEntity(v4, reporter.scrapeData().getBytes(UTF_8))\n\n  override def close(): Unit = reporter.stop()\n}\n\nobject MetricsRoute {\n  private val impl =\n    if (TransactionId.metricsKamon && TransactionId.metricConfig.prometheusEnabled) Some(new KamonPrometheus)\n    else None\n\n  def apply(): Route = impl.map(_.route).getOrElse(reject)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/ResizableSemaphore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.util.concurrent.atomic.AtomicInteger\nimport java.util.concurrent.locks.AbstractQueuedSynchronizer\nimport scala.annotation.tailrec\n\n/**\n * A Semaphore that has a specialized release process that optionally allows reduction of permits in batches.\n * When permit size after release is a factor of reductionSize, the release process will reset permits to state + 1 - reductionSize;\n * otherwise the release will reset permits to state + 1.\n * It also maintains an operationCount where a tryAquire + release is a single operation,\n * so that we can know once all operations are completed.\n * @param maxAllowed\n * @param reductionSize\n */\nclass ResizableSemaphore(maxAllowed: Int, reductionSize: Int) {\n  private val operationCount = new AtomicInteger(0)\n  class Sync extends AbstractQueuedSynchronizer {\n    setState(maxAllowed)\n\n    def permits: Int = getState\n\n    /** Try to release a permit and return whether or not that operation was successful. */\n    @tailrec\n    final def tryReleaseSharedWithResult(releases: Int): Boolean = {\n      val current = getState\n      val next2 = current + releases\n      val (next, reduced) = if (next2 % reductionSize == 0) {\n        (next2 - reductionSize, true)\n      } else {\n        (next2, false)\n      }\n      //next MIGHT be < current in case of reduction; this is OK!!!\n      if (compareAndSetState(current, next)) {\n        reduced\n      } else {\n        tryReleaseSharedWithResult(releases)\n      }\n    }\n\n    /**\n     * Try to acquire a permit and return whether or not that operation was successful. Requests may not finish in FIFO\n     * order, hence this method is not necessarily fair.\n     */\n    @tailrec\n    final def nonFairTryAcquireShared(acquires: Int): Int = {\n      val available = getState\n      val remaining = available - acquires\n      if (remaining < 0 || compareAndSetState(available, remaining)) {\n        remaining\n      } else {\n        nonFairTryAcquireShared(acquires)\n      }\n    }\n  }\n\n  val sync = new Sync\n\n  /**\n   * Acquires the given numbers of permits.\n   *\n   * @param acquires the number of permits to get\n   * @return `true`, iff the internal semaphore's number of permits is positive, `false` if negative\n   */\n  def tryAcquire(acquires: Int = 1): Boolean = {\n    require(acquires > 0, \"cannot acquire negative or no permits\")\n    if (sync.nonFairTryAcquireShared(acquires) >= 0) {\n      operationCount.incrementAndGet()\n      true\n    } else {\n      false\n    }\n  }\n\n  /**\n   * Releases the given amount of permits\n   *\n   * @param acquires the number of permits to release\n   * @return (releaseMemory, releaseAction) releaseMemory is true if concurrency count is a factor of reductionSize\n   *         releaseAction is true if the operationCount reaches 0\n   */\n  def release(acquires: Int = 1, opComplete: Boolean): (Boolean, Boolean) = {\n    require(acquires > 0, \"cannot release negative or no permits\")\n    //release always succeeds, so we can always adjust the operationCount\n    val releaseAction = if (opComplete) { // an operation completion\n      operationCount.decrementAndGet() == 0\n    } else { //otherwise an allocation + operation initialization\n      operationCount.incrementAndGet() == 0\n    }\n    (sync.tryReleaseSharedWithResult(acquires), releaseAction)\n  }\n\n  /** Returns the number of currently available permits. Possibly negative. */\n  def availablePermits: Int = sync.permits\n\n  //for testing\n  def counter = operationCount.get()\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/RingBuffer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport org.apache.commons.collections.buffer.CircularFifoBuffer\n\nobject RingBuffer {\n  def apply[T](size: Int) = new RingBuffer[T](size)\n}\n\nclass RingBuffer[T](size: Int) {\n  private val inner = new CircularFifoBuffer(size)\n\n  def add(el: T) = inner.add(el)\n\n  def toList = inner.toArray().asInstanceOf[Array[T]].toList\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/Scheduler.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\n\nimport org.apache.pekko.actor.Actor\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.actor.Cancellable\nimport org.apache.pekko.actor.Props\n\n/**\n * Scheduler utility functions to execute tasks in a repetitive way with controllable behavior\n * even for asynchronous tasks.\n */\nobject Scheduler {\n  case object WorkOnceNow\n  private case object ScheduledWork\n\n  /**\n   * Sets up an Actor to send itself a message to mimic schedulers behavior in a more controllable way.\n   *\n   * @param interval the time to wait between two runs\n   * @param alwaysWait always wait for the given amount of time or calculate elapsed time to wait\n   * @param closure the closure to be executed\n   */\n  private class Worker(initialDelay: FiniteDuration,\n                       interval: FiniteDuration,\n                       alwaysWait: Boolean,\n                       name: String,\n                       closure: () => Future[Any])(implicit logging: Logging, transid: TransactionId)\n      extends Actor {\n    implicit val ec = context.dispatcher\n\n    var lastSchedule: Option[Cancellable] = None\n\n    override def preStart() = {\n      if (initialDelay != Duration.Zero) {\n        lastSchedule = Some(context.system.scheduler.scheduleOnce(initialDelay, self, ScheduledWork))\n      } else {\n        self ! ScheduledWork\n      }\n    }\n    override def postStop() = {\n      logging.debug(this, s\"$name shutdown\")\n      lastSchedule.foreach(_.cancel())\n    }\n\n    def receive = {\n      case WorkOnceNow => Try(closure())\n\n      case ScheduledWork =>\n        val deadline = interval.fromNow\n        Try(closure()) match {\n          case Success(result) =>\n            result onComplete { _ =>\n              val timeToWait = if (alwaysWait) interval else deadline.timeLeft.max(Duration.Zero)\n              // context might be null here if a PoisonPill is sent while doing computations\n              lastSchedule = Option(context).map(_.system.scheduler.scheduleOnce(timeToWait, self, ScheduledWork))\n            }\n\n          case Failure(e) =>\n            logging.error(name, s\"halted because ${e.getMessage}\")\n        }\n    }\n  }\n\n  /**\n   * Schedules a closure to run continuously scheduled, with at least the given interval in between runs.\n   * This waits until the Future of the closure has finished, ignores its result and waits for at most the\n   * time specified. If the closure took as long or longer than the time specified, the next iteration\n   * is immediately fired.\n   *\n   * @param interval the time to wait at most between two runs of the closure\n   * @param initialDelay optionally delay the first scheduled iteration by given duration\n   * @param f the function to run\n   */\n  def scheduleWaitAtMost(interval: FiniteDuration,\n                         initialDelay: FiniteDuration = Duration.Zero,\n                         name: String = \"Scheduler\")(f: () => Future[Any])(implicit system: ActorSystem,\n                                                                           logging: Logging,\n                                                                           transid: TransactionId =\n                                                                             TransactionId.unknown) = {\n    require(interval > Duration.Zero)\n    system.actorOf(Props(new Worker(initialDelay, interval, false, name, f)))\n  }\n\n  /**\n   * Schedules a closure to run continuously scheduled, with at least the given interval in between runs.\n   * This waits until the Future of the closure has finished, ignores its result and then waits for the\n   * given interval.\n   *\n   * @param interval the time to wait between two runs of the closure\n   * @param initialDelay optionally delay the first scheduled iteration by given duration\n   * @param f the function to run\n   */\n  def scheduleWaitAtLeast(interval: FiniteDuration,\n                          initialDelay: FiniteDuration = Duration.Zero,\n                          name: String = \"Scheduler\")(f: () => Future[Any])(implicit system: ActorSystem,\n                                                                            logging: Logging,\n                                                                            transid: TransactionId =\n                                                                              TransactionId.unknown) = {\n    require(interval > Duration.Zero)\n    system.actorOf(Props(new Worker(initialDelay, interval, true, name, f)))\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/TransactionId.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.time.{Clock, Duration, Instant}\n\nimport org.apache.pekko.event.Logging.{DebugLevel, InfoLevel, LogLevel, WarningLevel}\nimport org.apache.pekko.http.scaladsl.model.headers.RawHeader\nimport spray.json._\nimport org.apache.openwhisk.core.ConfigKeys\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.common.tracing.WhiskTracerProvider\nimport org.apache.openwhisk.common.WhiskInstants._\n\nimport scala.util.Try\n\n/**\n * A transaction id for tracking operations in the system that are specific to a request.\n * An instance of TransactionId is implicitly received by all logging methods. The actual\n * metadata is stored indirectly in the referenced meta object.\n */\ncase class TransactionId private (meta: TransactionMetadata) extends AnyVal {\n  def root = findRoot(meta)\n  def id = meta.id\n  override def toString = meta.toString\n\n  def toHeader = RawHeader(TransactionId.generatorConfig.header, meta.id)\n\n  /**\n   * Method to count events.\n   *\n   * @param from Reference, where the method was called from.\n   * @param marker A LogMarkerToken. They are defined in <code>LoggingMarkers</code>.\n   * @param message An additional message to be written into the log, together with the other information.\n   * @param logLevel The Loglevel, the message should have. Default is <code>InfoLevel</code>.\n   */\n  def mark(from: AnyRef, marker: LogMarkerToken, message: => String = \"\", logLevel: LogLevel = DebugLevel)(\n    implicit logging: Logging) = {\n\n    if (TransactionId.metricsLog) {\n      // marker received with a debug level will be emitted on info level\n      logging.emit(InfoLevel, this, from, createMessageWithMarker(message, LogMarker(marker, deltaToStart)))\n    } else {\n      logging.emit(logLevel, this, from, message)\n    }\n\n    MetricEmitter.emitCounterMetric(marker)\n\n  }\n\n  /**\n   * Method to start taking time of an action in the code. It returns a <code>StartMarker</code> which has to be\n   * passed into the <code>finished</code>-method.\n   *\n   * @param from Reference, where the method was called from.\n   * @param marker A LogMarkerToken. They are defined in <code>LoggingMarkers</code>.\n   * @param message An additional message to be written into the log, together with the other information.\n   * @param logLevel The Loglevel, the message should have. Default is <code>InfoLevel</code>.\n   *\n   * @return startMarker that has to be passed to the finished or failed method to calculate the time difference.\n   */\n  def started(from: AnyRef, marker: LogMarkerToken, message: => String = \"\", logLevel: LogLevel = DebugLevel)(\n    implicit logging: Logging): StartMarker = {\n\n    if (TransactionId.metricsLog) {\n      // marker received with a debug level will be emitted on info level\n      logging.emit(InfoLevel, this, from, createMessageWithMarker(message, LogMarker(marker, deltaToStart)))\n    } else {\n      logging.emit(logLevel, this, from, message)\n    }\n\n    MetricEmitter.emitCounterMetric(marker)\n\n    //tracing support\n    WhiskTracerProvider.tracer.startSpan(marker, this)\n    StartMarker(Instant.now.inMills, marker)\n  }\n\n  /**\n   * Method to stop taking time of an action in the code. The time the method used will be written into a log message.\n   *\n   * @param from Reference, where the method was called from.\n   * @param startMarker <code>StartMarker</code> returned by a <code>starting</code> method.\n   * @param message An additional message to be written into the log, together with the other information.\n   * @param logLevel The Loglevel, the message should have. Default is <code>InfoLevel</code>.\n   * @param endTime Manually set the timestamp of the end. By default it is NOW.\n   */\n  def finished(from: AnyRef,\n               startMarker: StartMarker,\n               message: => String = \"\",\n               logLevel: LogLevel = DebugLevel,\n               endTime: Instant = Instant.now(Clock.systemUTC))(implicit logging: Logging) = {\n\n    val endMarker = startMarker.startMarker.asFinish\n    val deltaToEnd = deltaToMarker(startMarker, endTime)\n\n    if (TransactionId.metricsLog) {\n      logging.emit(\n        InfoLevel,\n        this,\n        from,\n        createMessageWithMarker(\n          if (logLevel <= InfoLevel) message else \"\",\n          LogMarker(endMarker, deltaToStart, Some(deltaToEnd))))\n    } else {\n      logging.emit(logLevel, this, from, message)\n    }\n\n    MetricEmitter.emitHistogramMetric(endMarker, deltaToEnd)\n\n    //tracing support\n    WhiskTracerProvider.tracer.finishSpan(this)\n  }\n\n  /**\n   * Method to stop taking time of an action in the code that failed. The time the method used will be written into a log message.\n   *\n   * @param from Reference, where the method was called from.\n   * @param startMarker <code>StartMarker</code> returned by a <code>starting</code> method.\n   * @param message An additional message to be written into the log, together with the other information.\n   * @param logLevel The <code>LogLevel</code> the message should have. Default is <code>WarningLevel</code>.\n   */\n  def failed(from: AnyRef, startMarker: StartMarker, message: => String = \"\", logLevel: LogLevel = WarningLevel)(\n    implicit logging: Logging) = {\n\n    val endMarker = startMarker.startMarker.asError\n    val deltaToEnd = deltaToMarker(startMarker)\n\n    if (TransactionId.metricsLog) {\n      logging.emit(\n        logLevel,\n        this,\n        from,\n        createMessageWithMarker(message, LogMarker(endMarker, deltaToStart, Some(deltaToEnd))))\n    } else {\n      logging.emit(logLevel, this, from, message)\n    }\n\n    MetricEmitter.emitHistogramMetric(endMarker, deltaToEnd)\n    MetricEmitter.emitCounterMetric(endMarker)\n\n    //tracing support\n    WhiskTracerProvider.tracer.error(this, message)\n  }\n\n  /**\n   * Calculates the time between now and the beginning of the transaction.\n   */\n  def deltaToStart = Duration.between(meta.start, Instant.now(Clock.systemUTC)).toMillis\n\n  /**\n   * Calculates the time between now and the startMarker that was returned by <code>starting</code>.\n   *\n   * @param startMarker <code>StartMarker</code> returned by a <code>starting</code> method.\n   * @param endTime Manually set the endtime. By default it is NOW.\n   */\n  def deltaToMarker(startMarker: StartMarker, endTime: Instant = Instant.now(Clock.systemUTC)) =\n    Duration.between(startMarker.start, endTime).toMillis\n\n  def hasParent = meta.parent.isDefined\n\n  /**\n   * Formats log message to include marker.\n   *\n   * @param message: The log message without the marker\n   * @param marker: The marker to add to the message\n   */\n  private def createMessageWithMarker(message: String, marker: LogMarker): String = s\"$message $marker\"\n\n  /**\n   * Find root transaction metadata\n   */\n  private def findRoot(meta: TransactionMetadata): TransactionMetadata =\n    meta.parent match {\n      case Some(parent) => findRoot(parent)\n      case _            => meta\n    }\n\n  def serialize = TransactionId.serdes.write(this).compactPrint\n}\n\n/**\n * The StartMarker which includes the <code>LogMarkerToken</code> and the start-time.\n *\n * @param start the time when the startMarker was set\n * @param startMarker the LogMarkerToken which defines the start event\n */\ncase class StartMarker(start: Instant, startMarker: LogMarkerToken)\n\n/**\n * The transaction metadata encapsulates important properties about a transaction.\n *\n * @param id the transaction identifier; it is positive for client requests,\n *           negative for system operation and zero when originator is not known\n * @param start the timestamp when the request processing commenced\n * @param extraLogging enables logging, if set to true\n */\nprotected case class TransactionMetadata(id: String,\n                                         start: Instant,\n                                         extraLogging: Boolean = false,\n                                         parent: Option[TransactionMetadata] = None) {\n  override def toString = s\"#tid_$id\"\n}\n\ncase class MetricConfig(prometheusEnabled: Boolean,\n                        kamonEnabled: Boolean,\n                        kamonTagsEnabled: Boolean,\n                        logsEnabled: Boolean)\nobject TransactionId {\n  val metricConfig = loadConfigOrThrow[MetricConfig](ConfigKeys.metrics)\n\n  // get the metric parameters directly from the environment since WhiskConfig can not be instantiated here\n  val metricsKamon: Boolean = metricConfig.kamonEnabled\n  val metricsKamonTags: Boolean = metricConfig.kamonTagsEnabled\n  val metricsLog: Boolean = metricConfig.logsEnabled\n\n  val generatorConfig = loadConfigOrThrow[TransactionGeneratorConfig](ConfigKeys.transactions)\n\n  val systemPrefix = \"sid_\"\n\n  val unknown = TransactionId(systemPrefix + \"unknown\")\n  val testing = TransactionId(systemPrefix + \"testing\") // Common id for for unit testing\n  val invoker = TransactionId(systemPrefix + \"invoker\") // Invoker startup/shutdown or GC activity\n  val invokerHealthManager = TransactionId(systemPrefix + \"invokerHealthManager\") // Invoker startup/shutdown or GC activity\n  def invokerHealthActivation = TransactionId(systemPrefix + \"invokerHealthActivation\") // Invoker health activation\n  val invokerWarmup = TransactionId(systemPrefix + \"invokerWarmup\") // Invoker warmup thread that makes stem-cell containers\n  val invokerColdstart = TransactionId(systemPrefix + \"invokerColdstart\") //Invoker cold start thread\n  val invokerNanny = TransactionId(systemPrefix + \"invokerNanny\") // Invoker nanny thread\n  val dispatcher = TransactionId(systemPrefix + \"dispatcher\") // Kafka message dispatcher\n  val loadbalancer = TransactionId(systemPrefix + \"loadbalancer\") // Loadbalancer thread\n  val invokerHealth = TransactionId(systemPrefix + \"invokerHealth\") // Invoker supervision\n  val controller = TransactionId(systemPrefix + \"controller\") // Controller startup\n  val dbBatcher = TransactionId(systemPrefix + \"dbBatcher\") // Database batcher\n  val actionHealthPing = TransactionId(systemPrefix + \"actionHealth\")\n  var containerCreation = TransactionId(systemPrefix + \"containerCreation\")\n  var containerDeletion = TransactionId(systemPrefix + \"containerDeletion\")\n  val warmUp = TransactionId(systemPrefix + \"warmUp\")\n\n  private val dict = ('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9')\n\n  def apply(tid: String, extraLogging: Boolean = false): TransactionId = {\n    val now = Instant.now(Clock.systemUTC()).inMills\n    TransactionId(TransactionMetadata(tid, now, extraLogging))\n  }\n\n  def childOf(parentTid: TransactionId): TransactionId = {\n    val now = Instant.now(Clock.systemUTC()).inMills\n    val tid = generateTid()\n    TransactionId(TransactionMetadata(tid, now, parentTid.meta.extraLogging, Some(parentTid.meta)))\n  }\n\n  def generateTid(): String = {\n    val sb = new StringBuilder\n    for (_ <- 1 to 32) sb.append(dict(util.Random.nextInt(dict.length)))\n    sb.toString\n  }\n\n  implicit val serdes = new RootJsonFormat[TransactionId] {\n\n    private def writeMetadata(meta: TransactionMetadata): JsArray = {\n      val base = Vector(JsString(meta.id), JsNumber(meta.start.toEpochMilli))\n      val extraLogging = if (meta.extraLogging) Vector(JsBoolean(meta.extraLogging)) else Vector.empty\n      val parent = meta.parent match {\n        case Some(p) => Vector(writeMetadata(p))\n        case _       => Vector.empty\n      }\n      JsArray(base ++ extraLogging ++ parent)\n    }\n\n    private def readMetadata(value: JsValue): Option[TransactionMetadata] = {\n      Try {\n        value match {\n          case JsArray(Vector(JsString(id), JsNumber(start))) =>\n            Some(TransactionMetadata(id, Instant.ofEpochMilli(start.longValue), false))\n          case JsArray(Vector(JsString(id), JsNumber(start), JsBoolean(extraLogging))) =>\n            Some(TransactionMetadata(id, Instant.ofEpochMilli(start.longValue), extraLogging))\n          case JsArray(Vector(JsString(id), JsNumber(start), JsBoolean(extraLogging), parent)) =>\n            Some(TransactionMetadata(id, Instant.ofEpochMilli(start.longValue), extraLogging, readMetadata(parent)))\n          case JsArray(Vector(JsString(id), JsNumber(start), parent)) =>\n            Some(TransactionMetadata(id, Instant.ofEpochMilli(start.longValue), false, readMetadata(parent)))\n        }\n      } getOrElse Option.empty\n    }\n\n    def write(t: TransactionId): JsArray = writeMetadata(t.meta)\n    def read(value: JsValue): TransactionId = readMetadata(value).map(meta => TransactionId(meta)).getOrElse(unknown)\n  }\n}\n\ncase class TransactionGeneratorConfig(header: String) {\n  val lowerCaseHeader = header.toLowerCase //to cache the lowercase version of the header name\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/UserEvents.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.connector.{EventMessage, MessageProducer}\n\nobject UserEvents {\n\n  case class UserEventsConfig(enabled: Boolean)\n\n  val enabled = loadConfigOrThrow[UserEventsConfig](ConfigKeys.userEvents).enabled\n\n  val userEventTopicPrefix = loadConfigOrThrow[String](ConfigKeys.kafkaTopicsUserEventPrefix)\n\n  def send(producer: MessageProducer, em: => EventMessage) = {\n    if (enabled) {\n      producer.send(userEventTopicPrefix + \"events\", em)\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/WhiskInstants.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.time.Instant\nimport java.time.temporal.ChronoUnit\n\n/**\n * JDK 11 Instant uses nano second precision by default. However OpenWhisk usage of Instant in\n * many cases involves storing the timestamp in database which uses milli second precision.\n *\n * To ensure consistency below utilities can be used to truncate the Instant to millis\n */\ntrait WhiskInstants {\n\n  implicit class InstantImplicits(i: Instant) {\n    def inMills = i.truncatedTo(ChronoUnit.MILLIS)\n  }\n\n  def nowInMillis(): Instant = Instant.now.truncatedTo(ChronoUnit.MILLIS)\n\n}\n\nobject WhiskInstants extends WhiskInstants\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/time/Clock.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common.time\n\nimport java.time.Instant\n\ntrait Clock {\n  def now(): Instant\n}\n\nobject SystemClock extends Clock {\n  def now() = Instant.now()\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/common/tracing/OpenTracingProvider.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common.tracing\n\nimport java.util.concurrent.TimeUnit\n\nimport brave.Tracing\nimport brave.opentracing.BraveTracer\nimport brave.sampler.Sampler\nimport com.github.benmanes.caffeine.cache.{Caffeine, Ticker}\nimport io.opentracing.propagation.{Format, TextMapExtractAdapter, TextMapInjectAdapter}\nimport io.opentracing.util.GlobalTracer\nimport io.opentracing.{Span, SpanContext, Tracer}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.common.{LogMarkerToken, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport zipkin2.reporter.okhttp3.OkHttpSender\nimport zipkin2.reporter.{AsyncReporter, Sender}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.concurrent.duration.Duration\n\n/**\n * OpenTracing based implementation for tracing\n */\nclass OpenTracer(val tracer: Tracer, tracingConfig: TracingConfig, ticker: Ticker = SystemTicker) extends WhiskTracer {\n  val spanMap = configureCache[String, List[Span]]()\n  val contextMap = configureCache[String, SpanContext]()\n\n  /**\n   * Start a Trace for given service.\n   *\n   * @param transactionId transactionId to which this Trace belongs.\n   * @return TracedRequest which provides details about current service being traced.\n   */\n  override def startSpan(logMarker: LogMarkerToken, transactionId: TransactionId): Unit = {\n    //initialize list for this transactionId\n    val spanList = spanMap.getOrElse(transactionId.meta.id, Nil)\n\n    val spanBuilder = tracer\n      .buildSpan(logMarker.action)\n      .withTag(\"transactionId\", transactionId.meta.id)\n\n    val active = spanList match {\n      case Nil =>\n        //Check if any active context then resume from that else create a fresh span\n        contextMap\n          .get(transactionId.meta.id)\n          .map(spanBuilder.asChildOf)\n          .getOrElse(spanBuilder.ignoreActiveSpan())\n          .startActive(true)\n          .span()\n      case head :: _ =>\n        //Create a child span of current head\n        spanBuilder.asChildOf(head).startActive(true).span()\n    }\n    //add active span to list\n    spanMap.put(transactionId.meta.id, active :: spanList)\n  }\n\n  /**\n   * Finish a Trace associated with given transactionId.\n   *\n   * @param transactionId\n   */\n  override def finishSpan(transactionId: TransactionId): Unit = {\n    clear(transactionId, withErrorMessage = None)\n  }\n\n  /**\n   * Register error\n   *\n   * @param transactionId\n   */\n  override def error(transactionId: TransactionId, message: => String): Unit = {\n    clear(transactionId, withErrorMessage = Some(message))\n  }\n\n  /**\n   * Get the current TraceContext which can be used for downstream services\n   *\n   * @param transactionId\n   * @return\n   */\n  override def getTraceContext(transactionId: TransactionId): Option[Map[String, String]] = {\n    spanMap\n      .get(transactionId.meta.id)\n      .flatMap(_.headOption)\n      .map { span =>\n        val map = mutable.Map.empty[String, String]\n        tracer.inject(span.context(), Format.Builtin.TEXT_MAP, new TextMapInjectAdapter(map.asJava))\n        map.toMap\n      }\n  }\n\n  /**\n   * Get the current TraceContext which can be used for downstream services\n   *\n   * @param transactionId\n   * @return\n   */\n  override def setTraceContext(transactionId: TransactionId, context: Option[Map[String, String]]) = {\n    context.foreach { scalaMap =>\n      val ctx: SpanContext = tracer.extract(Format.Builtin.TEXT_MAP, new TextMapExtractAdapter(scalaMap.asJava))\n      contextMap.put(transactionId.meta.id, ctx)\n    }\n  }\n\n  private def clear(transactionId: TransactionId, withErrorMessage: Option[String]): Unit = {\n    spanMap.get(transactionId.meta.id).foreach {\n      case head :: Nil =>\n        withErrorMessage.foreach(setErrorTags(head, _))\n        head.finish()\n        spanMap.remove(transactionId.meta.id)\n        contextMap.remove(transactionId.meta.id)\n      case head :: tail =>\n        withErrorMessage.foreach(setErrorTags(head, _))\n        head.finish()\n        spanMap.put(transactionId.meta.id, tail)\n      case Nil =>\n    }\n  }\n\n  private def setErrorTags(span: Span, message: => String): Unit = {\n    span.setTag(\"error\", true)\n    span.setTag(\"message\", message)\n  }\n\n  private def configureCache[T, R](): collection.concurrent.Map[T, R] =\n    Caffeine\n      .newBuilder()\n      .ticker(ticker)\n      .expireAfterAccess(tracingConfig.cacheExpiry.toSeconds, TimeUnit.SECONDS)\n      .build()\n      .asMap()\n      .asScala\n      .asInstanceOf[collection.concurrent.Map[T, R]]\n}\n\ntrait WhiskTracer {\n  def startSpan(logMarker: LogMarkerToken, transactionId: TransactionId): Unit = {}\n  def finishSpan(transactionId: TransactionId): Unit = {}\n  def error(transactionId: TransactionId, message: => String): Unit = {}\n  def getTraceContext(transactionId: TransactionId): Option[Map[String, String]] = None\n  def setTraceContext(transactionId: TransactionId, context: Option[Map[String, String]]): Unit = {}\n}\n\nobject WhiskTracerProvider {\n  val tracingConfig = loadConfigOrThrow[TracingConfig](ConfigKeys.tracing)\n\n  val tracer: WhiskTracer = createTracer(tracingConfig)\n\n  private def createTracer(tracingConfig: TracingConfig): WhiskTracer = {\n\n    tracingConfig.zipkin match {\n      case Some(zipkinConfig) => {\n        if (!GlobalTracer.isRegistered) {\n          val sender: Sender = OkHttpSender.create(zipkinConfig.generateUrl)\n          val spanReporter = AsyncReporter.create(sender)\n          val braveTracing = Tracing\n            .newBuilder()\n            .localServiceName(tracingConfig.component)\n            .spanReporter(spanReporter)\n            .sampler(Sampler.create(zipkinConfig.sampleRate.toFloat))\n            .build()\n\n          //register with OpenTracing\n          GlobalTracer.register(BraveTracer.create(braveTracing))\n\n          sys.addShutdownHook({ spanReporter.close() })\n        }\n      }\n      case None =>\n    }\n\n    if (GlobalTracer.isRegistered)\n      new OpenTracer(GlobalTracer.get(), tracingConfig)\n    else\n      NoopTracer\n  }\n}\n\nprivate object NoopTracer extends WhiskTracer\ncase class TracingConfig(component: String, cacheExpiry: Duration, zipkin: Option[ZipkinConfig] = None)\ncase class ZipkinConfig(url: String, sampleRate: String) {\n  def generateUrl = s\"$url/api/v2/spans\"\n}\nobject SystemTicker extends Ticker {\n  override def read() = {\n    System.nanoTime()\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/connector/kafka/KafkaConsumerConnector.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.connector.kafka\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer}\nimport org.apache.kafka.common.TopicPartition\nimport org.apache.kafka.common.errors.{RetriableException, WakeupException}\nimport org.apache.kafka.common.serialization.ByteArrayDeserializer\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, MetricEmitter, Scheduler}\nimport org.apache.openwhisk.connector.kafka.KafkaConfiguration._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.connector.MessageConsumer\nimport org.apache.openwhisk.utils.Exceptions\nimport org.apache.openwhisk.utils.TimeHelpers._\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.duration._\nimport scala.concurrent.{blocking, ExecutionContext, Future}\nimport scala.util.Failure\n\ncase class KafkaConsumerConfig(sessionTimeoutMs: Long)\n\nclass KafkaConsumerConnector(\n  kafkahost: String,\n  groupid: String,\n  topic: String,\n  override val maxPeek: Int = Int.MaxValue)(implicit logging: Logging, actorSystem: ActorSystem)\n    extends MessageConsumer\n    with Exceptions {\n\n  implicit val ec: ExecutionContext = actorSystem.dispatcher\n  private val gracefulWaitTime = 100.milliseconds\n\n  // The consumer is generally configured via getProps. This configuration only loads values necessary for \"outer\"\n  // logic, like the wakeup timer.\n  private val cfg = loadConfigOrThrow[KafkaConsumerConfig](ConfigKeys.kafkaConsumer)\n\n  // Currently consumed offset, is used to calculate the topic lag.\n  // It is updated from one thread in \"peek\", no concurrent data structure is necessary\n  // Note: Currently, this value used for metric reporting will not be accurate if using a multi-partition topic.\n  private var offset: Long = 0\n\n  // Markers for metrics, initialized only once\n  private val queueMetric = LoggingMarkers.KAFKA_QUEUE(topic)\n  private val delayMetric = LoggingMarkers.KAFKA_MESSAGE_DELAY(topic)\n\n  /**\n   * Long poll for messages. Method returns once message are available but no later than given\n   * duration.\n   *\n   * @param duration the maximum duration for the long poll\n   */\n  override def peek(duration: FiniteDuration = 500.milliseconds,\n                    retry: Int = 3): Iterable[(String, Int, Long, Array[Byte])] = {\n\n    // poll can be infinitely blocked in edge-cases, so we need to wakeup explicitly.\n    val wakeUpTask = actorSystem.scheduler.scheduleOnce(cfg.sessionTimeoutMs.milliseconds + 1.second) {\n      consumer.wakeup()\n      logging.info(this, s\"woke up consumer for topic '$topic'\")\n    }\n\n    try {\n      val response = synchronized(consumer.poll(duration)).asScala\n\n      // Cancel the scheduled wake-up task immediately.\n      wakeUpTask.cancel()\n\n      val now = System.currentTimeMillis\n\n      response.lastOption.foreach(record => offset = record.offset + 1)\n      response.map { r =>\n        // record the time between producing the message and reading it\n        MetricEmitter.emitHistogramMetric(delayMetric, (now - r.timestamp).max(0))\n        (r.topic, r.partition, r.offset, r.value)\n      }\n    } catch {\n      case _: WakeupException if retry > 0 =>\n        // Happens if the 'poll()' takes too long.\n        // This exception should occur iff 'poll()' has been woken up by the scheduled task.\n        // For this reason, it should not necessary to cancel the task. We cancel the task\n        // to be on the safe side because an ineffective `wakeup()` applies to the next\n        // consumer call that can be woken up.\n        // The scheduler is expected to safely ignore the cancellation of a task that already\n        // has been cancelled or is already complete.\n        wakeUpTask.cancel()\n        logging.error(this, s\"poll timeout occurred. Retrying $retry more times.\")\n        Thread.sleep(gracefulWaitTime.toMillis) // using Thread.sleep is okay, since `poll` is blocking anyway\n        peek(duration, retry - 1)\n      case e: RetriableException if retry > 0 =>\n        // Happens if something goes wrong with 'poll()' and 'poll()' can be retried.\n        wakeUpTask.cancel()\n        logging.error(this, s\"poll returned with failure. Retrying $retry more times. Exception: $e\")\n        Thread.sleep(gracefulWaitTime.toMillis) // using Thread.sleep is okay, since `poll` is blocking anyway\n        peek(duration, retry - 1)\n      case e: Throwable =>\n        // Every other error results in a restart of the consumer\n        wakeUpTask.cancel()\n        logging.error(this, s\"poll returned with failure. Recreating the consumer. Exception: $e\")\n        recreateConsumer()\n        throw e\n    }\n  }\n\n  /**\n   * Commits offsets from last poll.\n   */\n  def commit(retry: Int = 3): Unit =\n    try {\n      synchronized(consumer.commitSync())\n    } catch {\n      case e: RetriableException =>\n        if (retry > 0) {\n          logging.error(this, s\"$e: retrying $retry more times\")\n          Thread.sleep(gracefulWaitTime.toMillis) // using Thread.sleep is okay, since `commitSync` is blocking anyway\n          commit(retry - 1)\n        } else {\n          throw e\n        }\n      case e: WakeupException =>\n        logging.info(this, s\"WakeupException happened when do commit action for topic ${topic}\")\n        recreateConsumer()\n    }\n\n  override def close(): Unit = synchronized {\n    logging.info(this, s\"closing consumer for '$topic'\")\n    consumer.close()\n  }\n\n  /** Creates a new kafka consumer and subscribes to topic list if given. */\n  private def createConsumer(topic: String) = {\n    val config = Map(\n      ConsumerConfig.CLIENT_ID_CONFIG -> s\"consumer-$topic\",\n      ConsumerConfig.GROUP_ID_CONFIG -> groupid,\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> kafkahost,\n      ConsumerConfig.MAX_POLL_RECORDS_CONFIG -> maxPeek.toString) ++\n      configMapToKafkaConfig(loadConfigOrThrow[Map[String, String]](ConfigKeys.kafkaCommon)) ++\n      configMapToKafkaConfig(loadConfigOrThrow[Map[String, String]](ConfigKeys.kafkaConsumer))\n\n    verifyConfig(config, ConsumerConfig.configNames().asScala.toSet)\n\n    val consumer = tryAndThrow(s\"creating consumer for $topic\") {\n      new KafkaConsumer(config, new ByteArrayDeserializer, new ByteArrayDeserializer)\n    }\n\n    // subscribe does not need to be synchronized, because the reference to the consumer hasn't been returned yet and\n    // thus this is guaranteed only to be called by the calling thread.\n    tryAndThrow(s\"subscribing to $topic\")(consumer.subscribe(Seq(topic).asJavaCollection))\n\n    consumer\n  }\n\n  private def recreateConsumer(): Unit = synchronized {\n    logging.info(this, s\"recreating consumer for '$topic'\")\n    // According to documentation, the consumer is force closed if it cannot be closed gracefully.\n    // See https://kafka.apache.org/11/javadoc/index.html?org/apache/kafka/clients/consumer/KafkaConsumer.html\n    //\n    // For the moment, we have no special handling of 'InterruptException' - it may be possible or even\n    // needed to re-try the 'close()' when being interrupted.\n    tryAndSwallow(\"closing old consumer\")(consumer.close())\n    logging.info(this, s\"old consumer closed for '$topic'\")\n\n    consumer = createConsumer(topic)\n  }\n\n  @volatile private var consumer: KafkaConsumer[Array[Byte], Array[Byte]] = createConsumer(topic)\n\n  // Read current lag of the consumed topic, e.g. invoker queue\n  // Since we use only one partition in kafka, it is defined 0\n  Scheduler.scheduleWaitAtMost(\n    interval = loadConfigOrThrow[KafkaConfig](ConfigKeys.kafka).consumerLagCheckInterval,\n    initialDelay = 10.seconds,\n    name = \"kafka-lag-monitor\") { () =>\n    Future {\n      blocking {\n        if (offset > 0) {\n          val topicAndPartition = new TopicPartition(topic, 0)\n          synchronized(consumer.endOffsets(Set(topicAndPartition).asJava)).asScala.get(topicAndPartition).foreach {\n            endOffset =>\n              // endOffset could lag behind the offset reported by the consumer internally resulting in negative numbers\n              val queueSize = (endOffset - offset).max(0)\n              MetricEmitter.emitGaugeMetric(queueMetric, queueSize)\n          }\n        }\n      }\n    }.andThen {\n      case Failure(_: WakeupException) =>\n        recreateConsumer()\n      case Failure(e) =>\n        // Only log level info because failed metric reporting is not critical\n        logging.info(this, s\"lag metric reporting failed for topic '$topic': $e\")\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/connector/kafka/KafkaMessagingProvider.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.connector.kafka\n\nimport java.util.Properties\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.kafka.clients.admin.{AdminClient, AdminClientConfig, NewTopic}\nimport org.apache.kafka.common.errors.{RetriableException, TopicExistsException}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.common.{CausedBy, Logging}\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.core.connector.{MessageConsumer, MessageProducer, MessagingProvider}\nimport org.apache.openwhisk.core.entity.ByteSize\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.duration._\nimport scala.util.{Failure, Success, Try}\n\ncase class KafkaConfig(replicationFactor: Short, consumerLagCheckInterval: FiniteDuration)\n\n/**\n * A Kafka based implementation of MessagingProvider\n */\nobject KafkaMessagingProvider extends MessagingProvider {\n  import KafkaConfiguration._\n\n  private val topicPartitionsConfigKey = \"partitions\"\n\n  def getConsumer(config: WhiskConfig, groupId: String, topic: String, maxPeek: Int, maxPollInterval: FiniteDuration)(\n    implicit logging: Logging,\n    actorSystem: ActorSystem): MessageConsumer =\n    new KafkaConsumerConnector(config.kafkaHosts, groupId, topic, maxPeek)\n\n  def getProducer(config: WhiskConfig, maxRequestSize: Option[ByteSize] = None)(\n    implicit logging: Logging,\n    actorSystem: ActorSystem): MessageProducer =\n    new KafkaProducerConnector(config.kafkaHosts, maxRequestSize = maxRequestSize)\n\n  def ensureTopic(config: WhiskConfig, topic: String, topicConfigKey: String, maxMessageBytes: Option[ByteSize] = None)(\n    implicit logging: Logging): Try[Unit] = {\n    val kafkaConfig = loadConfigOrThrow[KafkaConfig](ConfigKeys.kafka)\n    val topicConfig = KafkaConfiguration.configMapToKafkaConfig(\n      loadConfigOrThrow[Map[String, String]](ConfigKeys.kafkaTopics + \".\" + topicConfigKey)) ++\n      (maxMessageBytes.map { max =>\n        Map(s\"max.message.bytes\" -> max.size.toString)\n      } getOrElse Map.empty)\n\n    val commonConfig = configMapToKafkaConfig(loadConfigOrThrow[Map[String, String]](ConfigKeys.kafkaCommon))\n\n    Try(AdminClient.create(commonConfig + (AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG -> config.kafkaHosts)))\n      .flatMap(client => {\n        val partitions = topicConfig.getOrElse(topicPartitionsConfigKey, \"1\").toInt\n        val safeTopicConfig = topicConfig - topicPartitionsConfigKey\n        val nt = new NewTopic(topic, partitions, kafkaConfig.replicationFactor).configs(safeTopicConfig.asJava)\n\n        def createTopic(retries: Int = 5): Try[Unit] = {\n          Try(client.listTopics().names().get())\n            .flatMap(topics =>\n              if (topics.contains(topic)) {\n                Success(logging.info(this, s\"$topic already exists and the user can see it, skipping creation.\"))\n              } else {\n                Try(client.createTopics(List(nt).asJava).values().get(topic).get())\n                  .map(_ => logging.info(this, s\"created topic $topic\"))\n                  .recoverWith {\n                    case CausedBy(_: TopicExistsException) =>\n                      Success(logging.info(this, s\"topic $topic already existed\"))\n                    case CausedBy(t: RetriableException) if retries > 0 =>\n                      logging.warn(this, s\"topic $topic could not be created because of $t, retries left: $retries\")\n                      Thread.sleep(1.second.toMillis)\n                      createTopic(retries - 1)\n                    case t =>\n                      logging.error(this, s\"ensureTopic for $topic failed due to $t\")\n                      Failure(t)\n                  }\n            })\n        }\n\n        val result = createTopic()\n        client.close()\n        result\n      })\n      .recoverWith {\n        case e =>\n          logging.error(this, s\"ensureTopic for $topic failed due to $e\")\n          Failure(e)\n      }\n  }\n}\n\nobject KafkaConfiguration {\n  import scala.language.implicitConversions\n\n  implicit def mapToProperties(map: Map[String, String]): Properties = {\n    val props = new Properties()\n    map.foreach { case (key, value) => props.setProperty(key, value) }\n    props\n  }\n\n  /**\n   * Converts TypesafeConfig keys to a KafkaConfig key.\n   *\n   * TypesafeConfig's keys are usually kebab-cased (dash-delimited), whereas KafkaConfig keys are dot.delimited. This\n   * converts an example-key-to-illustrate to example.key.to.illustrate.\n   */\n  def configToKafkaKey(configKey: String): String = configKey.replace(\"-\", \".\")\n\n  /** Converts a Map read from TypesafeConfig to a Map to be read by Kafka clients. */\n  def configMapToKafkaConfig(configMap: Map[String, String]): Map[String, String] = configMap.map {\n    case (key, value) => configToKafkaKey(key) -> value\n  }\n\n  /**\n   * Prints a warning for each unknown configuration item and returns false if at least one item is unknown.\n   *\n   * @param config the config to be checked\n   * @param validKeys known valid keys to configure\n   * @return true if all configuration keys are known, false if at least one is unknown\n   */\n  def verifyConfig(config: Map[String, String], validKeys: Set[String])(implicit logging: Logging): Boolean = {\n    val passedKeys = config.keySet\n    val knownKeys = validKeys intersect passedKeys\n    val unknownKeys = passedKeys -- knownKeys\n\n    if (unknownKeys.nonEmpty) {\n      logging.warn(this, s\"potential misconfiguration, unknown settings: ${unknownKeys.mkString(\",\")}\")\n      false\n    } else {\n      true\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/connector/kafka/KafkaMetrics.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.connector.kafka\n\nimport org.apache.pekko.http.scaladsl.server.{Directives, Route}\nimport org.apache.kafka.common.{Metric, MetricName => JMetricName}\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Success, Try}\n\ntrait KafkaMetricsProvider {\n  def metrics(): Future[Map[JMetricName, Metric]]\n}\n\n/**\n * Utility to convert a map of Kafka metrics to JSON\n */\nobject KafkaMetrics {\n  private case class MetricName(name: String, group: String, description: String, tags: Map[String, String])\n\n  private object MetricName extends DefaultJsonProtocol {\n    implicit val serdes = jsonFormat4(MetricName.apply)\n    def apply(m: JMetricName): MetricName = new MetricName(m.name(), m.group(), m.description(), m.tags().asScala.toMap)\n  }\n\n  def toJson(metrics: Map[JMetricName, Metric]): JsValue = {\n    val result = metrics.values.flatMap { m =>\n      getValue(m).map { v =>\n        val json = MetricName(m.metricName()).toJson.asJsObject\n        JsObject(json.fields + (\"value\" -> v))\n      }\n    }.toSeq\n    result.toJson\n  }\n\n  private def getValue(m: Metric): Option[JsValue] = {\n    Try(m.metricValue()) match {\n      case Success(v: java.lang.Double) => Some(JsNumber(v.toDouble))\n      case Success(v: String)           => Some(JsString(v))\n      case _                            => None\n    }\n  }\n}\n\n/**\n * Exposes the Kafka metrics as a json endpoint `/metrics/kafka`. This can be used\n * to expose metrics of a specific consumer or producer like in User Event service\n */\nobject KafkaMetricRoute extends Directives {\n  def apply(provider: KafkaMetricsProvider)(implicit ec: ExecutionContext): Route = {\n    path(\"metrics\" / \"kafka\") {\n      val metrics = provider.metrics().map(m => KafkaMetrics.toJson(m))\n      complete(metrics)\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/connector/kafka/KafkaProducerConnector.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.connector.kafka\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.pattern.after\nimport org.apache.kafka.clients.producer._\nimport org.apache.kafka.common.errors._\nimport org.apache.kafka.common.serialization.StringSerializer\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.common.{Counter, Logging, TransactionId}\nimport org.apache.openwhisk.connector.kafka.KafkaConfiguration._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.connector.{Message, MessageProducer, ResultMetadata}\nimport org.apache.openwhisk.core.entity.{ByteSize, UUIDs}\nimport org.apache.openwhisk.utils.Exceptions\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.duration._\nimport scala.concurrent.{blocking, ExecutionContext, Future, Promise}\nimport scala.util.{Failure, Success}\n\nclass KafkaProducerConnector(\n  kafkahosts: String,\n  id: String = UUIDs.randomUUID().toString,\n  maxRequestSize: Option[ByteSize] = None)(implicit logging: Logging, actorSystem: ActorSystem)\n    extends MessageProducer\n    with Exceptions {\n\n  implicit val ec: ExecutionContext = actorSystem.dispatcher\n  private val gracefulWaitTime = 100.milliseconds\n\n  override def sentCount(): Long = sentCounter.cur\n\n  /** Sends msg to topic. This is an asynchronous operation. */\n  override def send(topic: String, msg: Message, retry: Int = 3): Future[ResultMetadata] = {\n    implicit val transid: TransactionId = msg.transid\n    val record = new ProducerRecord[String, String](topic, msg.serialize)\n    val produced = Promise[ResultMetadata]()\n\n    Future {\n      blocking {\n        try {\n          producer.send(record, new Callback {\n            override def onCompletion(metadata: RecordMetadata, exception: Exception): Unit = {\n              if (exception == null)\n                produced.trySuccess(ResultMetadata(metadata.topic(), metadata.partition(), metadata.offset()))\n              else produced.tryFailure(exception)\n            }\n          })\n        } catch {\n          case e: Throwable =>\n            produced.tryFailure(e)\n        }\n      }\n    }\n\n    produced.future.andThen {\n      case Success(status) =>\n        logging.debug(this, s\"sent message: ${status.topic}[${status.partition}][${status.offset}]\")\n        sentCounter.next()\n      case Failure(t) =>\n        logging.error(this, s\"sending message on topic '$topic' failed: ${t.getMessage}\")\n    } recoverWith {\n      // Do not retry on these exceptions as they may cause duplicate messages on Kafka.\n      case _: NotEnoughReplicasAfterAppendException | _: TimeoutException =>\n        recreateProducer()\n        produced.future\n      case r: RetriableException if retry > 0 =>\n        logging.info(this, s\"$r: Retrying $retry more times\")\n        after(gracefulWaitTime, actorSystem.scheduler)(send(topic, msg, retry - 1))\n      // Ignore this exception as restarting the producer doesn't make sense\n      case e: RecordTooLargeException =>\n        Future.failed(e)\n      // All unknown errors just result in a recreation of the producer. The failure is propagated.\n      case _: Throwable =>\n        recreateProducer()\n        produced.future\n    }\n  }\n\n  /** Closes producer. */\n  override def close(): Unit = {\n    logging.info(this, \"closing producer\")\n    producer.close()\n  }\n\n  private val sentCounter = new Counter()\n\n  private def createProducer(): KafkaProducer[String, String] = {\n    val config = Map(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG -> kafkahosts) ++\n      configMapToKafkaConfig(loadConfigOrThrow[Map[String, String]](ConfigKeys.kafkaCommon)) ++\n      configMapToKafkaConfig(loadConfigOrThrow[Map[String, String]](ConfigKeys.kafkaProducer)) ++\n      (maxRequestSize map { max =>\n        Map(\"max.request.size\" -> max.size.toString)\n      } getOrElse Map.empty)\n\n    verifyConfig(config, ProducerConfig.configNames().asScala.toSet)\n\n    tryAndThrow(\"creating producer\")(new KafkaProducer(config, new StringSerializer, new StringSerializer))\n  }\n\n  private def recreateProducer(): Unit = {\n    logging.info(this, s\"recreating producer\")\n    tryAndSwallow(\"closing old producer\")(producer.close())\n    logging.info(this, s\"old producer closed\")\n    producer = createProducer()\n  }\n\n  @volatile private var producer = createProducer()\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/connector/kafka/KamonMetricsReporter.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.connector.kafka\n\nimport java.util\nimport java.util.concurrent.{ScheduledFuture, TimeUnit}\n\nimport kamon.Kamon\nimport kamon.metric.{Counter, Gauge, Metric}\nimport kamon.tag.TagSet\nimport org.apache.kafka.common.MetricName\nimport org.apache.kafka.common.metrics.stats.CumulativeSum\nimport org.apache.kafka.common.metrics.{KafkaMetric, MetricsReporter}\nimport org.apache.openwhisk.core.ConfigKeys\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.duration.FiniteDuration\nimport scala.util.{Success, Try}\nimport scala.collection.JavaConverters._\n\nclass KamonMetricsReporter extends MetricsReporter {\n  import KamonMetricsReporter._\n  private val metrics = new TrieMap[MetricName, MetricBridge]()\n  private val metricConfig = loadConfigOrThrow[KafkaMetricConfig](s\"${ConfigKeys.kafka}.metrics\")\n  @volatile\n  private var updater: Option[ScheduledFuture[_]] = None\n\n  override def init(metrics: util.List[KafkaMetric]): Unit = metrics.forEach(add)\n\n  override def metricChange(metric: KafkaMetric): Unit = {\n    remove(metric)\n    add(metric)\n  }\n\n  override def metricRemoval(metric: KafkaMetric): Unit = remove(metric)\n\n  override def close(): Unit = updater.foreach(_.cancel(false))\n\n  override def configure(configs: util.Map[String, _]): Unit = {\n    val interval = metricConfig.reportInterval.toSeconds\n    val f = Kamon.scheduler().scheduleAtFixedRate(() => updateAll(), interval, interval, TimeUnit.SECONDS)\n    updater = Some(f)\n  }\n\n  private def add(metric: KafkaMetric): Unit = {\n    val mn = metric.metricName()\n    if (metricConfig.names.contains(mn.name()) && shouldIncludeMetric(mn)) {\n      val tags = kafkaTagsToTagSet(mn.tags())\n      val metricName = kamonName(mn)\n      val bridge = if (isCounterMetric(metric)) {\n        val counter = Kamon.counter(metricName)\n        new CounterBridge(metric, counter, counter.withTags(tags))\n      } else {\n        val gauge = Kamon.gauge(metricName)\n        new GaugeBridge(metric, gauge, gauge.withTags(tags))\n      }\n      metrics.putIfAbsent(mn, bridge)\n    }\n  }\n\n  private def remove(metric: KafkaMetric) = metrics.remove(metric.metricName()).foreach(_.remove())\n\n  private def updateAll(): Unit = metrics.values.foreach(_.update())\n}\n\nobject KamonMetricsReporter {\n  val name = classOf[KamonMetricsReporter].getName\n  private val excludedTopicAttributes = Set(\"records-lag-max\", \"records-consumed-total\", \"bytes-consumed-total\")\n\n  case class KafkaMetricConfig(names: Set[String], reportInterval: FiniteDuration)\n\n  abstract class MetricBridge(val kafkaMetric: KafkaMetric, kamonMetric: Metric[_, _]) {\n    def remove(): Unit = kamonMetric.remove(kafkaTagsToTagSet(kafkaMetric.metricName().tags()))\n    def update(): Unit\n\n    def metricValue: Long =\n      Try(kafkaMetric.metricValue())\n        .map {\n          case d: java.lang.Double => d.toLong\n          case _                   => 0L\n        }\n        .getOrElse(0L)\n  }\n\n  class GaugeBridge(kafkaMetric: KafkaMetric, kamonMetric: Metric.Gauge, gauge: Gauge)\n      extends MetricBridge(kafkaMetric, kamonMetric) {\n    override def update(): Unit = gauge.update(metricValue)\n  }\n\n  class CounterBridge(kafkaMetric: KafkaMetric, kamonMetric: Metric.Counter, counter: Counter)\n      extends MetricBridge(kafkaMetric, kamonMetric) {\n    @volatile\n    private var lastValue: Long = 0\n    override def update(): Unit = {\n      val newValue = metricValue\n      counter.increment(newValue - lastValue)\n      lastValue = newValue\n    }\n  }\n\n  def kamonName(mn: MetricName): String = {\n    //Drop the `-total` suffix as it results in prometheus metrics ending with total twice\n    val name = if (mn.name().endsWith(\"-total\")) mn.name().dropRight(6) else mn.name()\n    s\"${mn.group()}_$name\"\n  }\n\n  def isCounterMetric(metric: KafkaMetric): Boolean = Try(metric.measurable()) match {\n    case Success(_: CumulativeSum) => true\n    case _                         => false\n  }\n\n  def shouldIncludeMetric(m: MetricName): Boolean = {\n    //Avoid duplicate metrics for specific cases which are recorded at multiple level\n    //For example `bytes-consumed-total` is recorded at consumer and topic level. As we use a 1-1 consumer per topic\n    //We can drop the lag recording at topic level\n    if (excludedTopicAttributes.contains(m.name())) !m.tags().containsKey(\"topic\")\n    else true\n  }\n\n  private def kafkaTagsToTagSet(kafkaTags: util.Map[String, String]): TagSet =\n    kafkaTags.asScala.foldLeft(TagSet.Empty) {\n      case (set, (k, v)) => set.withTag(k, v)\n    }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/connector/lean/LeanConsumer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.connector.lean\n\nimport scala.concurrent.duration._\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.connector.MessageConsumer\nimport java.util.concurrent.BlockingQueue\nimport java.util.concurrent.TimeUnit\n\nclass LeanConsumer(queue: BlockingQueue[Array[Byte]], override val maxPeek: Int)(implicit logging: Logging)\n    extends MessageConsumer {\n\n  /**\n   * Long poll for messages. Method returns once message available but no later than given\n   * duration.\n   *\n   * @param duration the maximum duration for the long poll\n   */\n  override def peek(duration: FiniteDuration, retry: Int): Iterable[(String, Int, Long, Array[Byte])] = {\n    Option(queue.poll(duration.toMillis, TimeUnit.MILLISECONDS))\n      .map(record => Iterable((\"\", 0, 0L, record)))\n      .getOrElse(Iterable.empty)\n  }\n\n  /**\n   * There's no cursor to advance since that's done in the poll above.\n   */\n  override def commit(retry: Int): Unit = { /*do nothing*/ }\n\n  override def close(): Unit = {\n    logging.info(this, s\"closing lean consumer\")\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/connector/lean/LeanMessagingProvider.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.connector.lean\n\nimport java.util.concurrent.BlockingQueue\nimport java.util.concurrent.LinkedBlockingQueue\n\nimport scala.collection.mutable.Map\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.duration.FiniteDuration\nimport scala.util.Success\nimport scala.util.Try\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.MessageConsumer\nimport org.apache.openwhisk.core.connector.MessageProducer\nimport org.apache.openwhisk.core.connector.MessagingProvider\nimport org.apache.openwhisk.core.entity.ByteSize\n\n/**\n * A simple implementation of MessagingProvider.\n */\nobject LeanMessagingProvider extends MessagingProvider {\n\n  /** Map to hold message queues, the key is the topic */\n  val queues: Map[String, BlockingQueue[Array[Byte]]] =\n    new TrieMap[String, BlockingQueue[Array[Byte]]]\n\n  def getConsumer(config: WhiskConfig, groupId: String, topic: String, maxPeek: Int, maxPollInterval: FiniteDuration)(\n    implicit logging: Logging,\n    actorSystem: ActorSystem): MessageConsumer = {\n\n    val queue = queues.getOrElseUpdate(topic, new LinkedBlockingQueue[Array[Byte]]())\n\n    new LeanConsumer(queue, maxPeek)\n  }\n\n  def getProducer(config: WhiskConfig, maxRequestSize: Option[ByteSize] = None)(\n    implicit logging: Logging,\n    actorSystem: ActorSystem): MessageProducer =\n    new LeanProducer(queues)\n\n  def ensureTopic(config: WhiskConfig, topic: String, topicConfigKey: String, maxMessageBytes: Option[ByteSize] = None)(\n    implicit logging: Logging): Try[Unit] = {\n    if (queues.contains(topic)) {\n      Success(logging.info(this, s\"topic $topic already existed\"))\n    } else {\n      queues.put(topic, new LinkedBlockingQueue[Array[Byte]]())\n      Success(logging.info(this, s\"topic $topic created\"))\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/connector/lean/LeanProducer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.connector.lean\n\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.concurrent.Future\nimport org.apache.openwhisk.common.Counter\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.connector.{Message, MessageProducer, ResultMetadata}\n\nimport java.util.concurrent.{BlockingQueue, LinkedBlockingQueue}\nimport scala.collection.mutable.Map\nimport java.nio.charset.StandardCharsets\nimport scala.concurrent.ExecutionContext\n\nclass LeanProducer(queues: Map[String, BlockingQueue[Array[Byte]]])(implicit logging: Logging, actorSystem: ActorSystem)\n    extends MessageProducer {\n\n  implicit val ec: ExecutionContext = actorSystem.dispatcher\n\n  override def sentCount(): Long = sentCounter.cur\n\n  /** Sends msg to topic. This is an asynchronous operation. */\n  override def send(topic: String, msg: Message, retry: Int = 3): Future[ResultMetadata] = {\n    implicit val transid = msg.transid\n\n    val queue = queues.getOrElseUpdate(topic, new LinkedBlockingQueue[Array[Byte]]())\n\n    Future {\n      queue.put(msg.serialize.getBytes(StandardCharsets.UTF_8))\n      sentCounter.next()\n      ResultMetadata(topic, 0, -1)\n    }\n  }\n\n  /** Closes producer. */\n  override def close(): Unit = {\n    logging.info(this, \"closing lean producer\")\n  }\n\n  private val sentCounter = new Counter()\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/FeatureFlags.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nobject FeatureFlags {\n  private case class FeatureFlagConfig(requireApiKeyAnnotation: Boolean, requireResponsePayload: Boolean)\n  private val config = loadConfigOrThrow[FeatureFlagConfig](ConfigKeys.featureFlags)\n\n  val requireApiKeyAnnotation: Boolean = config.requireApiKeyAnnotation\n  val requireResponsePayload: Boolean = config.requireResponsePayload\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/WarmUp.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.connector.{ActivationMessage, ContainerCreationMessage}\nimport org.apache.openwhisk.core.entity.ActivationId.ActivationIdGenerator\nimport org.apache.openwhisk.core.entity._\n\nobject WarmUp {\n  val warmUpActionIdentity = {\n    val whiskSystem = \"whisk.system\"\n    val uuid = UUID()\n    Identity(Subject(whiskSystem), Namespace(EntityName(whiskSystem), uuid), BasicAuthenticationAuthKey(uuid, Secret()))\n  }\n\n  private val actionName = \"warmUp\"\n\n  // this action doesn't need to be in database\n  val warmUpAction = FullyQualifiedEntityName(warmUpActionIdentity.namespace.name.toPath, EntityName(actionName))\n\n  def warmUpActivation(controller: ControllerInstanceId) = {\n    ActivationMessage(\n      transid = TransactionId.warmUp,\n      action = warmUpAction,\n      revision = DocRevision.empty,\n      user = warmUpActionIdentity,\n      activationId = new ActivationIdGenerator {}.make(),\n      rootControllerIndex = controller,\n      blocking = false,\n      content = None,\n      initArgs = Set.empty)\n  }\n\n  def warmUpContainerCreationMessage(scheduler: SchedulerInstanceId) =\n    ExecManifest.runtimesManifest\n      .resolveDefaultRuntime(\"nodejs:default\")\n      .map { manifest =>\n        val metadata = WhiskActionMetaData(\n          warmUpAction.path,\n          warmUpAction.name,\n          CodeExecMetaDataAsString(manifest, false, entryPoint = None))\n        ContainerCreationMessage(\n          TransactionId.warmUp,\n          warmUpActionIdentity.namespace.name.toString,\n          warmUpAction,\n          DocRevision.empty,\n          metadata,\n          scheduler,\n          \"\",\n          0)\n      }\n\n  def isWarmUpAction(fqn: FullyQualifiedEntityName): Boolean = {\n    fqn == warmUpAction\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/WhiskConfig.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core\n\nimport java.io.File\n\nimport org.apache.pekko.http.scaladsl.model.Uri.normalize\nimport org.apache.openwhisk.common.{Config, Logging}\n\nimport scala.io.Source\nimport scala.util.Try\n\n/**\n * A set of properties which might be needed to run a whisk microservice implemented\n * in scala.\n *\n * @param requiredProperties a Map whose keys define properties that must be bound to\n *                           a value, and whose values are default values. A null value in the Map means there is\n *                           no default value specified, so it must appear in the properties file.\n * @param optionalProperties a set of optional properties (which may not be defined).\n * @param propertiesFile     a File object, the whisk.properties file, which if given contains the property values.\n * @param env                an optional environment to initialize from.\n */\nclass WhiskConfig(requiredProperties: Map[String, String],\n                  optionalProperties: Set[String] = Set.empty,\n                  propertiesFile: File = null,\n                  env: Map[String, String] = sys.env)(implicit logging: Logging)\n    extends Config(requiredProperties, optionalProperties)(env) {\n\n  /**\n   * Loads the properties as specified above.\n   *\n   * @return a pair which is the Map defining the properties, and a boolean indicating whether validation succeeded.\n   */\n  override protected def getProperties() = {\n    val properties = super.getProperties()\n    if (!disableReadFromFile()) {\n      WhiskConfig.readPropertiesFromFile(properties, Option(propertiesFile) getOrElse (WhiskConfig.whiskPropertiesFile))\n    }\n    properties\n  }\n\n  private def disableReadFromFile() = java.lang.Boolean.getBoolean(WhiskConfig.disableWhiskPropsFileRead)\n\n  val servicePort = this(WhiskConfig.servicePort)\n  val dockerEndpoint = this(WhiskConfig.dockerEndpoint)\n  val dockerPort = this(WhiskConfig.dockerPort)\n\n  val wskApiHost: String = Try(\n    normalize(\n      s\"${this(WhiskConfig.wskApiProtocol)}://${this(WhiskConfig.wskApiHostname)}:${this(WhiskConfig.wskApiPort)}\"))\n    .getOrElse(\"\")\n\n  val controllerBlackboxFraction = this.getAsDouble(WhiskConfig.controllerBlackboxFraction, 0.10)\n\n  val edgeHost = this(WhiskConfig.edgeHostName) + \":\" + this(WhiskConfig.edgeHostApiPort)\n  val kafkaHosts = this(WhiskConfig.kafkaHostList)\n\n  val edgeHostName = this(WhiskConfig.edgeHostName)\n\n  val invokerHosts = this(WhiskConfig.invokerHostsList)\n  val zookeeperHosts = this(WhiskConfig.zookeeperHostList)\n\n  val dbPrefix = this(WhiskConfig.dbPrefix)\n  val mainDockerEndpoint = this(WhiskConfig.mainDockerEndpoint)\n\n  val runtimesManifest = this(WhiskConfig.runtimesManifest)\n  val actionInvokePerMinuteLimit = this(WhiskConfig.actionInvokePerMinuteLimit)\n  val actionInvokeConcurrentLimit = this(WhiskConfig.actionInvokeConcurrentLimit)\n  val triggerFirePerMinuteLimit = this(WhiskConfig.triggerFirePerMinuteLimit)\n  val actionSequenceLimit = this(WhiskConfig.actionSequenceMaxLimit)\n  val controllerSeedNodes = this(WhiskConfig.controllerSeedNodes)\n\n  val schedulerHost = this(WhiskConfig.schedulerHost)\n  val schedulerRpcPort = this(WhiskConfig.schedulerRpcPort)\n  val schedulerPekkoPort = this(WhiskConfig.schedulerPekkoPort)\n}\n\nobject WhiskConfig {\n  val disableWhiskPropsFileRead = Config.prefix + \"disable.whisks.props.file.read\"\n\n  /**\n   * Reads a key from system environment as if it was part of WhiskConfig.\n   */\n  def readFromEnv(key: String): Option[String] = sys.env.get(asEnvVar(key))\n\n  private def whiskPropertiesFile: File = {\n    def propfile(dir: String, recurse: Boolean = false): File =\n      if (dir != null) {\n        val base = new File(dir)\n        val file = new File(base, \"whisk.properties\")\n        if (file.exists())\n          file\n        else if (recurse)\n          propfile(base.getParent, true)\n        else null\n      } else null\n\n    val dir = sys.props.get(\"user.dir\")\n    if (dir.isDefined) {\n      propfile(dir.get, true)\n    } else {\n      null\n    }\n  }\n\n  /**\n   * Reads a Map of key-value pairs from the environment (sys.env) -- store them in the\n   * mutable properties object.\n   */\n  def readPropertiesFromFile(properties: scala.collection.mutable.Map[String, String], file: File)(\n    implicit logging: Logging) = {\n    if (file != null && file.exists) {\n      logging.info(this, s\"reading properties from file $file\")\n      val source = Source.fromFile(file)\n      try {\n        for (line <- source.getLines if line.trim != \"\") {\n          val parts = line.split('=')\n          if (parts.length >= 1) {\n            val p = parts(0).trim\n            val v = if (parts.length == 2) parts(1).trim else \"\"\n            if (properties.contains(p)) {\n              properties += p -> v\n              logging.debug(this, s\"properties file set value for $p\")\n            }\n          } else {\n            logging.warn(this, s\"ignoring properties $line\")\n          }\n        }\n      } finally {\n        source.close()\n      }\n    }\n  }\n\n  def asEnvVar(key: String): String = {\n    if (key != null)\n      key.replace('.', '_').toUpperCase\n    else null\n  }\n\n  val servicePort = \"port\"\n  val dockerPort = \"docker.port\"\n\n  val dockerEndpoint = \"main.docker.endpoint\"\n  val dbPrefix = \"db.prefix\"\n  // these are not private because they are needed\n  // in the invoker (they are part of the environment\n  // passed to the user container)\n  val edgeHostName = \"edge.host\"\n\n  val wskApiProtocol = \"whisk.api.host.proto\"\n  val wskApiPort = \"whisk.api.host.port\"\n  val wskApiHostname = \"whisk.api.host.name\"\n  val wskApiHost = Map(wskApiProtocol -> \"https\", wskApiPort -> 443.toString, wskApiHostname -> null)\n\n  val mainDockerEndpoint = \"main.docker.endpoint\"\n\n  val controllerBlackboxFraction = \"controller.blackboxFraction\"\n  val dbInstances = \"db.instances\"\n\n  val kafkaHostList = \"kafka.hosts\"\n  val zookeeperHostList = \"zookeeper.hosts\"\n\n  val edgeHostApiPort = \"edge.host.apiport\"\n\n  val invokerHostsList = \"invoker.hosts\"\n  val dbHostsList = \"db.hostsList\"\n\n  val edgeHost = Map(edgeHostName -> null, edgeHostApiPort -> null)\n  val invokerHosts = Map(invokerHostsList -> null)\n  val kafkaHosts = Map(kafkaHostList -> null)\n  val zookeeperHosts = Map(zookeeperHostList -> null)\n\n  val runtimesManifest = \"runtimes.manifest\"\n\n  val actionSequenceMaxLimit = \"limits.actions.sequence.maxLength\"\n  val actionInvokePerMinuteLimit = \"limits.actions.invokes.perMinute\"\n  val actionInvokeConcurrentLimit = \"limits.actions.invokes.concurrent\"\n  val triggerFirePerMinuteLimit = \"limits.triggers.fires.perMinute\"\n  val controllerSeedNodes = \"pekko.cluster.seed.nodes\"\n\n  val schedulerHost = \"whisk.scheduler.endpoints.host\"\n  val schedulerRpcPort = \"whisk.scheduler.endpoints.rpcPort\"\n  val schedulerPekkoPort = \"whisk.scheduler.endpoints.pekkoPort\"\n}\n\nobject ConfigKeys {\n  val cluster = \"whisk.cluster\"\n  val loadbalancer = \"whisk.loadbalancer\"\n  val fpcLoadBalancer = \"whisk.loadbalancer.fpc\"\n  val fraction = \"whisk.fraction\"\n  val buildInformation = \"whisk.info\"\n\n  val couchdb = \"whisk.couchdb\"\n  val cosmosdb = \"whisk.cosmosdb\"\n  val mongodb = \"whisk.mongodb\"\n  val kafka = \"whisk.kafka\"\n  val kafkaCommon = s\"$kafka.common\"\n  val kafkaProducer = s\"$kafka.producer\"\n  val kafkaConsumer = s\"$kafka.consumer\"\n  val kafkaTopics = s\"$kafka.topics\"\n  val kafkaTopicsPrefix = s\"$kafkaTopics.prefix\"\n  val kafkaTopicsUserEventPrefix = s\"$kafkaTopics.user-event.prefix\"\n\n  val memory = \"whisk.memory\"\n  val timeLimit = \"whisk.time-limit\"\n  val logLimit = \"whisk.log-limit\"\n  val concurrencyLimit = \"whisk.concurrency-limit\"\n  val parameterSizeLimit = \"whisk.parameter-size-limit\"\n\n  val namespaceMemoryLimit = \"whisk.namespace-default-limit.memory\"\n  val namespaceTimeLimit = \"whisk.namespace-default-limit.time-limit\"\n  val namespaceLogLimit = \"whisk.namespace-default-limit.log-limit\"\n  val namespaceConcurrencyLimit = \"whisk.namespace-default-limit.concurrency-limit\"\n  val namespaceParameterSizeLimit = \"whisk.namespace-default-limit.parameter-size-limit\"\n  val namespaceActivationPayloadLimit = \"whisk.namespace-default-limit.activation.payload\"\n\n  val activation = \"whisk.activation\"\n  val userEvents = \"whisk.user-events\"\n\n  val runtimes = \"whisk.runtimes\"\n  val runtimesWhitelists = s\"$runtimes.whitelists\"\n\n  val db = \"whisk.db\"\n\n  val docker = \"whisk.docker\"\n  val dockerClient = s\"$docker.client\"\n  val dockerContainerFactory = s\"$docker.container-factory\"\n  val standaloneDockerContainerFactory = s\"$docker.standalone.container-factory\"\n  val runc = \"whisk.runc\"\n  val runcTimeouts = s\"$runc.timeouts\"\n\n  val tracing = \"whisk.tracing\"\n\n  val containerFactory = \"whisk.container-factory\"\n  val containerArgs = s\"$containerFactory.container-args\"\n  val runtimesRegistry = s\"$containerFactory.runtimes-registry\"\n  val userImagesRegistry = s\"$containerFactory.user-images-registry\"\n  val containerPool = \"whisk.container-pool\"\n  val containerCreationMaxPeek = \"whisk.invoker.container-creation.max-peek\"\n  val blacklist = \"whisk.blacklist\"\n\n  val kubernetes = \"whisk.kubernetes\"\n  val kubernetesTimeouts = s\"$kubernetes.timeouts\"\n\n  val transactions = \"whisk.transactions\"\n\n  val logStore = \"whisk.logstore\"\n  val splunk = s\"$logStore.splunk\"\n  val logStoreElasticSearch = s\"$logStore.elasticsearch\"\n\n  val yarn = \"whisk.yarn\"\n\n  val containerProxy = \"whisk.container-proxy\"\n  val containerProxyTimeouts = s\"$containerProxy.timeouts\"\n  val containerProxyHealth = s\"$containerProxy.action-health-check\"\n  val containerProxyActivationErrorLogs = s\"$containerProxy.log-activation-errors\"\n\n  val s3 = \"whisk.s3\"\n  val query = \"whisk.query-limit\"\n  val execSizeLimit = \"whisk.exec-size-limit\"\n\n  val controller = s\"whisk.controller\"\n  val controllerActivation = s\"$controller.activation\"\n\n  val etcd = \"whisk.etcd\"\n  val etcdLeaseTimeout = \"whisk.etcd.lease.timeout\"\n  val etcdPoolThreads = \"whisk.etcd.pool.threads\"\n\n  val activationStore = \"whisk.activation-store\"\n  val elasticSearchActivationStore = s\"$activationStore.elasticsearch\"\n  val activationStoreWithFileStorage = s\"$activationStore.with-file-storage\"\n\n  val metrics = \"whisk.metrics\"\n  val featureFlags = \"whisk.feature-flags\"\n\n  val durationChecker = s\"whisk.duration-checker\"\n\n  val whiskConfig = \"whisk.config\"\n  val sharedPackageExecuteOnly = s\"whisk.shared-packages-execute-only\"\n  val swaggerUi = \"whisk.swagger-ui\"\n\n  /* DEPRECATED: disableStoreResult is deprecated for storeBlockingResultLevel */\n  val disableStoreResult = s\"$activation.disable-store-result\"\n  val storeBlockingResultLevel = s\"$activation.store-blocking-result-level\"\n  val storeNonBlockingResultLevel = s\"$activation.store-non-blocking-result-level\"\n  val unstoredLogsEnabled = s\"$activation.unstored-logs-enabled\"\n\n  val apacheClientConfig = \"whisk.apache-client\"\n\n  val parameterStorage = \"whisk.parameter-storage\"\n\n  val azBlob = \"whisk.azure-blob\"\n\n  val schedulerGrpcService = \"whisk.scheduler.grpc\"\n  val schedulerMaxPeek = \"whisk.scheduler.max-peek\"\n  val schedulerScheduling = \"whisk.scheduler.scheduling\"\n  val schedulerQueue = \"whisk.scheduler.queue\"\n  val schedulerQueueManager = \"whisk.scheduler.queue-manager\"\n  val schedulerInProgressJobRetention = \"whisk.scheduler.in-progress-job-retention\"\n  val schedulerBlackboxMultiple = \"whisk.scheduler.blackbox-multiple\"\n  val schedulerStaleThreshold = \"whisk.scheduler.stale-threshold\"\n\n  val whiskClusterName = \"whisk.cluster.name\"\n\n  val dataManagementServiceRetryInterval = \"whisk.scheduler.data-management-service.retry-interval\"\n\n  val whiskControllerUsername = \"whisk.controller.username\"\n  val whiskControllerPassword = \"whisk.controller.password\"\n\n  val whiskSchedulerUsername = \"whisk.scheduler.username\"\n  val whiskSchedulerPassword = \"whisk.scheduler.password\"\n\n  val whiskInvokerUsername = \"whisk.invoker.username\"\n  val whiskInvokerPassword = \"whisk.invoker.password\"\n\n  val invokerResourceTags = \"whisk.invoker.resource.tags\"\n  val invokerDedicatedNamespaces = \"whisk.invoker.dedicated.namespaces\"\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/ack/Ack.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.ack\nimport org.apache.openwhisk.common.{TransactionId, UserEvents}\nimport org.apache.openwhisk.core.connector.{AcknowledgementMessage, EventMessage, MessageProducer}\nimport org.apache.openwhisk.core.entity.{ControllerInstanceId, UUID, WhiskActivation}\n\nimport scala.concurrent.Future\n\n/**\n * A method for sending Active Acknowledgements (aka \"active ack\") messages to the load balancer. These messages\n * are either completion messages for an activation to indicate a resource slot is free, or result-forwarding\n * messages for continuations (e.g., sequences and conductor actions).\n *\n * The activation result is always provided because some acknowledgement messages may not carry the result of\n * the activation and this is needed for sending user events.\n *\n * @param tid the transaction id for the activation\n * @param activationResult is the activation result\n * @param blockingInvoke is true iff the activation was a blocking request\n * @param controllerInstance the originating controller/loadbalancer id\n * @param userId is the UUID for the namespace owning the activation\n * @param acknowledgement the acknowledgement message to send\n */\ntrait ActiveAck {\n  def apply(tid: TransactionId,\n            activationResult: WhiskActivation,\n            blockingInvoke: Boolean,\n            controllerInstance: ControllerInstanceId,\n            userId: UUID,\n            acknowledgement: AcknowledgementMessage): Future[Any]\n}\n\ntrait EventSender {\n  def send(msg: => EventMessage): Unit\n}\n\nclass UserEventSender(producer: MessageProducer) extends EventSender {\n  override def send(msg: => EventMessage): Unit = UserEvents.send(producer, msg)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/ack/HealthActionAck.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.ack\n\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.connector.{AcknowledgementMessage, MessageProducer}\nimport org.apache.openwhisk.core.entity.{ControllerInstanceId, UUID, WhiskActivation}\nimport spray.json.DefaultJsonProtocol._\n\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass HealthActionAck(producer: MessageProducer)(implicit logging: Logging, ec: ExecutionContext) extends ActiveAck {\n  override def apply(tid: TransactionId,\n                     activationResult: WhiskActivation,\n                     blockingInvoke: Boolean,\n                     controllerInstance: ControllerInstanceId,\n                     userId: UUID,\n                     acknowledgement: AcknowledgementMessage): Future[Any] = {\n    implicit val transid: TransactionId = tid\n\n    logging.debug(this, s\"health action was successfully invoked\")\n    if (activationResult.response.isContainerError || activationResult.response.isWhiskError) {\n      val actionPath =\n        activationResult.annotations.getAs[String](WhiskActivation.pathAnnotation).getOrElse(\"unknown_path\")\n      logging.error(this, s\"Failed to invoke action $actionPath, error: ${activationResult.response.toString}\")\n    }\n\n    Future.successful({})\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/ack/MessagingActiveAck.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.ack\n\nimport org.apache.kafka.common.errors.RecordTooLargeException\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.connector.{AcknowledgementMessage, EventMessage, MessageProducer}\nimport org.apache.openwhisk.core.entity._\nimport pureconfig._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success}\n\nclass MessagingActiveAck(producer: MessageProducer, instance: InstanceId, eventSender: Option[EventSender])(\n  implicit logging: Logging,\n  ec: ExecutionContext)\n    extends ActiveAck {\n\n  private val topicPrefix = loadConfigOrThrow[String](ConfigKeys.kafkaTopicsPrefix)\n\n  override def apply(tid: TransactionId,\n                     activationResult: WhiskActivation,\n                     blockingInvoke: Boolean,\n                     controllerInstance: ControllerInstanceId,\n                     userId: UUID,\n                     acknowledgement: AcknowledgementMessage): Future[Any] = {\n    implicit val transid: TransactionId = tid\n\n    def send(msg: AcknowledgementMessage, recovery: Boolean = false) = {\n      producer.send(topic = topicPrefix + \"completed\" + controllerInstance.asString, msg).andThen {\n        case Success(_) =>\n          val info = if (recovery) s\"recovery ${msg.messageType}\" else msg.messageType\n          logging.info(this, s\"posted $info of activation ${acknowledgement.activationId}\")\n      }\n    }\n\n    // UserMetrics are sent, when the slot is free again. This ensures, that all metrics are sent.\n    if (acknowledgement.isSlotFree.nonEmpty) {\n      eventSender.foreach { s =>\n        EventMessage.from(activationResult, instance.source, userId) match {\n          case Success(msg) => s.send(msg)\n          case Failure(t)   => logging.error(this, s\"activation event was not sent: $t\")\n        }\n      }\n    }\n\n    // An acknowledgement containing the result is only needed for blocking invokes in order to further the\n    // continuation. A result message for a non-blocking activation is not actually registered in the load balancer\n    // and the container proxy should not send such an acknowledgement unless it's a blocking request. Here the code\n    // is defensive and will shrink all non-blocking acknowledgements.\n    send(if (blockingInvoke) acknowledgement else acknowledgement.shrink).recoverWith {\n      case t if t.getCause.isInstanceOf[RecordTooLargeException] =>\n        send(acknowledgement.shrink, recovery = true)\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.connector\n\nimport scala.util.Try\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity._\n\nimport scala.concurrent.duration._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.openwhisk.core.entity.ActivationResponse.{statusForCode, ERROR_FIELD}\nimport org.apache.openwhisk.utils.JsHelpers\n\n/** Basic trait for messages that are sent on a message bus connector. */\ntrait Message {\n\n  /**\n   * A transaction id to attach to the message.\n   */\n  val transid = TransactionId.unknown\n\n  /**\n   * Serializes message to string. Must be idempotent.\n   */\n  def serialize: String\n\n  /**\n   * String representation of the message. Delegates to serialize.\n   */\n  override def toString = serialize\n}\n\ncase class ResultMetadata(topic: String, partition: Int, offset: Long)\n\ncase class ActivationMessage(override val transid: TransactionId,\n                             action: FullyQualifiedEntityName,\n                             revision: DocRevision,\n                             user: Identity,\n                             activationId: ActivationId,\n                             rootControllerIndex: ControllerInstanceId,\n                             blocking: Boolean,\n                             content: Option[JsValue],\n                             initArgs: Set[String] = Set.empty,\n                             lockedArgs: Map[String, String] = Map.empty,\n                             cause: Option[ActivationId] = None,\n                             traceContext: Option[Map[String, String]] = None)\n    extends Message {\n\n  override def serialize = ActivationMessage.serdes.write(this).compactPrint\n\n  override def toString = {\n    val value = (content getOrElse JsObject.empty).compactPrint\n    s\"$action?message=$value\"\n  }\n\n  def causedBySequence: Boolean = cause.isDefined\n}\n\n/**\n * Message that is sent from the invoker to the controller after action is completed or after slot is free again for\n * new actions.\n */\nabstract class AcknowledgementMessage(private val tid: TransactionId) extends Message {\n  override val transid: TransactionId = tid\n\n  override def serialize: String = AcknowledgementMessage.serdes.write(this).compactPrint\n\n  /** Pithy descriptor for logging. */\n  def messageType: String\n\n  /** Does message indicate slot is free? */\n  def isSlotFree: Option[InstanceId]\n\n  /** Does message contain a result? */\n  def result: Option[Either[ActivationId, WhiskActivation]]\n\n  /**\n   * Is the acknowledgement for an activation that failed internally?\n   * For some message, this is not relevant and the result is None.\n   */\n  def isSystemError: Option[Boolean]\n\n  def activationId: ActivationId\n\n  /** Serializes the message to JSON. */\n  def toJson: JsValue\n\n  /**\n   * Converts the message to a more compact form if it cannot cross the message bus as is or some of its details are not necessary.\n   */\n  def shrink: AcknowledgementMessage\n}\n\n/**\n * This message is sent from an invoker to the controller in situations when the resource slot and the action\n * result are available at the same time, and so the split-phase notification is not necessary. Instead the message\n * combines the `CompletionMessage` and `ResultMessage`. The `response` may be an `ActivationId` to allow for failures\n * to send the activation result because of event-bus size limitations.\n *\n * The constructor is private so that callers must use the more restrictive constructors which ensure the response is always\n * Right when this message is created.\n */\ncase class CombinedCompletionAndResultMessage private (override val transid: TransactionId,\n                                                       response: Either[ActivationId, WhiskActivation],\n                                                       override val isSystemError: Option[Boolean],\n                                                       instance: InstanceId)\n    extends AcknowledgementMessage(transid) {\n  override def messageType = \"combined\"\n\n  override def result = Some(response)\n\n  override def isSlotFree = Some(instance)\n\n  override def activationId = response.fold(identity, _.activationId)\n\n  override def toJson = CombinedCompletionAndResultMessage.serdes.write(this)\n\n  override def shrink = copy(response = response.flatMap(a => Left(a.activationId)))\n\n  override def toString = activationId.asString\n}\n\n/**\n * This message is sent from an invoker to the controller, once the resource slot in the invoker (used by the\n * corresponding activation) free again (i.e., after log collection). The `CompletionMessage` is part of a split\n * phase notification to the load balancer where an invoker first sends a `ResultMessage` and later sends the\n * `CompletionMessage`.\n */\ncase class CompletionMessage private (override val transid: TransactionId,\n                                      override val activationId: ActivationId,\n                                      override val isSystemError: Option[Boolean],\n                                      instance: InstanceId)\n    extends AcknowledgementMessage(transid) {\n  override def messageType = \"completion\"\n\n  override def result = None\n\n  override def isSlotFree = Some(instance)\n\n  override def toJson = CompletionMessage.serdes.write(this)\n\n  override def shrink = this\n\n  override def toString = activationId.asString\n}\n\n/**\n * This message is sent from an invoker to the load balancer once an action result is available for blocking actions.\n * This is part of a split phase notification, and does not indicate that the slot is available, which is indicated with\n * a `CompletionMessage`. Note that activation record will not contain any logs from the action execution, only the result.\n *\n * The constructor is private so that callers must use the more restrictive constructors which ensure the response is always\n * Right when this message is created.\n */\ncase class ResultMessage private (override val transid: TransactionId, response: Either[ActivationId, WhiskActivation])\n    extends AcknowledgementMessage(transid) {\n  override def messageType = \"result\"\n\n  override def result = Some(response)\n\n  override def isSlotFree = None\n\n  override def isSystemError = response.fold(_ => None, a => Some(a.response.isWhiskError))\n\n  override def activationId = response.fold(identity, _.activationId)\n\n  override def toJson = ResultMessage.serdes.write(this)\n\n  override def shrink = copy(response = response.flatMap(a => Left(a.activationId)))\n\n  override def toString = activationId.asString\n}\n\nobject ActivationMessage extends DefaultJsonProtocol {\n  def parse(msg: String) = Try(serdes.read(msg.parseJson))\n\n  private implicit val fqnSerdes = FullyQualifiedEntityName.serdes\n  implicit val serdes = jsonFormat12(ActivationMessage.apply)\n}\n\nobject CombinedCompletionAndResultMessage extends DefaultJsonProtocol {\n  // this constructor is restricted to ensure the message is always created with certain invariants\n  private def apply(transid: TransactionId,\n                    activation: Either[ActivationId, WhiskActivation],\n                    isSystemError: Option[Boolean],\n                    instance: InstanceId): CombinedCompletionAndResultMessage =\n    new CombinedCompletionAndResultMessage(transid, activation, isSystemError, instance)\n\n  def apply(transid: TransactionId,\n            activation: WhiskActivation,\n            instance: InstanceId): CombinedCompletionAndResultMessage =\n    new CombinedCompletionAndResultMessage(transid, Right(activation), Some(activation.response.isWhiskError), instance)\n\n  implicit private val eitherSerdes = AcknowledgementMessage.eitherResponse\n  implicit val serdes = jsonFormat4(\n    CombinedCompletionAndResultMessage\n      .apply(_: TransactionId, _: Either[ActivationId, WhiskActivation], _: Option[Boolean], _: InstanceId))\n}\n\nobject CompletionMessage extends DefaultJsonProtocol {\n  // this constructor is restricted to ensure the message is always created with certain invariants\n  private def apply(transid: TransactionId,\n                    activation: WhiskActivation,\n                    isSystemError: Option[Boolean],\n                    instance: InstanceId): CompletionMessage =\n    new CompletionMessage(transid, activation.activationId, Some(activation.response.isWhiskError), instance)\n\n  def apply(transid: TransactionId, activation: WhiskActivation, instance: InstanceId): CompletionMessage = {\n    new CompletionMessage(transid, activation.activationId, Some(activation.response.isWhiskError), instance)\n  }\n\n  implicit val serdes = jsonFormat4(\n    CompletionMessage.apply(_: TransactionId, _: ActivationId, _: Option[Boolean], _: InstanceId))\n}\n\nobject ResultMessage extends DefaultJsonProtocol {\n  // this constructor is restricted to ensure the message is always created with certain invariants\n  private def apply(transid: TransactionId, response: Either[ActivationId, WhiskActivation]): ResultMessage =\n    new ResultMessage(transid, response)\n\n  def apply(transid: TransactionId, activation: WhiskActivation): ResultMessage =\n    new ResultMessage(transid, Right(activation))\n\n  implicit private val eitherSerdes = AcknowledgementMessage.eitherResponse\n  implicit val serdes = jsonFormat2(ResultMessage.apply(_: TransactionId, _: Either[ActivationId, WhiskActivation]))\n}\n\nobject AcknowledgementMessage extends DefaultJsonProtocol {\n  def parse(msg: String): Try[AcknowledgementMessage] = Try(serdes.read(msg.parseJson))\n\n  protected[connector] val eitherResponse = new JsonFormat[Either[ActivationId, WhiskActivation]] {\n    def write(either: Either[ActivationId, WhiskActivation]) = either.fold(_.toJson, _.toJson)\n\n    def read(value: JsValue) = value match {\n      case _: JsString =>\n        // per the ActivationId serializer, an activation id is a String even if it only consists of digits\n        Left(value.convertTo[ActivationId])\n\n      case _: JsObject => Right(value.convertTo[WhiskActivation])\n      case _           => deserializationError(\"could not read ResultMessage\")\n    }\n  }\n\n  implicit val serdes = new RootJsonFormat[AcknowledgementMessage] {\n    override def write(m: AcknowledgementMessage): JsValue = m.toJson\n\n    // The field invoker is only part of CombinedCompletionAndResultMessage and CompletionMessage.\n    // If this field is part of the JSON, we try to deserialize into one of these two types,\n    // and otherwise to a ResultMessage. If all conversions fail, an error will be thrown that needs to be handled.\n    override def read(json: JsValue): AcknowledgementMessage = {\n      val JsObject(fields) = json\n      val completion = fields.contains(\"instance\")\n      val result = fields.contains(\"response\")\n      if (completion && result) {\n        json.convertTo[CombinedCompletionAndResultMessage]\n      } else if (completion) {\n        json.convertTo[CompletionMessage]\n      } else {\n        json.convertTo[ResultMessage]\n      }\n    }\n  }\n}\n\ncase class PingMessage(instance: InvokerInstanceId, isEnabled: Option[Boolean] = None) extends Message {\n  override def serialize = PingMessage.serdes.write(this).compactPrint\n\n  def invokerEnabled: Boolean = isEnabled.getOrElse(true)\n}\n\nobject PingMessage extends DefaultJsonProtocol {\n  def parse(msg: String) = Try(serdes.read(msg.parseJson))\n\n  implicit val serdes = jsonFormat(PingMessage.apply, \"name\", \"isEnabled\")\n}\n\ntrait EventMessageBody extends Message {\n  def typeName: String\n}\n\nobject EventMessageBody extends DefaultJsonProtocol {\n\n  implicit val format = new JsonFormat[EventMessageBody] {\n    def write(eventMessageBody: EventMessageBody) = eventMessageBody match {\n      case m: Metric     => m.toJson\n      case a: Activation => a.toJson\n    }\n\n    def read(value: JsValue) =\n      if (value.asJsObject.fields.contains(\"metricName\")) {\n        value.convertTo[Metric]\n      } else {\n        value.convertTo[Activation]\n      }\n  }\n}\n\ncase class Activation(name: String,\n                      activationId: String,\n                      statusCode: Int,\n                      duration: Duration,\n                      waitTime: Duration,\n                      initTime: Duration,\n                      kind: String,\n                      conductor: Boolean,\n                      memory: Int,\n                      causedBy: Option[String],\n                      size: Option[Int] = None,\n                      userDefinedStatusCode: Option[Int] = None)\n    extends EventMessageBody {\n  val typeName = Activation.typeName\n\n  override def serialize = toJson.compactPrint\n\n  def entityPath: FullyQualifiedEntityName = EntityPath(name).toFullyQualifiedEntityName\n\n  def toJson = Activation.activationFormat.write(this)\n\n  def status: String = statusForCode(statusCode)\n\n  def isColdStart: Boolean = initTime != Duration.Zero\n\n  def namespace: String = entityPath.path.root.name\n\n  def action: String = entityPath.fullPath.relativePath.get.namespace\n\n}\n\nobject Activation extends DefaultJsonProtocol {\n\n  val typeName = \"Activation\"\n\n  def parse(msg: String) = Try(activationFormat.read(msg.parseJson))\n\n  private implicit val durationFormat = new RootJsonFormat[Duration] {\n    override def write(obj: Duration): JsValue = obj match {\n      case o if o.isFinite => JsNumber(o.toMillis)\n      case _               => JsNumber.zero\n    }\n\n    override def read(json: JsValue): Duration = json match {\n      case JsNumber(n) if n <= 0 => Duration.Zero\n      case JsNumber(n)           => toDuration(n.longValue)\n    }\n  }\n\n  implicit val activationFormat =\n    jsonFormat(\n      Activation.apply _,\n      \"name\",\n      \"activationId\",\n      \"statusCode\",\n      \"duration\",\n      \"waitTime\",\n      \"initTime\",\n      \"kind\",\n      \"conductor\",\n      \"memory\",\n      \"causedBy\",\n      \"size\",\n      \"userDefinedStatusCode\")\n\n  /** Get \"StatusCode\" from result response set by action developer * */\n  def userDefinedStatusCode(result: Option[JsValue]): Option[Int] = {\n    val statusCode: Option[JsValue] = result match {\n      case Some(JsObject(fields)) =>\n        JsHelpers\n          .getFieldPath(JsObject(fields), ERROR_FIELD, \"statusCode\")\n          .orElse(JsHelpers.getFieldPath(JsObject(fields), \"statusCode\"))\n      case _ => None\n    }\n    statusCode.map {\n      case value => Try(value.convertTo[BigInt].intValue).toOption.getOrElse(BadRequest.intValue)\n    }\n  }\n\n  /** Constructs an \"Activation\" event from a WhiskActivation */\n  def from(a: WhiskActivation): Try[Activation] = {\n    for {\n      // There are no sensible defaults for these fields, so they are required. They should always be there but there is\n      // no static analysis to proof that so we're defensive here.\n      fqn <- a.annotations.getAs[String](WhiskActivation.pathAnnotation)\n      kind <- a.annotations.getAs[String](WhiskActivation.kindAnnotation)\n    } yield {\n      Activation(\n        fqn,\n        a.activationId.asString,\n        a.response.statusCode,\n        toDuration(a.duration.getOrElse(0)),\n        toDuration(a.annotations.getAs[Long](WhiskActivation.waitTimeAnnotation).getOrElse(0)),\n        toDuration(a.annotations.getAs[Long](WhiskActivation.initTimeAnnotation).getOrElse(0)),\n        kind,\n        a.annotations.getAs[Boolean](WhiskActivation.conductorAnnotation).getOrElse(false),\n        a.annotations\n          .getAs[ActionLimits](WhiskActivation.limitsAnnotation)\n          .map(_.memory.megabytes)\n          .getOrElse(0),\n        a.annotations.getAs[String](WhiskActivation.causedByAnnotation).toOption,\n        a.response.size,\n        userDefinedStatusCode(a.response.result))\n    }\n  }\n\n  def toDuration(milliseconds: Long) = new FiniteDuration(milliseconds, TimeUnit.MILLISECONDS)\n}\n\ncase class Metric(metricName: String, metricValue: Long) extends EventMessageBody {\n  val typeName = \"Metric\"\n\n  override def serialize = toJson.compactPrint\n\n  def toJson = Metric.metricFormat.write(this).asJsObject\n}\n\nobject Metric extends DefaultJsonProtocol {\n  val typeName = \"Metric\"\n\n  def parse(msg: String) = Try(metricFormat.read(msg.parseJson))\n\n  implicit val metricFormat = jsonFormat(Metric.apply _, \"metricName\", \"metricValue\")\n}\n\ncase class EventMessage(source: String,\n                        body: EventMessageBody,\n                        subject: Subject,\n                        namespace: String,\n                        userId: UUID,\n                        eventType: String,\n                        timestamp: Long = System.currentTimeMillis())\n    extends Message {\n  override def serialize = EventMessage.format.write(this).compactPrint\n}\n\nobject EventMessage extends DefaultJsonProtocol {\n  implicit val format =\n    jsonFormat(EventMessage.apply _, \"source\", \"body\", \"subject\", \"namespace\", \"userId\", \"eventType\", \"timestamp\")\n\n  def from(a: WhiskActivation, source: String, userId: UUID): Try[EventMessage] = {\n    Activation.from(a).map { body =>\n      EventMessage(source, body, a.subject, a.namespace.toString, userId, body.typeName)\n    }\n  }\n\n  def parse(msg: String) = Try(format.read(msg.parseJson))\n}\n\ncase class InvokerResourceMessage(status: String,\n                                  freeMemory: Long,\n                                  busyMemory: Long,\n                                  inProgressMemory: Long,\n                                  tags: Seq[String],\n                                  dedicatedNamespaces: Seq[String])\n    extends Message {\n\n  /**\n   * Serializes message to string. Must be idempotent.\n   */\n  override def serialize: String = InvokerResourceMessage.serdes.write(this).compactPrint\n\n  override def equals(that: Any): Boolean =\n    that match {\n      case that: InvokerResourceMessage =>\n        this.status == that.status &&\n          this.freeMemory == that.freeMemory &&\n          this.busyMemory == that.busyMemory &&\n          this.inProgressMemory == that.inProgressMemory &&\n          this.tags.toSet == that.tags.toSet &&\n          this.dedicatedNamespaces.toSet == that.dedicatedNamespaces.toSet\n\n      case _ => false\n    }\n\n  override def hashCode: Int = {\n    var result = 1\n    val prime = 31\n    result = prime * result + status.hashCode()\n    result = prime * result + freeMemory.hashCode()\n    result = prime * result + busyMemory.hashCode()\n    result = prime * result + inProgressMemory.hashCode()\n    result = prime * result + tags.hashCode()\n    result = prime * result + dedicatedNamespaces.hashCode()\n    result\n  }\n}\n\nobject InvokerResourceMessage extends DefaultJsonProtocol {\n  def parse(msg: String): Try[InvokerResourceMessage] = Try(serdes.read(msg.parseJson))\n\n  implicit val serdes =\n    jsonFormat(\n      InvokerResourceMessage.apply _,\n      \"status\",\n      \"freeMemory\",\n      \"busyMemory\",\n      \"inProgressMemory\",\n      \"tags\",\n      \"dedicatedNamespaces\")\n}\n\n/**\n * This case class is used when retrieving the snapshot of the queue status from the scheduler at a certain moment.\n * This is useful to figure out the internal status when any issue happens.\n * The following would be an example result.\n *\n * [\n * ...\n * {\n * \"data\": \"RunningData\",\n * \"fqn\": \"whisk.system/elasticsearch/status-alarm@0.0.2\",\n * \"invocationNamespace\": \"style95\",\n * \"status\": \"Running\",\n * \"waitingActivation\": 1\n * },\n * ...\n * ]\n */\nobject GetState\n\ncase class StatusData(invocationNamespace: String,\n                      fqn: String,\n                      waitingActivation: List[ActivationId],\n                      status: String,\n                      data: String)\n    extends Message {\n\n  override def serialize: String = StatusData.serdes.write(this).compactPrint\n\n}\n\nobject StatusData extends DefaultJsonProtocol {\n\n  implicit val serdes =\n    jsonFormat(StatusData.apply _, \"invocationNamespace\", \"fqn\", \"waitingActivation\", \"status\", \"data\")\n}\n\ncase class ContainerCreationMessage(override val transid: TransactionId,\n                                    invocationNamespace: String,\n                                    action: FullyQualifiedEntityName,\n                                    revision: DocRevision,\n                                    whiskActionMetaData: WhiskActionMetaData,\n                                    rootSchedulerIndex: SchedulerInstanceId,\n                                    schedulerHost: String,\n                                    rpcPort: Int,\n                                    retryCount: Int = 0,\n                                    creationId: CreationId = CreationId.generate())\n    extends ContainerMessage(transid) {\n\n  override def toJson: JsValue = ContainerCreationMessage.serdes.write(this)\n\n  override def serialize: String = toJson.compactPrint\n}\n\nobject ContainerCreationMessage extends DefaultJsonProtocol {\n  def parse(msg: String): Try[ContainerCreationMessage] = Try(serdes.read(msg.parseJson))\n\n  private implicit val fqnSerdes = FullyQualifiedEntityName.serdes\n  private implicit val instanceIdSerdes = SchedulerInstanceId.serdes\n  private implicit val byteSizeSerdes = size.serdes\n  implicit val serdes = jsonFormat10(\n    ContainerCreationMessage.apply(\n      _: TransactionId,\n      _: String,\n      _: FullyQualifiedEntityName,\n      _: DocRevision,\n      _: WhiskActionMetaData,\n      _: SchedulerInstanceId,\n      _: String,\n      _: Int,\n      _: Int,\n      _: CreationId))\n}\n\ncase class ContainerDeletionMessage(override val transid: TransactionId,\n                                    invocationNamespace: String,\n                                    action: FullyQualifiedEntityName,\n                                    revision: DocRevision,\n                                    whiskActionMetaData: WhiskActionMetaData)\n    extends ContainerMessage(transid) {\n  override def toJson: JsValue = ContainerDeletionMessage.serdes.write(this)\n\n  override def serialize: String = toJson.compactPrint\n}\n\nobject ContainerDeletionMessage extends DefaultJsonProtocol {\n  def parse(msg: String): Try[ContainerDeletionMessage] = Try(serdes.read(msg.parseJson))\n\n  private implicit val fqnSerdes = FullyQualifiedEntityName.serdes\n  private implicit val instanceIdSerdes = SchedulerInstanceId.serdes\n  private implicit val byteSizeSerdes = size.serdes\n  implicit val serdes = jsonFormat5(\n    ContainerDeletionMessage\n      .apply(_: TransactionId, _: String, _: FullyQualifiedEntityName, _: DocRevision, _: WhiskActionMetaData))\n}\n\nabstract class ContainerMessage(private val tid: TransactionId) extends Message {\n  override val transid: TransactionId = tid\n\n  override def serialize: String = ContainerMessage.serdes.write(this).compactPrint\n\n  /** Serializes the message to JSON. */\n  def toJson: JsValue\n}\n\nobject ContainerMessage extends DefaultJsonProtocol {\n  def parse(msg: String): Try[ContainerMessage] = Try(serdes.read(msg.parseJson))\n\n  implicit val serdes = new RootJsonFormat[ContainerMessage] {\n    override def write(m: ContainerMessage): JsValue = m.toJson\n\n    override def read(json: JsValue): ContainerMessage = {\n      val JsObject(fields) = json\n      val creation = fields.contains(\"creationId\")\n      if (creation) {\n        json.convertTo[ContainerCreationMessage]\n      } else {\n        json.convertTo[ContainerDeletionMessage]\n      }\n    }\n  }\n}\n\nsealed trait ContainerCreationError\n\nobject ContainerCreationError extends Enumeration {\n  import scala.language.implicitConversions\n  implicit def containerCreationErrorToString(x: ContainerCreationError): String = {\n    x match {\n      case NoAvailableInvokersError         => \"no available invoker is found\"\n      case NoAvailableResourceInvokersError => \"no available invoker with the resources is found: \"\n      case ResourceNotEnoughError           => \"invoker(s) have not enough resources\"\n      case WhiskError                       => \"whisk error(recoverable) happens\"\n      case UnknownError                     => \"a unknown error happens\"\n      case TimeoutError                     => \"a timeout error happens\"\n      case ShuttingDownError                => \"shutting down error happens\"\n      case NonExecutableActionError         => \"no executable found for the action\"\n      case DBFetchError                     => \"an error happens while fetching data from DB\"\n      case BlackBoxError                    => \"a blackbox error happens\"\n      case ZeroNamespaceLimit               => \"the namespace has 0 limit configured\"\n      case TooManyConcurrentRequests        => \"too many concurrent requests are in flight.\"\n      case InvalidActionLimitError          => \"a configured action limit is invalid.\"\n    }\n  }\n\n  case object NoAvailableInvokersError extends ContainerCreationError\n\n  case object NoAvailableResourceInvokersError extends ContainerCreationError\n\n  case object ResourceNotEnoughError extends ContainerCreationError\n\n  case object WhiskError extends ContainerCreationError\n\n  case object UnknownError extends ContainerCreationError\n\n  case object TimeoutError extends ContainerCreationError\n\n  case object ShuttingDownError extends ContainerCreationError\n\n  case object NonExecutableActionError extends ContainerCreationError\n\n  case object DBFetchError extends ContainerCreationError\n\n  case object BlackBoxError extends ContainerCreationError\n\n  case object ZeroNamespaceLimit extends ContainerCreationError\n\n  case object TooManyConcurrentRequests extends ContainerCreationError\n\n  case object InvalidActionLimitError extends ContainerCreationError\n\n  val whiskErrors: Set[ContainerCreationError] =\n    Set(\n      NoAvailableInvokersError,\n      NoAvailableResourceInvokersError,\n      ResourceNotEnoughError,\n      WhiskError,\n      ShuttingDownError,\n      UnknownError,\n      TimeoutError,\n      ZeroNamespaceLimit)\n\n  private def parse(name: String) = name.toUpperCase match {\n    case \"NOAVAILABLEINVOKERSERROR\"         => NoAvailableInvokersError\n    case \"NOAVAILABLERESOURCEINVOKERSERROR\" => NoAvailableResourceInvokersError\n    case \"RESOURCENOTENOUGHERROR\"           => ResourceNotEnoughError\n    case \"NONEXECUTBLEACTIONERROR\"          => NonExecutableActionError\n    case \"DBFETCHERROR\"                     => DBFetchError\n    case \"WHISKERROR\"                       => WhiskError\n    case \"BLACKBOXERROR\"                    => BlackBoxError\n    case \"TIMEOUTERROR\"                     => TimeoutError\n    case \"ZERONAMESPACELIMIT\"               => ZeroNamespaceLimit\n    case \"TOOMANYCONCURRENTREQUESTS\"        => TooManyConcurrentRequests\n    case \"UNKNOWNERROR\"                     => UnknownError\n    case \"INVALIDACTIONLIMITERROR\"          => InvalidActionLimitError\n  }\n\n  implicit val serds = new RootJsonFormat[ContainerCreationError] {\n    override def write(error: ContainerCreationError): JsValue = JsString(error.toString)\n\n    override def read(json: JsValue): ContainerCreationError =\n      Try {\n        val JsString(str) = json\n        ContainerCreationError.parse(str.trim.toUpperCase)\n      } getOrElse {\n        throw deserializationError(\"ContainerCreationError must be a valid string\")\n      }\n  }\n}\n\ncase class ContainerCreationAckMessage(override val transid: TransactionId,\n                                       creationId: CreationId,\n                                       invocationNamespace: String,\n                                       action: FullyQualifiedEntityName,\n                                       revision: DocRevision,\n                                       actionMetaData: WhiskActionMetaData,\n                                       rootInvokerIndex: InvokerInstanceId,\n                                       schedulerHost: String,\n                                       rpcPort: Int,\n                                       retryCount: Int = 0,\n                                       error: Option[ContainerCreationError] = None,\n                                       reason: Option[String] = None)\n    extends Message {\n\n  /**\n   * Serializes message to string. Must be idempotent.\n   */\n  override def serialize: String = ContainerCreationAckMessage.serdes.write(this).compactPrint\n}\n\nobject ContainerCreationAckMessage extends DefaultJsonProtocol {\n  def parse(msg: String): Try[ContainerCreationAckMessage] = Try(serdes.read(msg.parseJson))\n\n  private implicit val fqnSerdes = FullyQualifiedEntityName.serdes\n  private implicit val byteSizeSerdes = size.serdes\n  implicit val serdes = jsonFormat12(ContainerCreationAckMessage.apply)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/connector/MessageConsumer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.connector\n\nimport scala.annotation.tailrec\nimport scala.collection.immutable\nimport scala.concurrent.Future\nimport scala.concurrent.blocking\nimport scala.concurrent.duration._\nimport scala.util.Failure\nimport org.apache.kafka.clients.consumer.CommitFailedException\nimport org.apache.pekko.actor.FSM\nimport org.apache.pekko.pattern.pipe\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\n\ntrait MessageConsumer {\n\n  /** The maximum number of messages peeked (i.e., max number of messages retrieved during a long poll). */\n  val maxPeek: Int\n\n  /**\n   * Gets messages via a long poll. May or may not remove messages\n   * from the message connector. Use commit() to ensure messages are\n   * removed from the connector.\n   *\n   * @param duration for the long poll\n   * @return iterable collection (topic, partition, offset, bytes)\n   */\n  def peek(duration: FiniteDuration, retry: Int = 3): Iterable[(String, Int, Long, Array[Byte])]\n\n  /**\n   * Commits offsets from last peek operation to ensure they are removed\n   * from the connector.\n   */\n  def commit(retry: Int = 3): Unit\n\n  /** Closes consumer. */\n  def close(): Unit\n\n}\n\nobject MessageFeed {\n  protected sealed trait FeedState\n  protected[connector] case object Idle extends FeedState\n  protected[connector] case object FillingPipeline extends FeedState\n  protected[connector] case object DrainingPipeline extends FeedState\n\n  protected sealed trait FeedData\n  private case object NoData extends FeedData\n\n  /** Indicates the consumer is ready to accept messages from the message bus for processing. */\n  object Ready\n\n  /** Steady state message, indicates capacity in downstream process to receive more messages. */\n  object Processed\n\n  /** Indicates the fill operation has completed. */\n  private case class FillCompleted(messages: Seq[(String, Int, Long, Array[Byte])])\n}\n\n/**\n * This actor polls the message bus for new messages and dispatches them to the given\n * handler. The actor tracks the number of messages dispatched and will not dispatch new\n * messages until some number of them are acknowledged.\n *\n * This is used by the invoker to pull messages from the message bus and apply back pressure\n * when the invoker does not have resources to complete processing messages (i.e., no containers\n * are available to run new actions). It is also used in the load balancer to consume active\n * ack messages.\n * When the invoker releases resources (by reclaiming containers) it will send a message\n * to this actor which will then attempt to fill the pipeline with new messages.\n *\n * The actor tries to fill the pipeline with additional messages while the number\n * of outstanding requests is below the pipeline fill threshold.\n */\n@throws[IllegalArgumentException]\nclass MessageFeed(description: String,\n                  logging: Logging,\n                  consumer: MessageConsumer,\n                  maximumHandlerCapacity: Int,\n                  longPollDuration: FiniteDuration,\n                  handler: Array[Byte] => Future[Unit],\n                  autoStart: Boolean = true,\n                  logHandoff: Boolean = true)\n    extends FSM[MessageFeed.FeedState, MessageFeed.FeedData] {\n  import MessageFeed._\n\n  // double-buffer to make up for message bus read overhead\n  val maxPipelineDepth = maximumHandlerCapacity * 2\n  private val pipelineFillThreshold = maxPipelineDepth - consumer.maxPeek\n\n  require(\n    consumer.maxPeek <= maxPipelineDepth,\n    \"consumer may not yield more messages per peek than permitted by max depth\")\n\n  // Immutable Queue\n  // although on the surface it seems to make sense to use an immutable variable with a mutable Queue,\n  // Pekko Actor state defies the usual \"prefer immutable\" guideline in Scala, esp. w/ Collections.\n  // If, for some reason, this Queue was mutable and is accidentally leaked in say an Pekko message,\n  // another Actor or recipient would be able to mutate the internal state of this Actor.\n  // Best practice dictates a mutable variable pointing at an immutable collection for this reason\n  private var outstandingMessages = immutable.Queue.empty[(String, Int, Long, Array[Byte])]\n  private var handlerCapacity = maximumHandlerCapacity\n\n  private implicit val tid = TransactionId.dispatcher\n\n  logging.info(\n    this,\n    s\"handler capacity = $maximumHandlerCapacity, pipeline fill at = $pipelineFillThreshold, pipeline depth = $maxPipelineDepth\")\n\n  when(Idle) {\n    case Event(Ready, _) =>\n      fillPipeline()\n      goto(FillingPipeline)\n\n    case _ => stay\n  }\n\n  // wait for fill to complete, and keep filling if there is\n  // capacity otherwise wait to drain\n  when(FillingPipeline) {\n    case Event(Processed, _) =>\n      updateHandlerCapacity()\n      sendOutstandingMessages()\n      stay\n\n    case Event(FillCompleted(messages), _) =>\n      outstandingMessages = outstandingMessages ++ messages\n      sendOutstandingMessages()\n\n      if (shouldFillQueue()) {\n        fillPipeline()\n        stay\n      } else {\n        goto(DrainingPipeline)\n      }\n\n    case _ => stay\n  }\n\n  when(DrainingPipeline) {\n    case Event(Processed, _) =>\n      updateHandlerCapacity()\n      sendOutstandingMessages()\n      if (shouldFillQueue()) {\n        fillPipeline()\n        goto(FillingPipeline)\n      } else stay\n\n    case _ => stay\n  }\n\n  onTransition { case _ -> Idle => if (autoStart) self ! Ready }\n  startWith(Idle, MessageFeed.NoData)\n  initialize()\n\n  private implicit val ec = context.system.dispatchers.lookup(\"dispatchers.kafka-dispatcher\")\n\n  private def fillPipeline(): Unit = {\n    if (outstandingMessages.size <= pipelineFillThreshold) {\n      Future {\n        blocking {\n          // Grab next batch of messages and commit offsets immediately\n          // essentially marking the activation as having satisfied \"at most once\"\n          // semantics (this is the point at which the activation is considered started).\n          // If the commit fails, then messages peeked are peeked again on the next poll.\n          // While the commit is synchronous and will block until it completes, at steady\n          // state with enough buffering (i.e., maxPipelineDepth > maxPeek), the latency\n          // of the commit should be masked.\n          val records = consumer.peek(longPollDuration)\n          consumer.commit()\n          FillCompleted(records.toSeq)\n        }\n      }.andThen {\n          case Failure(e: CommitFailedException) =>\n            logging.error(this, s\"failed to commit $description consumer offset: $e\")\n          case Failure(e: Throwable) => logging.error(this, s\"exception while pulling new $description records: $e\")\n        }\n        .recover {\n          case _ => FillCompleted(Seq.empty)\n        }\n        .pipeTo(self)\n    } else {\n      logging.error(this, s\"dropping fill request until $description feed is drained\")\n    }\n  }\n\n  /** Send as many messages as possible to the handler. */\n  @tailrec\n  private def sendOutstandingMessages(): Unit = {\n    val occupancy = outstandingMessages.size\n    if (occupancy > 0 && handlerCapacity > 0) {\n      // Easiest way with an immutable queue to cleanly dequeue\n      // Head is the first elemeent of the queue, desugared w/ an assignment pattern\n      // Tail is everything but the first element, thus mutating the collection variable\n      val (topic, partition, offset, bytes) = outstandingMessages.head\n      outstandingMessages = outstandingMessages.tail\n\n      if (logHandoff) logging.debug(this, s\"processing $topic[$partition][$offset] ($occupancy/$handlerCapacity)\")\n      handler(bytes)\n      handlerCapacity -= 1\n\n      sendOutstandingMessages()\n    }\n  }\n\n  private def shouldFillQueue(): Boolean = {\n    val occupancy = outstandingMessages.size\n    if (occupancy <= pipelineFillThreshold) {\n      logging.debug(\n        this,\n        s\"$description pipeline has capacity: $occupancy <= $pipelineFillThreshold ($handlerCapacity)\")\n      true\n    } else {\n      logging.debug(this, s\"$description pipeline must drain: $occupancy > $pipelineFillThreshold\")\n      false\n    }\n  }\n\n  private def updateHandlerCapacity(): Int = {\n    logging.debug(self, s\"$description received processed msg, current capacity = $handlerCapacity\")\n\n    if (handlerCapacity < maximumHandlerCapacity) {\n      handlerCapacity += 1\n      handlerCapacity\n    } else {\n      if (handlerCapacity > maximumHandlerCapacity) logging.error(self, s\"$description capacity already at max\")\n      maximumHandlerCapacity\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/connector/MessageProducer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.connector\n\nimport scala.concurrent.Future\n\ntrait MessageProducer {\n\n  /** Count of messages sent. */\n  def sentCount(): Long\n\n  /** Sends msg to topic. This is an asynchronous operation. */\n  def send(topic: String, msg: Message, retry: Int = 0): Future[ResultMetadata]\n\n  /** Closes producer. */\n  def close(): Unit\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/connector/MessagingProvider.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.connector\n\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.concurrent.duration.DurationInt\nimport scala.concurrent.duration.FiniteDuration\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.spi.Spi\n\nimport scala.util.Try\n\n/**\n * An Spi for providing Messaging implementations.\n */\ntrait MessagingProvider extends Spi {\n  def getConsumer(\n    config: WhiskConfig,\n    groupId: String,\n    topic: String,\n    maxPeek: Int = Int.MaxValue,\n    maxPollInterval: FiniteDuration = 5.minutes)(implicit logging: Logging, actorSystem: ActorSystem): MessageConsumer\n  def getProducer(config: WhiskConfig, maxRequestSize: Option[ByteSize] = None)(\n    implicit logging: Logging,\n    actorSystem: ActorSystem): MessageProducer\n  def ensureTopic(config: WhiskConfig, topic: String, topicConfig: String, maxMessageBytes: Option[ByteSize] = None)(\n    implicit logging: Logging): Try[Unit]\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/ApacheBlockingContainerClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool\n\nimport java.net.NoRouteToHostException\nimport java.nio.charset.StandardCharsets\nimport java.time.Instant\n\nimport org.apache.commons.io.IOUtils\nimport org.apache.http.{HttpHeaders, NoHttpResponseException}\nimport org.apache.http.client.config.RequestConfig\nimport org.apache.http.client.methods.{HttpPost, HttpRequestBase}\nimport org.apache.http.client.utils.{HttpClientUtils, URIBuilder}\nimport org.apache.http.conn.HttpHostConnectException\nimport org.apache.http.entity.StringEntity\nimport org.apache.http.impl.NoConnectionReuseStrategy\nimport org.apache.http.impl.client.HttpClientBuilder\nimport org.apache.http.impl.conn.PoolingHttpClientConnectionManager\nimport org.apache.http.util.EntityUtils\nimport spray.json._\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.ActivationResponse._\nimport org.apache.openwhisk.core.entity.{ActivationEntityLimit, ByteSize}\nimport org.apache.openwhisk.core.entity.size.SizeLong\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.annotation.tailrec\nimport scala.concurrent._\nimport scala.concurrent.{Await, ExecutionContext, Future}\nimport scala.concurrent.duration._\nimport scala.util.{Failure, Success, Try}\nimport scala.util.control.NoStackTrace\n\n// Used internally to wrap all exceptions for which the request can be retried\nprotected[containerpool] case class RetryableConnectionError(t: Throwable) extends Exception(t) with NoStackTrace\n\n/**\n * This HTTP client is used only in the invoker to communicate with the action container.\n * It allows to POST a JSON object and receive JSON object back; that is the\n * content type and the accept headers are both 'application/json.\n * The reason we still use this class for the action container is a mysterious hang\n * in the Pekko http client where a future fails to properly timeout and we have not\n * determined why that is.\n *\n * @param hostname the host name\n * @param timeout the timeout in msecs to wait for a response\n * @param maxConcurrent the maximum number of concurrent requests allowed (Default is 1)\n */\nprotected class ApacheBlockingContainerClient(hostname: String, timeout: FiniteDuration, maxConcurrent: Int = 1)(\n  implicit logging: Logging,\n  ec: ExecutionContext)\n    extends ContainerClient {\n\n  /**\n   * Closes the HttpClient and all resources allocated by it.\n   *\n   * This will close the HttpClient that is generated for this instance of ApacheBlockingContainerClient. That will also cause the\n   * ConnectionManager to be closed alongside.\n   */\n  def close(): Future[Unit] = Future.successful(HttpClientUtils.closeQuietly(connection))\n\n  /**\n   * Posts to hostname/endpoint the given JSON object.\n   * Waits up to timeout before aborting on a good connection.\n   * If the endpoint is not ready, retry up to timeout.\n   * Every retry reduces the available timeout so that this method should not\n   * wait longer than the total timeout (within a small slack allowance).\n   *\n   * @param endpoint the path the api call relative to hostname\n   * @param body the JSON value to post (this is usually a JSON objecT)\n   * @param maxResponse the maximum size in bytes the connection will accept\n   * @param retry whether or not to retry on connection failure\n   * @return Left(Error Message) or Right(Status Code, Response as UTF-8 String)\n   */\n  def post(\n    endpoint: String,\n    body: JsValue,\n    maxResponse: ByteSize,\n    truncation: ByteSize,\n    retry: Boolean,\n    reschedule: Boolean = false)(implicit tid: TransactionId): Future[Either[ContainerHttpError, ContainerResponse]] = {\n    val entity = new StringEntity(body.compactPrint, StandardCharsets.UTF_8)\n    entity.setContentType(\"application/json\")\n\n    val request = new HttpPost(baseUri.setPath(endpoint).build)\n    request.addHeader(HttpHeaders.ACCEPT, \"application/json\")\n    request.setEntity(entity)\n\n    Future {\n      blocking {\n        execute(request, timeout, maxConcurrent, maxResponse, truncation, retry, reschedule)\n      }\n    }\n  }\n\n  // Annotation will make the compiler complain if no tail recursion is possible\n  @tailrec private def execute(\n    request: HttpRequestBase,\n    timeout: FiniteDuration,\n    maxConcurrent: Int,\n    maxResponse: ByteSize,\n    truncation: ByteSize,\n    retry: Boolean,\n    reschedule: Boolean = false)(implicit tid: TransactionId): Either[ContainerHttpError, ContainerResponse] = {\n    val start = Instant.now\n\n    Try(connection.execute(request)).map { response =>\n      val containerResponse = Option(response.getEntity)\n        .map { entity =>\n          val statusCode = response.getStatusLine.getStatusCode\n          val contentLength = entity.getContentLength\n\n          // Negative contentLength means unknown or overflow. We don't want to consume in either case.\n          if (contentLength >= 0) {\n            if (contentLength <= maxResponse.toBytes) {\n              // optimized route to consume the entire stream into a string\n              val str = EntityUtils.toString(entity, StandardCharsets.UTF_8) // consumes and closes the whole stream\n              Right(ContainerResponse(statusCode, str, None))\n            } else {\n              // only consume a bounded number of bytes according to the system limits\n              val str = new String(IOUtils.toByteArray(entity.getContent, truncation.toBytes), StandardCharsets.UTF_8)\n              EntityUtils.consumeQuietly(entity) // consume the rest of the stream to free the connection\n              Right(ContainerResponse(statusCode, str, Some(contentLength.B, maxResponse)))\n            }\n          } else {\n            EntityUtils.consumeQuietly(entity) // silently consume the whole stream to free the connection\n            Left(NoResponseReceived())\n          }\n        }\n        .getOrElse {\n          // entity is null\n          Left(NoResponseReceived())\n        }\n\n      response.close()\n      containerResponse\n    } recoverWith {\n      // The route to target socket as well as the target socket itself may need some time to be available -\n      // particularly on a loaded system.\n      // The following exceptions occur on such transient conditions. In addition, no data has been transmitted\n      // yet if these exceptions occur. For this reason, it is safe and reasonable to retry.\n      //\n      // HttpHostConnectException: no target socket is listening (yet).\n      case t: HttpHostConnectException => Failure(RetryableConnectionError(t))\n      //\n      // NoRouteToHostException: route to target host is not known (yet).\n      case t: NoRouteToHostException => Failure(RetryableConnectionError(t))\n\n      //In general with NoHttpResponseException it cannot be said if server has processed the request or not\n      //For some cases like in standalone mode setup it should be fine to retry\n      case t: NoHttpResponseException if ApacheBlockingContainerClient.clientConfig.retryNoHttpResponseException =>\n        Failure(RetryableConnectionError(t))\n    } match {\n      case Success(response)                                  => response\n      case Failure(_: RetryableConnectionError) if reschedule =>\n        //propagate as a failed future; clients can retry at a different container\n        throw ContainerHealthError(tid, request.getURI.toString)\n      case Failure(t: RetryableConnectionError) if retry =>\n        if (timeout > Duration.Zero) {\n          Thread.sleep(50) // Sleep for 50 milliseconds\n          val newTimeout = timeout - (Instant.now.toEpochMilli - start.toEpochMilli).milliseconds\n          execute(request, newTimeout, maxConcurrent, maxResponse, truncation, retry = true)\n        } else {\n          logging.warn(this, s\"POST failed with $t - no retry because timeout exceeded.\")\n          Left(Timeout(t))\n        }\n      case Failure(t: Throwable) => Left(ConnectionError(t))\n    }\n  }\n\n  private val baseUri = new URIBuilder()\n    .setScheme(\"http\")\n    .setHost(hostname)\n\n  private val httpconfig = RequestConfig.custom\n    .setConnectTimeout(timeout.toMillis.toInt)\n    .setConnectionRequestTimeout(timeout.toMillis.toInt)\n    .setSocketTimeout(timeout.toMillis.toInt)\n    .build\n\n  private val connection = HttpClientBuilder.create\n    .setDefaultRequestConfig(httpconfig)\n    // Connections are not reused by most of the available runtimes. To circumvent any issues we might have regarding\n    // connections randomly breaking due to our pause/resume cycle, we don't reuse connections at all.\n    .setConnectionReuseStrategy(new NoConnectionReuseStrategy)\n    .setConnectionManager {\n      // A PoolingHttpClientConnectionManager is the default when not specifying any ConnectionManager.\n      // The PoolingHttpClientConnectionManager has the benefit of actively checking if a connection has become stale,\n      // which is very important because pausing/resuming containers can cause a connection to become silently broken.\n      // This causes very subtle bugs, especially when containers are reused after a pretty long time (like > 5 minutes).\n      //\n      // The BasicHttpClientConnectionManager (which would be alternative here) doesn't have such a mechanism and thus\n      // isn't suitable for our usage.\n      val cm = new PoolingHttpClientConnectionManager()\n      // perRoute effectively means per host in our use-case, which means setting it to the same value as the maximum\n      // total of all connections in the pool is appropriate here.\n      cm.setDefaultMaxPerRoute(maxConcurrent)\n      cm.setMaxTotal(maxConcurrent)\n      cm\n    }\n    .useSystemProperties()\n    .disableAutomaticRetries()\n    .build\n}\n\ncase class ApacheClientConfig(retryNoHttpResponseException: Boolean)\n\nobject ApacheBlockingContainerClient {\n  val clientConfig: ApacheClientConfig = loadConfigOrThrow[ApacheClientConfig](ConfigKeys.apacheClientConfig)\n\n  /** A helper method to post one single request to a connection. Used for container tests. */\n  def post(host: String, port: Int, endPoint: String, content: JsValue)(\n    implicit logging: Logging,\n    tid: TransactionId,\n    ec: ExecutionContext): (Int, Option[JsObject]) = {\n    val timeout = 90.seconds\n    val connection = new ApacheBlockingContainerClient(s\"$host:$port\", timeout)\n    val response = executeRequest(connection, endPoint, content)\n    val result = Await.result(response, timeout)\n    connection.close()\n    result\n  }\n\n  /** A helper method to post multiple concurrent requests to a single connection. Used for container tests. */\n  def concurrentPost(host: String, port: Int, endPoint: String, contents: Seq[JsValue], timeout: Duration)(\n    implicit logging: Logging,\n    tid: TransactionId,\n    ec: ExecutionContext): Seq[(Int, Option[JsObject])] = {\n    val connection = new ApacheBlockingContainerClient(s\"$host:$port\", 90.seconds, contents.size)\n    val futureResults = contents.map { content =>\n      executeRequest(connection, endPoint, content)\n    }\n    val results = Await.result(Future.sequence(futureResults), timeout)\n    connection.close()\n    results\n  }\n\n  private def executeRequest(connection: ApacheBlockingContainerClient, endpoint: String, content: JsValue)(\n    implicit logging: Logging,\n    tid: TransactionId,\n    ec: ExecutionContext): Future[(Int, Option[JsObject])] = {\n    connection.post(\n      endpoint,\n      content,\n      ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT,\n      ActivationEntityLimit.MAX_ACTIVATION_ENTITY_TRUNCATION_LIMIT,\n      retry = true) map {\n      case Right(r)                   => (r.statusCode, Try(r.entity.parseJson.asJsObject).toOption)\n      case Left(NoResponseReceived()) => throw new IllegalStateException(\"no response from container\")\n      case Left(Timeout(_))           => throw new java.util.concurrent.TimeoutException()\n      case Left(ConnectionError(_: java.net.SocketTimeoutException)) =>\n        throw new java.util.concurrent.TimeoutException()\n      case Left(ConnectionError(t)) => throw new IllegalStateException(t.getMessage)\n    }\n\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/Container.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool\n\nimport java.time.Instant\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.util.ByteString\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json.DefaultJsonProtocol._\nimport spray.json.{JsObject, JsValue}\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.ActivationResponse.{ContainerConnectionError, ContainerResponse}\nimport org.apache.openwhisk.core.entity.{ActivationEntityLimit, ActivationResponse, ByteSize, WhiskAction}\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.concurrent.duration.{Duration, FiniteDuration, _}\nimport scala.util.{Failure, Success}\n\n/**\n * An OpenWhisk biased container abstraction. This is **not only** an abstraction\n * for different container providers, but the implementation also needs to include\n * OpenWhisk specific behavior, especially for initialize and run.\n */\ncase class ContainerId(asString: String) {\n  require(asString.nonEmpty, \"ContainerId must not be empty\")\n}\ncase class ContainerAddress(host: String, port: Int = 8080) {\n  require(host.nonEmpty, \"ContainerIp must not be empty\")\n  def asString() = s\"${host}:${port}\"\n}\n\nobject Container {\n\n  /**\n   * The action proxies insert this line in the logs at the end of each activation for stdout/stderr.\n   *\n   * Note: Blackbox containers might not add this sentinel, as we cannot be sure the action developer actually does this.\n   */\n  val ACTIVATION_LOG_SENTINEL = \"XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\"\n\n  protected[containerpool] val config: ContainerPoolConfig =\n    loadConfigOrThrow[ContainerPoolConfig](ConfigKeys.containerPool)\n}\n\n/**\n * Abstraction for Container operations.\n * Container manipulation (specifically suspend/resume/destroy) is NOT thread-safe and MUST be synchronized by caller.\n * Container access (specifically run) is thread-safe (e.g. for concurrent activation processing).\n */\ntrait Container {\n\n  implicit protected val as: ActorSystem\n  protected val id: ContainerId\n  protected[core] val addr: ContainerAddress\n  protected implicit val logging: Logging\n  protected implicit val ec: ExecutionContext\n\n  /** HTTP connection to the container, will be lazily established by callContainer */\n  protected var httpConnection: Option[ContainerClient] = None\n\n  /** maxConcurrent+timeout are cached during first init, so that resuming connections can reference */\n  protected var containerHttpMaxConcurrent: Int = 1\n  protected var containerHttpTimeout: FiniteDuration = 60.seconds\n\n  def containerId: ContainerId = id\n\n  /** Stops the container from consuming CPU cycles. NOT thread-safe - caller must synchronize. */\n  def suspend()(implicit transid: TransactionId): Future[Unit] = {\n    //close connection first, then close connection pool\n    //(testing pool recreation vs connection closing, time was similar - so using the simpler recreation approach)\n    val toClose = httpConnection\n    httpConnection = None\n    closeConnections(toClose)\n  }\n\n  /** Dual of halt. NOT thread-safe - caller must synchronize.*/\n  def resume()(implicit transid: TransactionId): Future[Unit] = {\n    httpConnection = Some(openConnections(containerHttpTimeout, containerHttpMaxConcurrent))\n    Future.successful({})\n  }\n\n  /** Obtains logs up to a given threshold from the container. Optionally waits for a sentinel to appear. */\n  def logs(limit: ByteSize, waitForSentinel: Boolean)(implicit transid: TransactionId): Source[ByteString, Any]\n\n  /** Completely destroys this instance of the container. */\n  def destroy()(implicit transid: TransactionId): Future[Unit] = {\n    closeConnections(httpConnection)\n  }\n\n  /** Initializes code in the container. */\n  def initialize(initializer: JsObject,\n                 timeout: FiniteDuration,\n                 maxConcurrent: Int,\n                 entity: Option[WhiskAction] = None)(implicit transid: TransactionId): Future[Interval] = {\n    val start = transid.started(\n      this,\n      LoggingMarkers.INVOKER_ACTIVATION_INIT,\n      s\"sending initialization to $id $addr\",\n      logLevel = InfoLevel)\n    containerHttpMaxConcurrent = maxConcurrent\n    containerHttpTimeout = timeout\n    val body = JsObject(\"value\" -> initializer)\n    callContainer(\n      \"/init\",\n      body,\n      timeout,\n      maxConcurrent,\n      ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT,\n      ActivationEntityLimit.MAX_ACTIVATION_ENTITY_TRUNCATION_LIMIT,\n      retry = true)\n      .andThen { // never fails\n        case Success(r: RunResult) =>\n          transid.finished(\n            this,\n            start.copy(start = r.interval.start),\n            s\"initialization result: ${r.toBriefString}\",\n            endTime = r.interval.end,\n            logLevel = InfoLevel)\n        case Failure(t) =>\n          transid.failed(this, start, s\"initialization failed with $t\")\n      }\n      .flatMap { result =>\n        // if runtime container is shutting down, reschedule the activation message\n        result.response.right.map { res =>\n          if (res.shuttingDown) {\n            throw ContainerHealthError(transid, containerId.asString)\n          }\n        }\n\n        if (result.ok) {\n          Future.successful(result.interval)\n        } else if (result.interval.duration >= timeout) {\n          Future.failed(\n            InitializationError(\n              result.interval,\n              ActivationResponse.developerError(Messages.timedoutActivation(timeout, true))))\n        } else {\n          Future.failed(\n            InitializationError(\n              result.interval,\n              ActivationResponse.processInitResponseContent(result.response, logging)))\n        }\n      }\n  }\n\n  /** Runs code in the container. Thread-safe - caller may invoke concurrently for concurrent activation processing. */\n  def run(parameters: JsValue,\n          environment: JsObject,\n          timeout: FiniteDuration,\n          maxConcurrent: Int,\n          maxResponse: ByteSize,\n          truncation: ByteSize,\n          reschedule: Boolean = false)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n    val actionName = environment.fields.get(\"action_name\").map(_.convertTo[String]).getOrElse(\"\")\n    val start =\n      transid.started(\n        this,\n        LoggingMarkers.INVOKER_ACTIVATION_RUN,\n        s\"sending arguments to $actionName at $id $addr\",\n        logLevel = InfoLevel)\n\n    val parameterWrapper = JsObject(\"value\" -> parameters)\n    val body = JsObject(parameterWrapper.fields ++ environment.fields)\n    callContainer(\"/run\", body, timeout, maxConcurrent, maxResponse, truncation, retry = false, reschedule)\n      .andThen { // never fails\n        case Success(r: RunResult) =>\n          transid.finished(\n            this,\n            start.copy(start = r.interval.start),\n            s\"running result: ${r.toBriefString}\",\n            endTime = r.interval.end,\n            logLevel = InfoLevel)\n        case Failure(t) =>\n          transid.failed(this, start, s\"run failed with $t\")\n      }\n      .map { result =>\n        // if runtime container is shutting down, reschedule the activation message\n        result.response.right.map { res =>\n          if (res.shuttingDown) {\n            throw ContainerHealthError(transid, containerId.asString)\n          }\n        }\n\n        val response = if (result.interval.duration >= timeout) {\n          ActivationResponse.developerError(Messages.timedoutActivation(timeout, false))\n        } else {\n          ActivationResponse.processRunResponseContent(result.response, logging)\n        }\n\n        (result.interval, response)\n      }\n  }\n\n  /**\n   * Makes an HTTP request to the container.\n   *\n   * Note that `http.post` will not throw an exception, hence the generated Future cannot fail.\n   *\n   * @param path relative path to use in the http request\n   * @param body body to send\n   * @param timeout timeout of the request\n   * @param retry whether or not to retry the request\n   * @param reschedule throw a reschedule error in case of connection failure\n   */\n  protected def callContainer(path: String,\n                              body: JsObject,\n                              timeout: FiniteDuration,\n                              maxConcurrent: Int,\n                              maxResponse: ByteSize,\n                              truncation: ByteSize,\n                              retry: Boolean = false,\n                              reschedule: Boolean = false)(implicit transid: TransactionId): Future[RunResult] = {\n    val started = Instant.now()\n    val http = httpConnection.getOrElse {\n      val conn = openConnections(timeout, maxConcurrent)\n      httpConnection = Some(conn)\n      conn\n    }\n    http\n      .post(path, body, maxResponse, truncation, retry, reschedule)\n      .map { response =>\n        val finished = Instant.now()\n        RunResult(Interval(started, finished), response)\n      }\n  }\n  private def openConnections(timeout: FiniteDuration, maxConcurrent: Int) = {\n    if (Container.config.pekkoClient) {\n      new PekkoContainerClient(addr.host, addr.port, timeout, 1024)\n    } else {\n      new ApacheBlockingContainerClient(s\"${addr.host}:${addr.port}\", timeout, maxConcurrent)\n    }\n  }\n  private def closeConnections(toClose: Option[ContainerClient]): Future[Unit] = {\n    toClose.map(_.close()).getOrElse(Future.successful(()))\n  }\n\n  /** This is so that we can easily log the container id during ContainerPool.logContainerStart().\n   *  Null check is here since some tests use stub[Container] so id is null during those tests. */\n  override def toString() = if (id == null) \"no-container-id\" else id.toString\n}\n\n/** Indicates a general error with the container */\nsealed abstract class ContainerError(msg: String) extends Exception(msg)\n\n/** Indicates an error while starting a container */\nsealed abstract class ContainerStartupError(msg: String) extends ContainerError(msg)\n\n/** Indicates any error while starting a container either of a managed runtime or a non-application-specific blackbox container */\ncase class WhiskContainerStartupError(msg: String) extends ContainerStartupError(msg)\n\n/** Indicates an application-specific error while starting a blackbox container */\ncase class BlackboxStartupError(msg: String) extends ContainerStartupError(msg)\n\n/** Indicates an error while initializing a container */\ncase class InitializationError(interval: Interval, response: ActivationResponse) extends Exception(response.toString)\n\n/** Indicates a connection error after resuming a container */\ncase class ContainerHealthError(tid: TransactionId, msg: String) extends Exception(msg)\n\ncase class Interval(start: Instant, end: Instant) {\n  def duration = Duration.create(end.toEpochMilli() - start.toEpochMilli(), MILLISECONDS)\n}\n\ncase class RunResult(interval: Interval, response: Either[ContainerConnectionError, ContainerResponse]) {\n  def ok = response.exists(_.ok)\n  def toBriefString = response.fold(_.toString, _.toString)\n}\n\nobject Interval {\n\n  /** An interval starting now with zero duration. */\n  def zero = {\n    val now = Instant.now\n    Interval(now, now)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool\n\nimport scala.concurrent.Future\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.ActivationResponse.ContainerHttpError\nimport org.apache.openwhisk.core.entity.ActivationResponse._\nimport org.apache.openwhisk.core.entity.ByteSize\n\ntrait ContainerClient {\n  def post(endpoint: String,\n           body: JsValue,\n           maxResponse: ByteSize,\n           truncation: ByteSize,\n           retry: Boolean,\n           reschedule: Boolean)(implicit tid: TransactionId): Future[Either[ContainerHttpError, ContainerResponse]]\n  def close(): Future[Unit]\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.entity.{ByteSize, ExecManifest, ExecutableWhiskAction, InvokerInstanceId}\nimport org.apache.openwhisk.spi.Spi\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration.FiniteDuration\nimport scala.math.{max, round}\n\ncase class ContainerArgsConfig(network: String,\n                               dnsServers: Seq[String] = Seq.empty,\n                               dnsSearch: Seq[String] = Seq.empty,\n                               dnsOptions: Seq[String] = Seq.empty,\n                               extraEnvVars: Seq[String] = Seq.empty,\n                               extraArgs: Map[String, Set[String]] = Map.empty) {\n\n  val extraEnvVarMap: Map[String, String] =\n    extraEnvVars.flatMap {\n      _.split(\"=\", 2) match {\n        case Array(key)        => Some(key -> \"\")\n        case Array(key, value) => Some(key -> value)\n        case _                 => None\n      }\n    }.toMap\n}\n\ncase class ContainerPoolConfig(userMemory: ByteSize,\n                               concurrentPeekFactor: Double,\n                               pekkoClient: Boolean,\n                               prewarmExpirationCheckInitDelay: FiniteDuration,\n                               prewarmExpirationCheckInterval: FiniteDuration,\n                               prewarmExpirationCheckIntervalVariance: Option[FiniteDuration],\n                               prewarmExpirationLimit: Int,\n                               prewarmMaxRetryLimit: Int,\n                               prewarmPromotion: Boolean,\n                               memorySyncInterval: FiniteDuration,\n                               batchDeletionSize: Int,\n                               userCpus: Option[Double] = None,\n                               prewarmContainerCreationConfig: Option[PrewarmContainerCreationConfig] = None) {\n  require(\n    concurrentPeekFactor > 0 && concurrentPeekFactor <= 1.0,\n    s\"concurrentPeekFactor must be > 0 and <= 1.0; was $concurrentPeekFactor\")\n\n  require(prewarmExpirationCheckInterval.toSeconds > 0, \"prewarmExpirationCheckInterval must be > 0\")\n  require(batchDeletionSize > 0, \"batch deletion size must be > 0\")\n  require(userCpus.forall(_ > 0), \"userCpus must be > 0\")\n\n  /**\n   * The shareFactor indicates the number of containers that would share a single core, on average.\n   * cpuShare is a docker option (-c) whereby a container's CPU access is limited.\n   * A value of 1024 is the full share so a strict resource division with a shareFactor of 2 would yield 512.\n   * On an idle/underloaded system, a container will still get to use underutilized CPU shares.\n   */\n  private val totalShare = 1024.0 // This is a pre-defined value coming from docker and not our hard-coded value.\n  // Grant more CPU to a container if it allocates more memory.\n  def cpuShare(reservedMemory: ByteSize) =\n    max((totalShare / (userMemory.toBytes / reservedMemory.toBytes)).toInt, 2) // The minimum allowed cpu-shares is 2\n\n  private val minContainerCpus = 0.01 // The minimum cpus allowed by docker is 0.01\n  private val roundingMultiplier = 100000\n  def cpuLimit(reservedMemory: ByteSize): Option[Double] = {\n    userCpus.map(c => {\n      val containerCpus = c / (userMemory.toBytes / reservedMemory.toBytes)\n      val roundedContainerCpus = round(containerCpus * roundingMultiplier).toDouble / roundingMultiplier // Only use decimal precision of 5\n      max(roundedContainerCpus, minContainerCpus)\n    })\n  }\n}\n\ncase class PrewarmContainerCreationConfig(maxConcurrent: Int, creationDelay: FiniteDuration) {\n  require(maxConcurrent > 0, \"maxConcurrent for per invoker must be > 0\")\n  require(creationDelay.toSeconds > 0, \"creationDelay must be > 0\")\n}\n\ncase class RuntimesRegistryCredentials(user: String, password: String)\n\ncase class RuntimesRegistryConfig(url: String, credentials: Option[RuntimesRegistryCredentials])\n\n/**\n * An abstraction for Container creation\n */\ntrait ContainerFactory {\n\n  /**\n   * Create a new Container\n   *\n   * The created container has to satisfy following requirements:\n   * - The container's file system is based on the provided action image and may have a read/write layer on top.\n   *   Some managed action runtimes may need the capability to write files.\n   * - If the specified image is not available on the system, it is pulled from an image\n   *   repository - for example, Docker Hub.\n   * - The container needs a network setup - usually, a network interface - such that the invoker is able\n   *   to connect the action container. The container must be able to perform DNS resolution based\n   *   on the settings provided via ContainerArgsConfig. If needed by action authors,\n   *   the container should be able to connect to other systems or even the internet to consume services.\n   * - The IP address of said interface is stored in the created Container instance if you want to use\n   *   the standard init / run behaviour defined in the Container trait.\n   * - The default process specified in the action image is run.\n   * - It is desired that all stdout / stderr written by processes in the container is captured such\n   *   that it can be obtained using the logs() method of the Container trait.\n   * - It is desired that the container supports and enforces the specified memory limit and CPU shares.\n   *   In particular, action memory limits rely on the underlying container technology.\n   */\n  def createContainer(\n    tid: TransactionId,\n    name: String,\n    actionImage: ExecManifest.ImageName,\n    userProvidedImage: Boolean,\n    memory: ByteSize,\n    cpuShares: Int,\n    cpuLimit: Option[Double],\n    action: Option[ExecutableWhiskAction])(implicit config: WhiskConfig, logging: Logging): Future[Container] = {\n    createContainer(tid, name, actionImage, userProvidedImage, memory, cpuShares, cpuLimit)\n  }\n\n  def createContainer(tid: TransactionId,\n                      name: String,\n                      actionImage: ExecManifest.ImageName,\n                      userProvidedImage: Boolean,\n                      memory: ByteSize,\n                      cpuShares: Int,\n                      cpuLimit: Option[Double])(implicit config: WhiskConfig, logging: Logging): Future[Container]\n\n  /** perform any initialization */\n  def init(): Unit\n\n  /** cleanup any remaining Containers; should block until complete; should ONLY be run at startup/shutdown */\n  def cleanup(): Unit\n}\n\nobject ContainerFactory {\n\n  /** based on https://github.com/moby/moby/issues/3138 and https://github.com/moby/moby/blob/master/daemon/names/names.go */\n  private def isAllowed(c: Char) = c.isLetterOrDigit || c == '_' || c == '.' || c == '-'\n\n  /** include the instance name, if specified and strip invalid chars before attempting to use them in the container name */\n  def containerNamePrefix(instanceId: InvokerInstanceId): String =\n    s\"wsk${instanceId.uniqueName.getOrElse(\"\")}${instanceId.toInt}\".filter(isAllowed)\n\n  def resolveRegistryConfig(userProvidedImage: Boolean,\n                            runtimesRegistryConfig: RuntimesRegistryConfig,\n                            userImagesRegistryConfig: RuntimesRegistryConfig): RuntimesRegistryConfig = {\n    if (userProvidedImage) userImagesRegistryConfig else runtimesRegistryConfig\n  }\n}\n\n/**\n * An SPI for ContainerFactory creation\n * All impls should use the parameters specified as additional args to \"docker run\" commands\n */\ntrait ContainerFactoryProvider extends Spi {\n  def instance(actorSystem: ActorSystem,\n               logging: Logging,\n               config: WhiskConfig,\n               instance: InvokerInstanceId,\n               parameters: Map[String, Set[String]]): ContainerFactory\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/PekkoContainerClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.marshalling.Marshal\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.headers.Accept\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport org.apache.pekko.stream.StreamTcpException\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.util.ByteString\nimport org.apache.openwhisk.common.LoggingMarkers.CONTAINER_CLIENT_RETRIES\nimport org.apache.openwhisk.common.{Logging, MetricEmitter, TransactionId}\nimport org.apache.openwhisk.core.entity.ActivationResponse.{ContainerHttpError, _}\nimport org.apache.openwhisk.core.entity.size.SizeLong\nimport org.apache.openwhisk.core.entity.{ActivationEntityLimit, ByteSize}\nimport org.apache.openwhisk.http.PoolingRestClient\nimport pureconfig.loadConfigOrThrow\nimport spray.json._\n\nimport java.time.Instant\nimport scala.concurrent.{Await, ExecutionContext, Future, TimeoutException}\nimport scala.concurrent.duration._\nimport scala.util.Try\nimport scala.util.control.NonFatal\n\n/**\n * This HTTP client is used only in the invoker to communicate with the action container.\n * It allows to POST a JSON object and receive JSON object back; that is the\n * content type and the accept headers are both 'application/json.\n * This implementation uses the pekko http host-level client API.\n * NOTE: Keepalive is disabled to prevent issues with paused containers\n *\n * @param hostname the host name\n * @param port the port\n * @param timeout the timeout in msecs to wait for a response\n * @param queueSize once all connections are used, how big of queue to allow for additional requests\n * @param retryInterval duration between retries for TCP connection errors\n */\nprotected class PekkoContainerClient(\n  hostname: String,\n  port: Int,\n  timeout: FiniteDuration,\n  queueSize: Int,\n  retryInterval: FiniteDuration = 100.milliseconds)(implicit logging: Logging, as: ActorSystem, ec: ExecutionContext)\n    extends PoolingRestClient(\"http\", hostname, port, queueSize, timeout = Some(timeout))(as, ec)\n    with ContainerClient {\n\n  def close() = shutdown()\n\n  /**\n   * Posts to hostname/endpoint the given JSON object.\n   * Waits up to timeout before aborting on a good connection.\n   * If the endpoint is not ready, retry up to timeout.\n   * Every retry reduces the available timeout so that this method should not\n   * wait longer than the total timeout (within a small slack allowance).\n   *\n   * @param endpoint the path the api call relative to hostname\n   * @param body the JSON value to post (this is usually a JSON objecT)\n   * @param maxResponse the maximum size in bytes the connection will accept\n   * @param truncation the truncation size in bytes\n   * @param retry whether or not to retry on connection failure\n   * @param reschedule whether or not to throw ContainerHealthError (triggers reschedule) on connection failure\n   * @return Left(Error Message) or Right(Status Code, Response as UTF-8 String)\n   */\n  def post(\n    endpoint: String,\n    body: JsValue,\n    maxResponse: ByteSize,\n    truncation: ByteSize,\n    retry: Boolean,\n    reschedule: Boolean = false)(implicit tid: TransactionId): Future[Either[ContainerHttpError, ContainerResponse]] = {\n\n    //create the request\n    val req = Marshal(body).to[MessageEntity].map { b =>\n      HttpRequest(HttpMethods.POST, endpoint, entity = b)\n        .withHeaders(Accept(MediaTypes.`application/json`))\n    }\n\n    retryingRequest(req, timeout, retry, reschedule, endpoint)\n      .flatMap {\n        case (response, retries) => {\n          if (retries > 0) {\n            logging.debug(this, s\"completed request to $endpoint after $retries retries\")\n            MetricEmitter.emitHistogramMetric(CONTAINER_CLIENT_RETRIES, retries)\n          }\n\n          response.entity.contentLengthOption match {\n            case Some(contentLength) if response.status != StatusCodes.NoContent =>\n              if (contentLength <= maxResponse.toBytes) {\n                Unmarshal(response.entity.withSizeLimit(maxResponse.toBytes)).to[String].map { o =>\n                  Right(ContainerResponse(response.status.intValue, o, None))\n                }\n              } else {\n                truncated(truncation, response.entity.dataBytes).map { s =>\n                  Right(ContainerResponse(response.status.intValue, s, Some(contentLength.B, maxResponse)))\n                }\n              }\n            case _ =>\n              //handle missing Content-Length as NoResponseReceived\n              //also handle 204 as NoResponseReceived, for parity with ApacheBlockingContainerClient client\n              //per https://github.com/akka/akka-http/issues/1459, don't use discardEntityBytes!\n              //(discardEntityBytes was causing failures in WskUnicodeTests)\n              response.entity.dataBytes.runWith(Sink.ignore).map(_ => Left(NoResponseReceived()))\n          }\n        }\n      }\n      .recoverWith {\n        case t: TimeoutException =>\n          Future.successful(Left(Timeout(t)))\n        case t: ContainerHealthError =>\n          //propagate as a failed future; clients can retry at a different container\n          Future.failed(t)\n        case NonFatal(t) =>\n          Future.successful(Left(ConnectionError(t)))\n      }\n  }\n  //returns a Future HttpResponse -> Int (where Int is the retryCount)\n  private def retryingRequest(req: Future[HttpRequest],\n                              timeout: FiniteDuration,\n                              retry: Boolean,\n                              reschedule: Boolean,\n                              endpoint: String,\n                              retryCount: Int = 0)(implicit tid: TransactionId): Future[(HttpResponse, Int)] = {\n    val start = Instant.now\n\n    request(req)\n      .map((_, retryCount))\n      .recoverWith {\n        case _: StreamTcpException if reschedule =>\n          Future.failed(ContainerHealthError(tid, endpoint))\n        case t: StreamTcpException if retry =>\n          if (timeout > Duration.Zero) {\n            org.apache.pekko.pattern.after(retryInterval, as.scheduler)({\n              val newTimeout = timeout - (Instant.now.toEpochMilli - start.toEpochMilli).milliseconds\n              retryingRequest(req, newTimeout, retry, reschedule, endpoint, retryCount + 1)\n            })\n          } else {\n            logging.warn(\n              this,\n              s\"POST failed after $retryCount retries with $t - no more retries because timeout exceeded.\")\n            Future.failed(new TimeoutException(t.getMessage))\n          }\n      }\n  }\n\n  private def truncated(truncation: ByteSize,\n                        responseBytes: Source[ByteString, _],\n                        previouslyCaptured: ByteString = ByteString.empty): Future[String] = {\n    responseBytes.prefixAndTail(1).runWith(Sink.head).flatMap {\n      case (Nil, tail) =>\n        //ignore the tail (MUST CONSUME ENTIRE ENTITY!)\n        tail.runWith(Sink.ignore).map(_ => previouslyCaptured.utf8String)\n      case (Seq(prefix), tail) =>\n        val truncatedResponse = previouslyCaptured ++ prefix\n        if (truncatedResponse.size < truncation.toBytes) {\n          truncated(truncation, tail, truncatedResponse)\n        } else {\n          //ignore the tail (MUST CONSUME ENTIRE ENTITY!)\n          //captured string MAY be larger than the truncation size, so take only truncation bytes to get the exact length\n          tail.runWith(Sink.ignore).map(_ => truncatedResponse.take(truncation.toBytes.toInt).utf8String)\n        }\n    }\n  }\n}\n\nobject PekkoContainerClient {\n  private val queueSize = loadConfigOrThrow[Int](\"pekko.http.host-connection-pool.max-connections\")\n\n  /** A helper method to post one single request to a connection. Used for container tests. */\n  def post(host: String, port: Int, endPoint: String, content: JsValue, timeout: FiniteDuration)(\n    implicit logging: Logging,\n    as: ActorSystem,\n    ec: ExecutionContext,\n    tid: TransactionId): (Int, Option[JsObject]) = {\n    val connection = new PekkoContainerClient(host, port, timeout, 1)\n    val response = executeRequest(connection, endPoint, content)\n    val result = Await.result(response, timeout + 10.seconds) //additional timeout to complete futures\n    connection.close()\n    result\n  }\n\n  /** A helper method to post one single request to a connection. Used for container tests. */\n  def postForJsArray(host: String, port: Int, endPoint: String, content: JsValue, timeout: FiniteDuration)(\n    implicit logging: Logging,\n    as: ActorSystem,\n    ec: ExecutionContext,\n    tid: TransactionId): (Int, Option[JsArray]) = {\n    val connection = new PekkoContainerClient(host, port, timeout, 1)\n    val response = executeRequestForJsArray(connection, endPoint, content)\n    val result = Await.result(response, timeout + 10.seconds) //additional timeout to complete futures\n    connection.close()\n    result\n  }\n\n  /** A helper method to post multiple concurrent requests to a single connection. Used for container tests. */\n  def concurrentPost(host: String, port: Int, endPoint: String, contents: Seq[JsValue], timeout: FiniteDuration)(\n    implicit logging: Logging,\n    tid: TransactionId,\n    as: ActorSystem,\n    ec: ExecutionContext): Seq[(Int, Option[JsObject])] = {\n    val connection = new PekkoContainerClient(host, port, timeout, queueSize)\n    val futureResults = contents.map { executeRequest(connection, endPoint, _) }\n    val results = Await.result(Future.sequence(futureResults), timeout + 10.seconds) //additional timeout to complete futures\n    connection.close()\n    results\n  }\n\n  private def executeRequest(connection: PekkoContainerClient, endpoint: String, content: JsValue)(\n    implicit logging: Logging,\n    as: ActorSystem,\n    ec: ExecutionContext,\n    tid: TransactionId): Future[(Int, Option[JsObject])] = {\n\n    val res = connection\n      .post(\n        endpoint,\n        content,\n        ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT,\n        ActivationEntityLimit.MAX_ACTIVATION_ENTITY_TRUNCATION_LIMIT,\n        true)\n      .map({\n        case Right(r)                   => (r.statusCode, Try(r.entity.parseJson.asJsObject).toOption)\n        case Left(NoResponseReceived()) => throw new IllegalStateException(\"no response from container\")\n        case Left(Timeout(_))           => throw new java.util.concurrent.TimeoutException()\n        case Left(ConnectionError(t: java.net.SocketTimeoutException)) =>\n          throw new java.util.concurrent.TimeoutException()\n        case Left(ConnectionError(t)) => throw new IllegalStateException(t.getMessage)\n      })\n\n    res\n  }\n\n  private def executeRequestForJsArray(connection: PekkoContainerClient, endpoint: String, content: JsValue)(\n    implicit logging: Logging,\n    as: ActorSystem,\n    ec: ExecutionContext,\n    tid: TransactionId): Future[(Int, Option[JsArray])] = {\n\n    val res = connection\n      .post(\n        endpoint,\n        content,\n        ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT,\n        ActivationEntityLimit.MAX_ACTIVATION_ENTITY_TRUNCATION_LIMIT,\n        retry = true)\n      .map({\n        case Right(r)                   => (r.statusCode, Try(r.entity.parseJson.convertTo[JsArray]).toOption)\n        case Left(NoResponseReceived()) => throw new IllegalStateException(\"no response from container\")\n        case Left(Timeout(_))           => throw new java.util.concurrent.TimeoutException()\n        case Left(ConnectionError(t: java.net.SocketTimeoutException)) =>\n          throw new java.util.concurrent.TimeoutException()\n        case Left(ConnectionError(t)) => throw new IllegalStateException(t.getMessage)\n      })\n\n    res\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/logging/DockerToActivationFileLogStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport java.nio.file.{Files, Path, Paths}\nimport java.nio.file.attribute.PosixFilePermission.{\n  GROUP_READ,\n  GROUP_WRITE,\n  OTHERS_READ,\n  OTHERS_WRITE,\n  OWNER_READ,\n  OWNER_WRITE\n}\nimport java.util.EnumSet\nimport java.time.Instant\n\nimport org.apache.pekko.NotUsed\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.connectors.file.scaladsl.LogRotatorSink\nimport org.apache.pekko.stream.{Graph, RestartSettings, SinkShape, UniformFanOutShape}\nimport org.apache.pekko.stream.scaladsl.{Broadcast, Flow, GraphDSL, Keep, MergeHub, RestartSink, Sink, Source}\nimport org.apache.pekko.util.ByteString\n\nimport org.apache.openwhisk.common.{PekkoLogging, TransactionId}\nimport org.apache.openwhisk.core.containerpool.Container\nimport org.apache.openwhisk.core.entity.{ActivationLogs, ExecutableWhiskAction, Identity, WhiskActivation}\nimport org.apache.openwhisk.core.entity.size._\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\n\n/**\n * Docker based implementation of a LogStore.\n *\n * Relies on docker's implementation details with regards to the JSON log-driver. When using the JSON log-driver\n * docker writes stdout/stderr to a JSON formatted file which is read by this store. Logs are written in the\n * activation record itself.\n *\n * Additionally writes logs to a separate file which can be processed by any backend service asynchronously.\n */\nclass DockerToActivationFileLogStore(system: ActorSystem, destinationDirectory: Path = Paths.get(\"logs\"))\n    extends DockerToActivationLogStore(system) {\n\n  private val logging = new PekkoLogging(system.log)\n\n  /**\n   * End of an event as written to a file. Closes the json-object and also appends a newline.\n   */\n  private val eventEnd = ByteString(\"}\\n\")\n\n  private def fieldsString(fields: Map[String, JsValue]) =\n    fields\n      .map {\n        case (key, value) => s\"\"\"\"$key\":${value.compactPrint}\"\"\"\n      }\n      .mkString(\",\")\n\n  /**\n   * Merges all file-writing streams into one globally buffered stream.\n   *\n   * This effectively decouples the time it takes to {@code collectLogs} from the time it takes to write the augmented\n   * logging data to a file on the disk.\n   *\n   * All lines are written to a rotating sink, which will create a new file, appended with the creation timestamp,\n   * once the defined limit is reached.\n   */\n  val bufferSize = 100.MB\n  val perms = EnumSet.of(OWNER_READ, OWNER_WRITE, GROUP_READ, GROUP_WRITE, OTHERS_READ, OTHERS_WRITE)\n  protected val writeToFile: Sink[ByteString, _] = MergeHub\n    .source[ByteString]\n    .batchWeighted(bufferSize.toBytes, _.length, identity)(_ ++ _)\n    .to(RestartSink.withBackoff(RestartSettings(minBackoff = 1.seconds, maxBackoff = 60.seconds, randomFactor = 0.2)) {\n      () =>\n        LogRotatorSink(() => {\n          val maxSize = bufferSize.toBytes\n          var bytesRead = maxSize\n          element =>\n            {\n              val size = element.size\n              if (bytesRead + size > maxSize) {\n                bytesRead = size\n                val logFilePath = destinationDirectory.resolve(s\"userlogs-${Instant.now.toEpochMilli}.log\")\n                logging.info(this, s\"Rotating log file to '$logFilePath'\")\n                try {\n                  Files.createFile(logFilePath)\n                  Files.setPosixFilePermissions(logFilePath, perms)\n                } catch {\n                  case t: Throwable =>\n                    logging.error(this, s\"Couldn't create userlogs file: $t\")\n                    throw t\n                }\n                Some(logFilePath)\n              } else {\n                bytesRead += size\n                None\n              }\n            }\n        })\n    })\n    .run()\n\n  override def collectLogs(transid: TransactionId,\n                           user: Identity,\n                           activation: WhiskActivation,\n                           container: Container,\n                           action: ExecutableWhiskAction): Future[ActivationLogs] = {\n\n    val logLimit = action.limits.logs\n    val isDeveloperError = activation.response.isContainerError // container error means developer error\n    val logs = logStream(transid, container, logLimit, action.exec.sentinelledLogs, isDeveloperError)\n\n    // Adding the userId field to every written record, so any background process can properly correlate.\n    val userIdField = Map(\"namespaceId\" -> user.namespace.uuid.toJson)\n\n    val additionalMetadata = Map(\n      \"activationId\" -> activation.activationId.asString.toJson,\n      \"action\" -> action.fullyQualifiedName(false).asString.toJson,\n      \"namespace\" -> user.namespace.name.asString.toJson) ++ userIdField\n\n    val augmentedActivation = JsObject(activation.toJson.fields ++ userIdField)\n\n    // Manually construct JSON fields to omit parsing the whole structure\n    val metadata = ByteString(\",\" + fieldsString(additionalMetadata))\n\n    val toSeq = Flow[ByteString].via(DockerToActivationLogStore.toFormattedString).toMat(Sink.seq[String])(Keep.right)\n    val toFile = Flow[ByteString]\n    // As each element is a JSON-object, we know we can add the manually constructed fields to it by dropping\n    // the closing \"}\", adding the fields and finally add \"}\\n\" to the end again.\n      .map(_.dropRight(1) ++ metadata ++ eventEnd)\n      // As the last element of the stream, print the activation record.\n      .concat(Source.single(ByteString(augmentedActivation.toJson.compactPrint + \"\\n\")))\n      .to(writeToFile)\n\n    val combined = OwSink.combine(toSeq, toFile)(Broadcast[ByteString](_))\n\n    logs.runWith(combined)._1.flatMap { seq =>\n      val logs = ActivationLogs(seq.toVector)\n      if (!isLogCollectingError(logs, logLimit, isDeveloperError)) {\n        Future.successful(logs)\n      } else {\n        Future.failed(LogCollectingException(logs))\n      }\n    }\n  }\n}\n\nobject DockerToActivationFileLogStoreProvider extends LogStoreProvider {\n  override def instance(actorSystem: ActorSystem): LogStore = new DockerToActivationFileLogStore(actorSystem)\n}\n\nobject OwSink {\n\n  /**\n   * Combines two sinks into one sink using the given strategy. The materialized value is a Tuple2 of the materialized\n   * values of either sink. Code basically copied from {@code Sink.combine}\n   */\n  def combine[T, U, M1, M2](first: Sink[U, M1], second: Sink[U, M2])(\n    strategy: Int => Graph[UniformFanOutShape[T, U], NotUsed]): Sink[T, (M1, M2)] = {\n    Sink.fromGraph(GraphDSL.createGraph(first, second)((_, _)) { implicit b => (s1, s2) =>\n      import GraphDSL.Implicits._\n      val d = b.add(strategy(2))\n\n      d ~> s1\n      d ~> s2\n\n      SinkShape(d.in)\n    })\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/logging/DockerToActivationLogStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport java.time.Instant\n\nimport org.apache.pekko.NotUsed\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.Sink\nimport org.apache.pekko.stream.scaladsl.Flow\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.util.ByteString\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.Container\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.core.database.UserContext\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport spray.json._\n\n/**\n * Represents a single log line as read from a docker log\n */\nprotected[core] case class LogLine(time: String, stream: String, log: String) {\n  def toFormattedString = f\"$time%-30s $stream: ${log.stripLineEnd}\"\n}\n\nprotected[core] object LogLine extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat3(LogLine.apply)\n}\n\nobject DockerToActivationLogStore {\n\n  /** Transforms chunked JsObjects into formatted strings */\n  val toFormattedString: Flow[ByteString, String, NotUsed] =\n    Flow[ByteString].map(_.utf8String.parseJson.convertTo[LogLine].toFormattedString)\n}\n\n/**\n * Docker based implementation of a LogStore.\n *\n * Relies on docker's implementation details with regards to the JSON log-driver. When using the JSON log-driver\n * docker writes stdout/stderr to a JSON formatted file which is read by this store. Logs are written in the\n * activation record itself.\n */\nclass DockerToActivationLogStore(system: ActorSystem) extends LogStore {\n  implicit val ec: ExecutionContext = system.dispatcher\n  implicit val actorSystem: ActorSystem = system\n\n  /* \"json-file\" is the log-driver that writes out to file */\n  override val containerParameters = Map(\"--log-driver\" -> Set(\"json-file\"))\n\n  /* As logs are already part of the activation record, just return that bit of it */\n  override def fetchLogs(namespace: String,\n                         activationId: ActivationId,\n                         start: Option[Instant],\n                         end: Option[Instant],\n                         activationLogs: Option[ActivationLogs],\n                         context: UserContext): Future[ActivationLogs] =\n    activationLogs match {\n      case Some(logs) => Future.successful(logs)\n      case None       => Future.failed(new RuntimeException(s\"Activation logs not available for activation ${activationId}\"))\n    }\n\n  /**\n   * Obtains the container's stdout and stderr output.\n   *\n   * Managed action runtimes are expected to produce sentinels on developer errors during\n   * init and run. For certain developer errors like process abortion due to unhandled errors\n   * or memory limit exhaustion, the action runtime will likely not be able to produce sentinels.\n   *\n   * In addition, there are situations where user actions (un)intentionally cause a developer error\n   * in a managed action runtime and prevent the production of sentinels. In that case, log file\n   * reading may continue endlessly.\n   *\n   * For these reasons, do not wait for sentinels to appear in log output when activations end up\n   * in a developer error. It is expected that sentinels are filtered in container.logs() even\n   * if they are not waited for.\n   *\n   * In case of a developer error, append a warning message to the logs that data might be missing.\n   *\n   * TODO: instead of just appending a warning message when a developer error occurs, we should\n   *       have an out-of-band error handling that injects such messages later on.\n   *\n   * @param transid transaction id\n   * @param container container to obtain the log from\n   * @param action action that defines the log limit\n   * @param isTimedoutActivation is activation timed out\n   *\n   * @return a vector of Strings with log lines in our own JSON format\n   */\n  protected def logStream(transid: TransactionId,\n                          container: Container,\n                          logLimit: LogLimit,\n                          sentinelledLogs: Boolean,\n                          isDeveloperError: Boolean): Source[ByteString, Any] = {\n\n    // Wait for a sentinel only if no container (developer) error occurred to avoid\n    // that log collection continues if the action code still logs after developer error.\n    val waitForSentinel = sentinelledLogs && !isDeveloperError\n    val logs = container.logs(logLimit.asMegaBytes, waitForSentinel)(transid)\n    val logsWithPossibleError = if (isDeveloperError) {\n      logs.concat(\n        Source.single(\n          ByteString(LogLine(Instant.now.toString, \"stderr\", Messages.logWarningDeveloperError).toJson.compactPrint)))\n    } else logs\n    logsWithPossibleError\n  }\n\n  /**\n   * Determine whether the passed activation log had a log collecting error or not.\n   * It is expected that the log collecting stream appends a message from a well known\n   * set of error messages if log collecting failed.\n   *\n   * If the activation failed due to a developer error, an additional error message is appended.\n   * In that case, the second last message indicates whether there was a log collecting error AND\n   * the last message MUST be the additional error message mentioned above.\n   *\n   * TODO: this function needs to deal with different combinations of error / warning messages that\n   *       were appended to / injected into the log collecting stream.\n   *       Instead, we should have an out-of-band error handling that does not use log messages to\n   *       detect error conditions but detects errors and appends error / warning messages in\n   *       a different way.\n   *\n   * @param actLogs the activation logs to check\n   * @param logLimit the log limit applying to the activation\n   * @param isDeveloperError did activation fail due to developer error?\n   * @return true if log collecting failed, false otherwise\n   */\n  protected def isLogCollectingError(actLogs: ActivationLogs,\n                                     logLimit: LogLimit,\n                                     isDeveloperError: Boolean): Boolean = {\n    val logs = actLogs.logs\n    val logCollectingErrorMessages = Set(Messages.logFailure, Messages.truncateLogs(logLimit.asMegaBytes))\n    val lastLine: Option[String] = logs.lastOption\n    val secondLastLine: Option[String] = logs.takeRight(2).dropRight(1).lastOption\n\n    if (isDeveloperError) {\n      // Developer error: the second last line indicates whether there was a log collecting error.\n      val secondLastLineContainsLogCollectingError =\n        secondLastLine.exists(line => logCollectingErrorMessages.exists(line.contains))\n\n      // If a developer error occurred when initializing or running an action,\n      // the last message in logs must be Messages.logWarningDeveloperError.\n      // If not, this is a log collecting error.\n      val lastLineContainsDeveloperError = lastLine.exists(line => line.contains(Messages.logWarningDeveloperError))\n\n      secondLastLineContainsLogCollectingError || !lastLineContainsDeveloperError\n    } else {\n      // The last line indicates whether there was a log collecting error.\n      lastLine.exists(line => logCollectingErrorMessages.exists(line.contains))\n    }\n  }\n\n  override def collectLogs(transid: TransactionId,\n                           user: Identity,\n                           activation: WhiskActivation,\n                           container: Container,\n                           action: ExecutableWhiskAction): Future[ActivationLogs] = {\n\n    val logLimit = action.limits.logs\n    val isDeveloperError = activation.response.isContainerError // container error means developer error\n    val logs = logStream(transid, container, logLimit, action.exec.sentinelledLogs, isDeveloperError)\n\n    logs\n      .via(DockerToActivationLogStore.toFormattedString)\n      .runWith(Sink.seq)\n      .flatMap { seq =>\n        val logs = ActivationLogs(seq.toVector)\n        if (!isLogCollectingError(logs, logLimit, isDeveloperError)) {\n          Future.successful(logs)\n        } else {\n          Future.failed(LogCollectingException(logs))\n        }\n      }\n  }\n}\n\nobject DockerToActivationLogStoreProvider extends LogStoreProvider {\n  override def instance(actorSystem: ActorSystem): LogStore = new DockerToActivationLogStore(actorSystem)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/logging/ElasticSearchLogStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport java.nio.file.{Path, Paths}\nimport java.time.Instant\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.Flow\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.openwhisk.core.entity.{ActivationId, ActivationLogs, Identity}\nimport org.apache.openwhisk.core.containerpool.logging.ElasticSearchJsonProtocol._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.UserContext\n\nimport scala.concurrent.{Future, Promise}\nimport scala.util.Try\nimport spray.json._\nimport pureconfig._\nimport pureconfig.generic.auto._\n\ncase class ElasticSearchLogFieldConfig(userLogs: String,\n                                       message: String,\n                                       tenantId: String,\n                                       activationId: String,\n                                       stream: String,\n                                       time: String)\n\ncase class ElasticSearchLogStoreConfig(protocol: String,\n                                       host: String,\n                                       port: Int,\n                                       path: String,\n                                       logSchema: ElasticSearchLogFieldConfig,\n                                       requiredHeaders: Seq[String] = Seq.empty)\n\n/**\n * ElasticSearch based implementation of a DockerToActivationFileLogStore. When using the JSON log driver, docker writes\n * stdout/stderr to JSON formatted files. Those files can be processed by a backend service asynchronously to store\n * user logs in ElasticSearch. This log store allows user logs then to be fetched from ElasticSearch.\n */\nclass ElasticSearchLogStore(\n  system: ActorSystem,\n  httpFlow: Option[Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), Any]] = None,\n  destinationDirectory: Path = Paths.get(\"logs\"),\n  elasticSearchConfig: ElasticSearchLogStoreConfig =\n    loadConfigOrThrow[ElasticSearchLogStoreConfig](ConfigKeys.logStoreElasticSearch))\n    extends DockerToActivationFileLogStore(system, destinationDirectory) {\n\n  // Schema of resultant logs from ES\n  case class UserLogEntry(message: String, stream: String, time: String) {\n    def toFormattedString = s\"${time} ${stream}: ${message.stripLineEnd}\"\n  }\n\n  object UserLogEntry extends DefaultJsonProtocol {\n    implicit val serdes =\n      jsonFormat(\n        UserLogEntry.apply,\n        elasticSearchConfig.logSchema.message,\n        elasticSearchConfig.logSchema.stream,\n        elasticSearchConfig.logSchema.time)\n  }\n\n  private val esClient = new ElasticSearchRestClient(\n    elasticSearchConfig.protocol,\n    elasticSearchConfig.host,\n    elasticSearchConfig.port,\n    httpFlow)(system, system.dispatcher)\n\n  private def transcribeLogs(queryResult: EsSearchResult): ActivationLogs =\n    ActivationLogs(queryResult.hits.hits.map(_.source.convertTo[UserLogEntry].toFormattedString))\n\n  private def extractRequiredHeaders(headers: Seq[HttpHeader]) =\n    headers.filter(h => elasticSearchConfig.requiredHeaders.contains(h.lowercaseName)).toList\n\n  private def generatePayload(namespace: String, activationId: ActivationId) = {\n    val logQuery =\n      s\"_type: ${elasticSearchConfig.logSchema.userLogs} AND ${elasticSearchConfig.logSchema.tenantId}: ${namespace} AND ${elasticSearchConfig.logSchema.activationId}: ${activationId}\"\n    val queryString = EsQueryString(logQuery)\n    val queryOrder = EsQueryOrder(elasticSearchConfig.logSchema.time, EsOrderAsc)\n\n    EsQuery(queryString, Some(queryOrder))\n  }\n\n  private def generatePath(user: Identity) = elasticSearchConfig.path.format(user.namespace.uuid.asString)\n\n  override def fetchLogs(namespace: String,\n                         activationId: ActivationId,\n                         start: Option[Instant],\n                         end: Option[Instant],\n                         activationLogs: Option[ActivationLogs],\n                         context: UserContext): Future[ActivationLogs] = {\n    val headers = extractRequiredHeaders(context.request.headers)\n\n    // Return logs from ElasticSearch, or return logs from activation if required headers are not present\n    if (headers.length == elasticSearchConfig.requiredHeaders.length) {\n      esClient\n        .search[EsSearchResult](generatePath(context.user), generatePayload(namespace, activationId), headers)\n        .flatMap {\n          case Right(queryResult) =>\n            Future.successful(transcribeLogs(queryResult))\n          case Left(code) =>\n            Future.failed(new RuntimeException(s\"Status code '$code' was returned from log store\"))\n        }\n    } else {\n      activationLogs match {\n        case Some(logs) => Future.successful(logs)\n        case None =>\n          Future.failed(new RuntimeException(s\"Activation logs not available for activation ${activationId}\"))\n      }\n    }\n  }\n}\n\nobject ElasticSearchLogStoreProvider extends LogStoreProvider {\n  override def instance(actorSystem: ActorSystem): LogStore = new ElasticSearchLogStore(actorSystem)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/logging/ElasticSearchRestClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport scala.concurrent.{ExecutionContext, Future, Promise}\nimport scala.util.{Either, Try}\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.{GET, POST}\nimport org.apache.pekko.http.scaladsl.model.headers.Accept\nimport org.apache.pekko.stream.scaladsl.Flow\n\nimport scala.util.Try\nimport spray.json._\nimport org.apache.openwhisk.http.PoolingRestClient\nimport org.apache.openwhisk.http.PoolingRestClient._\n\ntrait EsQueryMethod\ntrait EsOrder\ntrait EsRange\ntrait EsAgg\ntrait EsMatch\n\n// Schema of ES query operators\ncase object EsOrderAsc extends EsOrder { override def toString = \"asc\" }\ncase object EsOrderDesc extends EsOrder { override def toString = \"desc\" }\ncase object EsRangeGte extends EsRange { override def toString = \"gte\" }\ncase object EsRangeGt extends EsRange { override def toString = \"gt\" }\ncase object EsRangeLte extends EsRange { override def toString = \"lte\" }\ncase object EsRangeLt extends EsRange { override def toString = \"lt\" }\ncase object EsAggMax extends EsAgg { override def toString = \"max\" }\ncase object EsAggMin extends EsAgg { override def toString = \"min\" }\ncase object EsMatchPhrase extends EsMatch { override def toString = \"phrase\" }\ncase object EsMatchPhrasePrefix extends EsMatch { override def toString = \"phrase_prefix\" }\n\n// Schema of ES queries\ncase class EsQueryAggs(aggField: String, agg: EsAgg, field: String)\ncase class EsQueryRange(key: String, range: EsRange, value: String)\ncase class EsQueryBoolMatch(key: String, value: String)\ncase class EsQueryOrder(field: String, kind: EsOrder)\ncase class EsQueryAll() extends EsQueryMethod\ncase class EsQueryMust(matches: Vector[EsQueryBoolMatch], range: Vector[EsQueryRange] = Vector.empty)\n    extends EsQueryMethod\ncase class EsQueryMatch(field: String, value: String, matchType: Option[EsMatch] = None) extends EsQueryMethod\ncase class EsQueryTerm(key: String, value: String) extends EsQueryMethod\ncase class EsQueryString(queryString: String) extends EsQueryMethod\ncase class EsQuery(query: EsQueryMethod,\n                   sort: Option[EsQueryOrder] = None,\n                   size: Option[Int] = None,\n                   from: Int = 0,\n                   aggs: Option[EsQueryAggs] = None)\n\n// Schema of ES query results\ncase class EsSearchHit(source: JsObject)\ncase class EsSearchHits(hits: Vector[EsSearchHit], total: Int)\ncase class EsSearchResult(hits: EsSearchHits)\n\nobject ElasticSearchJsonProtocol extends DefaultJsonProtocol {\n\n  implicit object EsQueryMatchJsonFormat extends RootJsonFormat[EsQueryMatch] {\n    def read(query: JsValue) = ???\n    def write(query: EsQueryMatch) = {\n      val matchQuery = Map(\"query\" -> query.value.toJson) ++ query.matchType.map(m => \"type\" -> m.toString.toJson)\n      JsObject(\"match\" -> JsObject(query.field -> matchQuery.toJson))\n    }\n  }\n\n  implicit object EsQueryTermJsonFormat extends RootJsonFormat[EsQueryTerm] {\n    def read(query: JsValue) = ???\n    def write(query: EsQueryTerm) = JsObject(\"term\" -> JsObject(query.key -> query.value.toJson))\n  }\n\n  implicit object EsQueryStringJsonFormat extends RootJsonFormat[EsQueryString] {\n    def read(query: JsValue) = ???\n    def write(query: EsQueryString) =\n      JsObject(\"query_string\" -> JsObject(\"query\" -> query.queryString.toJson))\n  }\n\n  implicit object EsQueryRangeJsonFormat extends RootJsonFormat[EsQueryRange] {\n    def read(query: JsValue) = ???\n    def write(query: EsQueryRange) =\n      JsObject(\"range\" -> JsObject(query.key -> JsObject(query.range.toString -> query.value.toJson)))\n  }\n\n  implicit object EsQueryBoolMatchJsonFormat extends RootJsonFormat[EsQueryBoolMatch] {\n    def read(query: JsValue) = ???\n    def write(query: EsQueryBoolMatch) = JsObject(\"match\" -> JsObject(query.key -> query.value.toJson))\n  }\n\n  implicit object EsQueryMustJsonFormat extends RootJsonFormat[EsQueryMust] {\n    def read(query: JsValue) = ???\n    def write(query: EsQueryMust) = {\n      val boolQuery = Map(\"must\" -> query.matches.toJson) ++ Map(\"filter\" -> query.range.toJson)\n        .filter(_._2 != JsArray.empty)\n      JsObject(\"bool\" -> boolQuery.toJson)\n    }\n  }\n\n  implicit object EsQueryOrderJsonFormat extends RootJsonFormat[EsQueryOrder] {\n    def read(query: JsValue) = ???\n    def write(query: EsQueryOrder) =\n      JsArray(JsObject(query.field -> JsObject(\"order\" -> query.kind.toString.toJson)))\n  }\n\n  implicit object EsQueryAggsJsonFormat extends RootJsonFormat[EsQueryAggs] {\n    def read(query: JsValue) = ???\n    def write(query: EsQueryAggs) =\n      JsObject(query.aggField -> JsObject(query.agg.toString -> JsObject(\"field\" -> query.field.toJson)))\n  }\n\n  implicit object EsQueryAllJsonFormat extends RootJsonFormat[EsQueryAll] {\n    def read(query: JsValue) = ???\n    def write(query: EsQueryAll) = JsObject(\"match_all\" -> JsObject.empty)\n  }\n\n  implicit object EsQueryMethod extends RootJsonFormat[EsQueryMethod] {\n    def read(query: JsValue) = ???\n    def write(method: EsQueryMethod) = method match {\n      case queryTerm: EsQueryTerm     => queryTerm.toJson\n      case queryString: EsQueryString => queryString.toJson\n      case queryMatch: EsQueryMatch   => queryMatch.toJson\n      case queryMust: EsQueryMust     => queryMust.toJson\n      case queryAll: EsQueryAll       => queryAll.toJson\n    }\n  }\n\n  implicit val esQueryFormat = jsonFormat5(EsQuery.apply)\n  implicit val esSearchHitFormat = jsonFormat(EsSearchHit.apply _, \"_source\")\n  implicit val esSearchHitsFormat = jsonFormat2(EsSearchHits.apply)\n  implicit val esSearchResultFormat = jsonFormat1(EsSearchResult.apply)\n}\n\nclass ElasticSearchRestClient(\n  protocol: String,\n  host: String,\n  port: Int,\n  httpFlow: Option[Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), Any]] = None)(\n  implicit system: ActorSystem,\n  ec: ExecutionContext)\n    extends PoolingRestClient(protocol, host, port, 16 * 1024, httpFlow)(system, ec) {\n\n  import ElasticSearchJsonProtocol._\n\n  private val baseHeaders: List[HttpHeader] = List(Accept(MediaTypes.`application/json`))\n\n  def info(headers: List[HttpHeader] = List.empty): Future[Either[StatusCode, JsObject]] = {\n    requestJson[JsObject](mkRequest(GET, Uri./, headers = baseHeaders ++ headers))\n  }\n\n  def index(index: String, headers: List[HttpHeader] = List.empty): Future[Either[StatusCode, JsObject]] = {\n    requestJson[JsObject](mkRequest(GET, Uri(index), headers = baseHeaders ++ headers))\n  }\n\n  def search[T: RootJsonReader](index: String,\n                                payload: EsQuery = EsQuery(EsQueryAll()),\n                                headers: List[HttpHeader] = List.empty): Future[Either[StatusCode, T]] =\n    requestJson[T](mkJsonRequest(POST, Uri(index), payload.toJson.asJsObject, baseHeaders ++ headers))\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/logging/LogDriverLogStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport java.time.Instant\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.Container\nimport org.apache.openwhisk.core.entity.{ActivationId, ActivationLogs, ExecutableWhiskAction, Identity, WhiskActivation}\nimport org.apache.openwhisk.core.database.UserContext\n\nimport scala.concurrent.Future\n\n/**\n * Docker log driver based LogStore impl. Uses docker log driver to emit container logs to an external store.\n * Fetching logs from that external store is not provided in this trait. This SPI requires the\n * ContainerArgs.extraArgs to be used to indicate where the logs are shipped.\n * see https://docs.docker.com/config/containers/logging/configure/#configure-the-logging-driver-for-a-container\n *\n * Fetching logs here is a NOOP, but extended versions can customize fetching, e.g. from ELK or Splunk etc.\n */\nclass LogDriverLogStore(actorSystem: ActorSystem) extends LogStore {\n\n  /** Indicate --log-driver and --log-opt flags via ContainerArgsConfig.extraArgs */\n  override def containerParameters = Map.empty\n\n  override val logCollectionOutOfBand: Boolean = true\n\n  def collectLogs(transid: TransactionId,\n                  user: Identity,\n                  activation: WhiskActivation,\n                  container: Container,\n                  action: ExecutableWhiskAction): Future[ActivationLogs] =\n    Future.successful(ActivationLogs()) //no logs collected when using docker log drivers (see DockerLogStore for json-file exception)\n\n  /** no logs exposed to API/CLI using only the LogDriverLogStore; use an extended version,\n   * e.g. the SplunkLogStore to expose logs from some external source */\n  def fetchLogs(namespace: String,\n                activationId: ActivationId,\n                start: Option[Instant],\n                end: Option[Instant],\n                activationLogs: Option[ActivationLogs],\n                context: UserContext): Future[ActivationLogs] =\n    Future.successful(ActivationLogs(Vector(\"Logs are not available.\")))\n}\n\nobject LogDriverLogStoreProvider extends LogStoreProvider {\n  override def instance(actorSystem: ActorSystem) = new LogDriverLogStore(actorSystem)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/logging/LogStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport java.time.Instant\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.Container\nimport org.apache.openwhisk.core.entity.{ActivationId, ActivationLogs, ExecutableWhiskAction, Identity, WhiskActivation}\nimport org.apache.openwhisk.spi.Spi\nimport org.apache.openwhisk.core.database.UserContext\n\nimport scala.concurrent.Future\n\n/**\n * Interface to gather logs after the activation, define their way of storage and fetch them from that storage upon\n * user request.\n *\n * Lifecycle wise, log-handling runs through two steps:\n * 1. Collecting logs after an activation has run to store them in the database.\n * 2. Fetching logs from the API to use them (as a user).\n *\n * Both of those lifecycle steps can independently implemented via {@code collectLogs} and {@code fetchLogs}\n * respectively.\n *\n * The implementation can choose to not fetch logs at all but use the underlying container orchestrator to handle\n * log-storage/forwarding (via {@code containerParameters}). In this case, {@code collectLogs} would be implemented\n * as a stub, only returning a line of log hinting that logs are stored elsewhere. {@code fetchLogs} though can\n * implement the API of the backend system to be able to still show the logs of a specific activation via the OpenWhisk\n * API.\n */\ntrait LogStore {\n\n  /** Additional parameters to pass to container creation */\n  def containerParameters: Map[String, Set[String]]\n\n  /**\n   * Determines if log collection is actually done by the implementation or done out of band by the\n   * underlying container orchestrator\n   */\n  val logCollectionOutOfBand: Boolean = false\n\n  /**\n   * Collect logs after the activation has finished.\n   *\n   * This method is called after an activation has finished. The logs gathered here are stored along the activation\n   * record in the database.\n   *\n   * @param transid transaction the activation ran in\n   * @param user the user who ran the activation\n   * @param activation the activation record\n   * @param container container used by the activation\n   * @param action action that was activated\n   * @return logs for the given activation\n   */\n  def collectLogs(transid: TransactionId,\n                  user: Identity,\n                  activation: WhiskActivation,\n                  container: Container,\n                  action: ExecutableWhiskAction): Future[ActivationLogs]\n\n  /**\n   * Fetch relevant logs for the given activation from the store.\n   *\n   * This method is called when a user requests logs via the API.\n   * The \"logs\" parameter is not empty if:\n   * - the activation record exists\n   * - the logs are stored embedded in the activation record\n   *\n   * @param namespace namespace to fetch the logs for\n   * @param activationId activation to fetch the logs for\n   * @param start activation start\n   * @param end activation end\n   * @return the relevant logs\n   */\n  def fetchLogs(namespace: String,\n                activationId: ActivationId,\n                start: Option[Instant],\n                end: Option[Instant],\n                logs: Option[ActivationLogs],\n                content: UserContext): Future[ActivationLogs]\n}\n\ntrait LogStoreProvider extends Spi {\n  def instance(actorSystem: ActorSystem): LogStore\n}\n\n/** Indicates reading logs has failed either terminally or truncated logs */\ncase class LogCollectingException(partialLogs: ActivationLogs) extends Exception(\"Failed to read logs\")\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/containerpool/logging/SplunkLogStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport java.time.Instant\nimport java.time.temporal.ChronoUnit\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.ConnectionContext\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.client.RequestBuilding.Post\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.FormData\nimport org.apache.pekko.http.scaladsl.model.HttpRequest\nimport org.apache.pekko.http.scaladsl.model.HttpResponse\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport org.apache.pekko.http.scaladsl.model.Uri.Path\nimport org.apache.pekko.http.scaladsl.model.headers.Authorization\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport org.apache.pekko.stream.QueueOfferResult\nimport org.apache.pekko.stream.scaladsl.Flow\nimport org.apache.pekko.stream.scaladsl.Keep\nimport org.apache.pekko.stream.scaladsl.Sink\nimport org.apache.pekko.stream.scaladsl.Source\nimport javax.net.ssl.{SSLContext, SSLEngine}\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.Future\nimport scala.concurrent.Promise\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport spray.json._\nimport org.apache.openwhisk.common.PekkoLogging\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.{ActivationId, ActivationLogs}\nimport org.apache.openwhisk.core.database.UserContext\n\nimport scala.concurrent.duration.FiniteDuration\n\ncase class SplunkLogStoreConfig(host: String,\n                                port: Int,\n                                username: String,\n                                password: String,\n                                index: String,\n                                logTimestampField: String,\n                                logStreamField: String,\n                                logMessageField: String,\n                                namespaceField: String,\n                                activationIdField: String,\n                                queryConstraints: String,\n                                finalizeMaxTime: FiniteDuration,\n                                earliestTimeOffset: FiniteDuration,\n                                queryTimestampOffset: FiniteDuration,\n                                disableSNI: Boolean)\ncase class SplunkResponse(results: Vector[JsObject])\nobject SplunkResponseJsonProtocol extends DefaultJsonProtocol {\n  implicit val orderFormat = jsonFormat1(SplunkResponse)\n}\n\n/**\n * A Splunk based impl of LogDriverLogStore. Logs are routed to splunk via docker log driver, and retrieved via Splunk REST API\n *\n * @param actorSystem\n * @param httpFlow Optional Flow to use for HttpRequest handling (to enable stream based tests)\n */\nclass SplunkLogStore(\n  actorSystem: ActorSystem,\n  httpFlow: Option[Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), Any]] = None,\n  splunkConfig: SplunkLogStoreConfig = loadConfigOrThrow[SplunkLogStoreConfig](ConfigKeys.splunk))\n    extends LogDriverLogStore(actorSystem) {\n  implicit val as = actorSystem\n  implicit val ec = as.dispatcher\n  private val logging = new PekkoLogging(actorSystem.log)\n\n  private val splunkApi = Path / \"services\" / \"search\" / \"jobs\" //see http://docs.splunk.com/Documentation/Splunk/6.6.3/RESTREF/RESTsearch#search.2Fjobs\n\n  import SplunkResponseJsonProtocol._\n\n  val maxPendingRequests = 500\n\n  def createInsecureSslEngine(host: String, port: Int): SSLEngine = {\n    val engine = SSLContext.getDefault.createSSLEngine(host, port)\n    engine.setUseClientMode(true)\n\n    // WARNING: this creates an SSL Engine without enabling endpoint identification/verification procedures\n    // Disabling host name verification is a very bad idea, please don't unless you have a very good reason to.\n\n    engine\n  }\n\n  val defaultHttpFlow = Http().cachedHostConnectionPoolHttps[Promise[HttpResponse]](\n    host = splunkConfig.host,\n    port = splunkConfig.port,\n    connectionContext =\n      if (splunkConfig.disableSNI)\n        ConnectionContext.httpsClient(createInsecureSslEngine _)\n      else Http().defaultClientHttpsContext)\n\n  override def fetchLogs(namespace: String,\n                         activationId: ActivationId,\n                         start: Option[Instant],\n                         end: Option[Instant],\n                         logs: Option[ActivationLogs],\n                         context: UserContext): Future[ActivationLogs] = {\n\n    //example curl request:\n    //    curl -u  username:password -k https://splunkhost:port/services/search/jobs -d exec_mode=oneshot -d output_mode=json -d \"search=search index=someindex | search namespace=guest | search activation_id=a930e5ae4ad4455c8f2505d665aad282 | spath=log_message | table log_message\" -d \"earliest_time=2017-08-29T12:00:00\" -d \"latest_time=2017-10-29T12:00:00\"\n    //example response:\n    //    {\"preview\":false,\"init_offset\":0,\"messages\":[],\"fields\":[{\"name\":\"log_message\"}],\"results\":[{\"log_message\":\"some log message\"}], \"highlighted\":{}}\n    //note: splunk returns results in reverse-chronological order, therefore we include \"| reverse\" to cause results to arrive in chronological order\n    val search =\n      s\"\"\"search index=\"${splunkConfig.index}\" | search ${splunkConfig.queryConstraints} | search ${splunkConfig.namespaceField}=${namespace} | search ${splunkConfig.activationIdField}=${activationId} | spath ${splunkConfig.logMessageField} | table ${splunkConfig.logTimestampField}, ${splunkConfig.logStreamField}, ${splunkConfig.logMessageField} | reverse\"\"\"\n\n    val entity = FormData(\n      Map(\n        \"exec_mode\" -> \"oneshot\",\n        \"search\" -> search,\n        \"output_mode\" -> \"json\",\n        \"earliest_time\" -> start\n          .getOrElse(Instant.now().minus(splunkConfig.earliestTimeOffset.toSeconds, ChronoUnit.SECONDS))\n          .minusSeconds(splunkConfig.queryTimestampOffset.toSeconds)\n          .toString, //assume that activation start/end are UTC zone, and splunk events are the same\n        \"latest_time\" -> end\n          .getOrElse(Instant.now())\n          .plusSeconds(splunkConfig.queryTimestampOffset.toSeconds) //add 5s to avoid a timerange of 0 on short-lived activations\n          .toString,\n        \"max_time\" -> splunkConfig.finalizeMaxTime.toSeconds.toString //max time for the search query to run in seconds\n      )).toEntity\n\n    logging.debug(this, \"sending request\")\n    queueRequest(\n      Post(Uri(path = splunkApi))\n        .withEntity(entity)\n        .withHeaders(List(Authorization(BasicHttpCredentials(splunkConfig.username, splunkConfig.password)))))\n      .flatMap(response => {\n        logging.debug(this, s\"splunk API response ${response}\")\n        Unmarshal(response.entity)\n          .to[SplunkResponse]\n          .map(\n            r =>\n              ActivationLogs(\n                r.results\n                  .map(js => Try(toLogLine(js)))\n                  .map {\n                    case Success(s) => s\n                    case Failure(t) =>\n                      logging.debug(\n                        this,\n                        s\"The log message might have been too large \" +\n                          s\"for '${splunkConfig.index}' Splunk index and can't be retrieved, ${t.getMessage}\")\n                      s\"The log message can't be retrieved, ${t.getMessage}\"\n                  }))\n      })\n  }\n\n  private def toLogLine(l: JsObject) = //format same as org.apache.openwhisk.core.containerpool.logging.LogLine.toFormattedString\n    f\"${l.fields(splunkConfig.logTimestampField).convertTo[String]}%-30s ${l\n      .fields(splunkConfig.logStreamField)\n      .convertTo[String]}: ${l.fields(splunkConfig.logMessageField).convertTo[String].trim}\"\n\n  //based on https://pekko.apache.org/docs/pekko-http/current/client-side/host-level.html\n  // BoundedSourceQueue automatically drops new elements when full, maintaining dropNew behavior\n  val queue =\n    Source\n      .queue[(HttpRequest, Promise[HttpResponse])](maxPendingRequests)\n      .via(httpFlow.getOrElse(defaultHttpFlow))\n      .toMat(Sink.foreach({\n        case ((Success(resp), p)) => p.success(resp)\n        case ((Failure(e), p))    => p.failure(e)\n      }))(Keep.left)\n      .run()\n\n  def queueRequest(request: HttpRequest): Future[HttpResponse] = {\n    val responsePromise = Promise[HttpResponse]()\n    queue.offer(request -> responsePromise) match {\n      case QueueOfferResult.Enqueued => responsePromise.future\n      case QueueOfferResult.Dropped =>\n        Future.failed(new RuntimeException(\"Splunk API Client Queue overflowed. Try again later.\"))\n      case QueueOfferResult.Failure(ex) => Future.failed(ex)\n      case QueueOfferResult.QueueClosed =>\n        Future.failed(\n          new RuntimeException(\n            \"Splunk API Client Queue was closed (pool shut down) while running the request. Try again later.\"))\n    }\n  }\n}\n\nobject SplunkLogStoreProvider extends LogStoreProvider {\n  override def instance(actorSystem: ActorSystem) = new SplunkLogStore(actorSystem)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/ActivationFileStorage.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.nio.file.attribute.PosixFilePermission._\nimport java.nio.file.{Files, Path}\nimport java.time.Instant\nimport java.util.EnumSet\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.RestartSettings\nimport org.apache.pekko.stream.connectors.file.scaladsl.LogRotatorSink\nimport org.apache.pekko.stream.scaladsl.{Flow, MergeHub, RestartSink, Sink, Source}\nimport org.apache.pekko.util.ByteString\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.containerpool.logging.ElasticSearchJsonProtocol._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport spray.json._\n\nimport scala.concurrent.duration._\n\nclass ActivationFileStorage(logFilePrefix: String,\n                            logPath: Path,\n                            writeResultToFile: Boolean,\n                            actorSystem: ActorSystem,\n                            logging: Logging) {\n  implicit val system: ActorSystem = actorSystem\n\n  private var logFile = logPath\n  private val bufferSize = 100.MB\n  private val perms = EnumSet.of(OWNER_READ, OWNER_WRITE, GROUP_READ, GROUP_WRITE, OTHERS_READ, OTHERS_WRITE)\n  private val writeToFile: Sink[ByteString, _] = MergeHub\n    .source[ByteString]\n    .batchWeighted(bufferSize.toBytes, _.length, identity)(_ ++ _)\n    .to(RestartSink.withBackoff(RestartSettings(minBackoff = 1.seconds, maxBackoff = 60.seconds, randomFactor = 0.2)) {\n      () =>\n        LogRotatorSink(() => {\n          val maxSize = bufferSize.toBytes\n          var bytesRead = maxSize\n          element =>\n            {\n              val size = element.size\n\n              if (bytesRead + size > maxSize) {\n                logFile = logPath.resolve(s\"$logFilePrefix-${Instant.now.toEpochMilli}.log\")\n\n                logging.info(this, s\"Rotating log file to '$logFile'\")\n                createLogFile(logFile)\n                bytesRead = size\n                Some(logFile)\n              } else {\n                bytesRead += size\n                None\n              }\n            }\n        })\n    })\n    .run()\n\n  private def createLogFile(path: Path) =\n    try {\n      Files.createFile(path)\n      Files.setPosixFilePermissions(path, perms)\n    } catch {\n      case t: Throwable =>\n        logging.error(this, s\"Couldn't create user log file '$t'\")\n        throw t\n    }\n\n  private def transcribeLogs(activation: WhiskActivation, additionalFields: Map[String, JsValue]) =\n    activation.logs.logs.map { log =>\n      val line = JsObject(\n        Map(\"type\" -> \"user_log\".toJson) ++ Map(\"message\" -> log.toJson) ++ Map(\n          \"activationId\" -> activation.activationId.toJson) ++ additionalFields)\n\n      ByteString(s\"${line.compactPrint}\\n\")\n    }\n\n  private def transcribeActivation(activation: WhiskActivation,\n                                   additionalFields: Map[String, JsValue],\n                                   includeResult: Boolean) = {\n    val transactionType = Map(\"type\" -> \"activation_record\".toJson)\n    val message = Map(if (includeResult) {\n      \"message\" -> JsString(activation.response.result.getOrElse(JsNull).compactPrint)\n    } else {\n      \"message\" -> JsString(s\"Activation record '${activation.activationId}' for entity '${activation.name}'\")\n    })\n    val annotations = activation.annotations.toJsObject.fields\n    val addFields = transactionType ++ annotations ++ message ++ additionalFields\n    val removeFields = Seq(\"logs\", \"annotations\")\n    val line = activation.metadata.toExtendedJson(removeFields, addFields)\n\n    ByteString(s\"${line.compactPrint}\\n\")\n  }\n\n  def getLogFile: Path = logFile\n\n  def activationToFile(activation: WhiskActivation,\n                       context: UserContext,\n                       additionalFields: Map[String, JsValue] = Map.empty): Unit = {\n    activationToFileExtended(activation, context, additionalFields, additionalFields)\n  }\n\n  // used by external ArtifactActivationStore SPI implementation\n  def activationToFileExtended(activation: WhiskActivation,\n                               context: UserContext,\n                               additionalFieldsForLogs: Map[String, JsValue] = Map.empty,\n                               additionalFieldsForActivation: Map[String, JsValue] = Map.empty,\n                               includeResult: Boolean = writeResultToFile): Unit = {\n    val transcribedLogs = transcribeLogs(activation, additionalFieldsForLogs)\n    val transcribedActivation = transcribeActivation(activation, additionalFieldsForActivation, includeResult)\n\n    // Write each log line to file and then write the activation metadata\n    Source\n      .fromIterator(() => transcribedLogs.iterator)\n      .runWith(Flow[ByteString].concat(Source.single(transcribedActivation)).to(writeToFile))\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/ActivationStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.time.Instant\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.HttpRequest\nimport spray.json.JsObject\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.spi.Spi\nimport pureconfig.loadConfigOrThrow\n\nimport scala.concurrent.Future\n\ncase class UserContext(user: Identity, request: HttpRequest = HttpRequest())\n\ntrait ActivationStore {\n  val logging: Logging\n\n  /* DEPRECATED: disableStoreResult config is now deprecated replaced with blocking activation store level (storeBlockingResultLevel) */\n  protected val disableStoreResultConfig = loadConfigOrThrow[Boolean](ConfigKeys.disableStoreResult)\n  protected val storeBlockingResultLevelConfig = {\n    try {\n      ActivationStoreLevel.valueOf(loadConfigOrThrow[String](ConfigKeys.storeBlockingResultLevel))\n    } catch {\n      case _: Exception =>\n        val disableStoreResultConfig = loadConfigOrThrow[Boolean](ConfigKeys.disableStoreResult)\n        logging.warn(\n          this,\n          s\"The config ${ConfigKeys.disableStoreResult} being used is deprecated. Please use the replacement config ${ConfigKeys.storeBlockingResultLevel}\")\n        if (disableStoreResultConfig) ActivationStoreLevel.STORE_FAILURES else ActivationStoreLevel.STORE_ALWAYS\n    }\n  }\n  protected val storeNonBlockingResultLevelConfig =\n    ActivationStoreLevel.valueOf(loadConfigOrThrow[String](ConfigKeys.storeNonBlockingResultLevel))\n  protected val unstoredLogsEnabledConfig = loadConfigOrThrow[Boolean](ConfigKeys.unstoredLogsEnabled)\n\n  /**\n   * Checks if an activation should be stored in database and stores it.\n   *\n   * @param activation activation to store\n   * @param isBlockingActivation is activation blocking\n   * @param blockingStoreLevel do not store activation if successful and blocking\n   * @param nonBlockingStoreLevel do not store activation if successful and non-blocking\n   * @param context user and request context\n   * @param transid transaction ID for request\n   * @param notifier cache change notifier\n   * @return Future containing DocInfo related to stored activation\n   */\n  def storeAfterCheck(activation: WhiskActivation,\n                      isBlockingActivation: Boolean,\n                      blockingStoreLevel: Option[ActivationStoreLevel.Value],\n                      nonBlockingStoreLevel: Option[ActivationStoreLevel.Value],\n                      context: UserContext)(implicit transid: TransactionId,\n                                            notifier: Option[CacheChangeNotification],\n                                            logging: Logging): Future[DocInfo] = {\n    if (context.user.limits.storeActivations.getOrElse(true) &&\n        shouldStoreActivation(\n          activation.response,\n          isBlockingActivation,\n          transid.meta.extraLogging,\n          blockingStoreLevel.getOrElse(storeBlockingResultLevelConfig),\n          nonBlockingStoreLevel.getOrElse(storeNonBlockingResultLevelConfig))) {\n\n      store(activation, context)\n    } else {\n      if (unstoredLogsEnabledConfig) {\n        logging.info(\n          this,\n          s\"Explicitly NOT storing activation ${activation.activationId.asString} for action ${activation.name} from namespace ${activation.namespace.asString} with response_size=${activation.response.size\n            .getOrElse(\"0\")}B\")\n      }\n      Future.successful(DocInfo(activation.docid))\n    }\n  }\n\n  /**\n   * Stores an activation in the database.\n   *\n   * @param activation activation to store\n   * @param context user and request context\n   * @param transid transaction ID for request\n   * @param notifier cache change notifier\n   * @return Future containing DocInfo related to stored activation\n   */\n  def store(activation: WhiskActivation, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo]\n\n  /**\n   * Retrieves an activation corresponding to the specified activation ID.\n   *\n   * @param activationId ID of activation to retrieve\n   * @param context user and request context\n   * @param transid transaction ID for request\n   * @return Future containing the retrieved WhiskActivation\n   */\n  def get(activationId: ActivationId, context: UserContext)(implicit transid: TransactionId): Future[WhiskActivation]\n\n  /**\n   * Deletes an activation corresponding to the provided activation ID.\n   *\n   * @param activationId ID of activation to delete\n   * @param context user and request context\n   * @param transid transaction ID for the request\n   * @param notifier cache change notifier\n   * @return Future containing a Boolean value indication whether the activation was deleted\n   */\n  def delete(activationId: ActivationId, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[Boolean]\n\n  /**\n   * Counts the number of activations in a namespace.\n   *\n   * @param namespace namespace to query\n   * @param name entity name to query\n   * @param skip number of activations to skip\n   * @param since timestamp to retrieve activations after\n   * @param upto timestamp to retrieve activations before\n   * @param context user and request context\n   * @param transid transaction ID for request\n   * @return Future containing number of activations returned from query in JSON format\n   */\n  def countActivationsInNamespace(namespace: EntityPath,\n                                  name: Option[EntityPath] = None,\n                                  skip: Int,\n                                  since: Option[Instant] = None,\n                                  upto: Option[Instant] = None,\n                                  context: UserContext)(implicit transid: TransactionId): Future[JsObject]\n\n  /**\n   * Returns activations corresponding to provided entity name.\n   *\n   * @param namespace namespace to query\n   * @param name entity name to query\n   * @param skip number of activations to skip\n   * @param limit maximum number of activations to list\n   * @param includeDocs return document with each activation\n   * @param since timestamp to retrieve activations after\n   * @param upto timestamp to retrieve activations before\n   * @param context user and request context\n   * @param transid transaction ID for request\n   * @return When docs are not included, a Future containing a List of activations in JSON format is returned. When docs\n   *         are included, a List of WhiskActivation is returned.\n   */\n  def listActivationsMatchingName(\n    namespace: EntityPath,\n    name: EntityPath,\n    skip: Int,\n    limit: Int,\n    includeDocs: Boolean = false,\n    since: Option[Instant] = None,\n    upto: Option[Instant] = None,\n    context: UserContext)(implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]]\n\n  /**\n   * List all activations in a specified namespace.\n   *\n   * @param namespace namespace to query\n   * @param skip number of activations to skip\n   * @param limit maximum number of activations to list\n   * @param includeDocs return document with each activation\n   * @param since timestamp to retrieve activations after\n   * @param upto timestamp to retrieve activations before\n   * @param context user and request context\n   * @param transid transaction ID for request\n   * @return When docs are not included, a Future containing a List of activations in JSON format is returned. When docs\n   *         are included, a List of WhiskActivation is returned.\n   */\n  def listActivationsInNamespace(\n    namespace: EntityPath,\n    skip: Int,\n    limit: Int,\n    includeDocs: Boolean = false,\n    since: Option[Instant] = None,\n    upto: Option[Instant] = None,\n    context: UserContext)(implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]]\n\n  /**\n   * Checks if the system is configured to not store the activation in the database.\n   * Only stores activations if one of these is true:\n   * - result is an error,\n   * - a non-blocking activation\n   * - an activation in debug mode\n   * - activation stores is not disabled via a configuration parameter\n   *\n   * @param activationResponse to check\n   * @param isBlocking is blocking activation\n   * @param debugMode is logging header set to \"on\" for the invocation\n   * @param blockingStoreLevel level of activation status to store for blocking invocations\n   * @param nonBlockingStoreLevel level of activation status to store for blocking invocations\n   * @return Should the activation be stored to the database\n   */\n  private def shouldStoreActivation(activationResponse: ActivationResponse,\n                                    isBlocking: Boolean,\n                                    debugMode: Boolean,\n                                    blockingStoreLevel: ActivationStoreLevel.Value,\n                                    nonBlockingStoreLevel: ActivationStoreLevel.Value): Boolean = {\n    def shouldStoreOnLevel(storageLevel: ActivationStoreLevel.Value): Boolean = {\n      storageLevel match {\n        case ActivationStoreLevel.STORE_ALWAYS   => true\n        case ActivationStoreLevel.STORE_FAILURES => !activationResponse.isSuccess\n        case ActivationStoreLevel.STORE_FAILURES_NOT_APPLICATION_ERRORS =>\n          activationResponse.isContainerError || activationResponse.isWhiskError\n      }\n    }\n\n    debugMode || (isBlocking && shouldStoreOnLevel(blockingStoreLevel)) || (!isBlocking && shouldStoreOnLevel(\n      nonBlockingStoreLevel))\n  }\n}\n\ntrait ActivationStoreProvider extends Spi {\n  def instance(actorSystem: ActorSystem, logging: Logging): ActivationStore\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/ActivationStoreLevel.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nobject ActivationStoreLevel extends Enumeration {\n  type ActivationStoreLevel = Value\n  val STORE_ALWAYS, STORE_FAILURES, STORE_FAILURES_NOT_APPLICATION_ERRORS = Value\n\n  def valueOf(value: String): Value = {\n    values\n      .find(_.toString == value.toUpperCase())\n      .getOrElse(throw new IllegalArgumentException(s\"Invalid log level: $value\"))\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/ArtifactActivationStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.time.Instant\n\nimport org.apache.pekko.actor.ActorSystem\nimport spray.json.JsObject\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.entity._\n\nimport scala.concurrent.Future\nimport scala.util.{Failure, Success}\n\nclass ArtifactActivationStore(actorSystem: ActorSystem, override val logging: Logging) extends ActivationStore {\n\n  implicit val executionContext = actorSystem.dispatcher\n\n  private val artifactStore: ArtifactStore[WhiskActivation] =\n    WhiskActivationStore.datastore()(actorSystem, logging)\n\n  def store(activation: WhiskActivation, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo] = {\n\n    logging.debug(this, s\"recording activation '${activation.activationId}'\")\n\n    val res = WhiskActivation.put(artifactStore, activation)\n\n    res onComplete {\n      case Success(id) => logging.debug(this, s\"recorded activation\")\n      case Failure(t) =>\n        logging.error(\n          this,\n          s\"failed to record activation ${activation.activationId} with error ${t.getLocalizedMessage}\")\n    }\n\n    res\n  }\n\n  def get(activationId: ActivationId, context: UserContext)(\n    implicit transid: TransactionId): Future[WhiskActivation] = {\n    WhiskActivation.get(artifactStore, DocId(activationId.asString))\n  }\n\n  /**\n   * Here there is added overhead of retrieving the specified activation before deleting it, so this method should not\n   * be used in production or performance related code.\n   */\n  def delete(activationId: ActivationId, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[Boolean] = {\n    WhiskActivation.get(artifactStore, DocId(activationId.asString)) flatMap { doc =>\n      WhiskActivation.del(artifactStore, doc.docinfo)\n    }\n  }\n\n  def countActivationsInNamespace(namespace: EntityPath,\n                                  name: Option[EntityPath] = None,\n                                  skip: Int,\n                                  since: Option[Instant] = None,\n                                  upto: Option[Instant] = None,\n                                  context: UserContext)(implicit transid: TransactionId): Future[JsObject] = {\n    WhiskActivation.countCollectionInNamespace(\n      artifactStore,\n      name.map(p => namespace.addPath(p)).getOrElse(namespace),\n      skip,\n      since,\n      upto,\n      StaleParameter.UpdateAfter,\n      name.map(_ => WhiskActivation.filtersView).getOrElse(WhiskActivation.view))\n  }\n\n  def listActivationsMatchingName(\n    namespace: EntityPath,\n    name: EntityPath,\n    skip: Int,\n    limit: Int,\n    includeDocs: Boolean = false,\n    since: Option[Instant] = None,\n    upto: Option[Instant] = None,\n    context: UserContext)(implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]] = {\n    WhiskActivation.listActivationsMatchingName(\n      artifactStore,\n      namespace,\n      name,\n      skip,\n      limit,\n      includeDocs,\n      since,\n      upto,\n      StaleParameter.UpdateAfter)\n  }\n\n  def listActivationsInNamespace(\n    namespace: EntityPath,\n    skip: Int,\n    limit: Int,\n    includeDocs: Boolean = false,\n    since: Option[Instant] = None,\n    upto: Option[Instant] = None,\n    context: UserContext)(implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]] = {\n    WhiskActivation.listCollectionInNamespace(\n      artifactStore,\n      namespace,\n      skip,\n      limit,\n      includeDocs,\n      since,\n      upto,\n      StaleParameter.UpdateAfter)\n  }\n\n}\n\nobject ArtifactActivationStoreProvider extends ActivationStoreProvider {\n  override def instance(actorSystem: ActorSystem, logging: Logging) =\n    new ArtifactActivationStore(actorSystem, logging)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/ArtifactStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\n\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.stream.scaladsl._\nimport org.apache.pekko.util.ByteString\nimport spray.json.JsObject\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.Attachments.Attached\nimport org.apache.openwhisk.core.entity.DocInfo\n\nabstract class StaleParameter(val value: Option[String])\n\nobject StaleParameter {\n  case object Ok extends StaleParameter(Some(\"ok\"))\n  case object UpdateAfter extends StaleParameter(Some(\"update_after\"))\n  case object No extends StaleParameter(None)\n}\n\n/** Basic client to put and delete artifacts in a data store. */\ntrait ArtifactStore[DocumentAbstraction] {\n\n  /** Execution context for futures */\n  protected[core] implicit val executionContext: ExecutionContext\n\n  implicit val logging: Logging\n\n  /**\n   * Puts (saves) document to database using a future.\n   * If the operation is successful, the future completes with DocId else an appropriate exception.\n   *\n   * @param d the document to put in the database\n   * @param transid the transaction id for logging\n   * @return a future that completes either with DocId\n   */\n  protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo]\n\n  /**\n   * Deletes document from database using a future.\n   * If the operation is successful, the future completes with true.\n   *\n   * @param doc the document info for the record to delete (must contain valid id and rev)\n   * @param transid the transaction id for logging\n   * @return a future that completes true iff the document is deleted, else future is failed\n   */\n  protected[database] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean]\n\n  /**\n   * Gets document from database by id using a future.\n   * If the operation is successful, the future completes with the requested document if it exists.\n   *\n   * @param doc the document info for the record to get (must contain valid id and rev)\n   * @param attachmentHandler function to update the attachment details in document\n   * @param transid the transaction id for logging\n   * @param ma manifest for A to determine its runtime type, required by some db APIs\n   * @return a future that completes either with DocumentAbstraction if the document exists and is deserializable into desired type\n   */\n  protected[database] def get[A <: DocumentAbstraction](\n    doc: DocInfo,\n    attachmentHandler: Option[(A, Attached) => A] = None)(implicit transid: TransactionId, ma: Manifest[A]): Future[A]\n\n  /**\n   * Gets all documents from database view that match a start key, up to an end key, using a future.\n   * If the operation is successful, the promise completes with List[View] with zero or more documents.\n   *\n   * @param table the name of the table to query\n   * @param startKey to starting key to query the view for\n   * @param endKey to starting key to query the view for\n   * @param skip the number of record to skip (for pagination)\n   * @param limit the maximum number of records matching the key to return, iff > 0\n   * @param includeDocs include full documents matching query iff true (shall not be used with reduce)\n   * @param descending reverse results iff true\n   * @param reduce apply reduction associated with query to the result iff true\n   * @param stale a flag to permit a stale view result to be returned\n   * @param transid the transaction id for logging\n   * @return a future that completes with List[JsObject] of all documents from view between start and end key (list may be empty)\n   */\n  protected[core] def query(table: String,\n                            startKey: List[Any],\n                            endKey: List[Any],\n                            skip: Int,\n                            limit: Int,\n                            includeDocs: Boolean,\n                            descending: Boolean,\n                            reduce: Boolean,\n                            stale: StaleParameter)(implicit transid: TransactionId): Future[List[JsObject]]\n\n  /**\n   * Counts all documents from database view that match a start key, up to an end key, using a future.\n   * If the operation is successful, the promise completes with Long.\n   *\n   * @param table the name of the table to query\n   * @param startKey to starting key to query the view for\n   * @param endKey to starting key to query the view for\n   * @param skip the number of record to skip (for pagination)\n   * @param stale a flag to permit a stale view result to be returned\n   * @param transid the transaction id for logging\n   * @return a future that completes with Long that is the number of documents from view between start and end key (count may be zero)\n   */\n  protected[core] def count(table: String, startKey: List[Any], endKey: List[Any], skip: Int, stale: StaleParameter)(\n    implicit transid: TransactionId): Future[Long]\n\n  /**\n   * Attaches a \"file\" of type `contentType` to an existing document. The revision for the document must be set.\n   *\n   * @param update - function to transform the document with new attachment details\n   * @param oldAttachment Optional old document instance for the update scenario. It would be used to determine\n   *                      the existing attachment details.\n   */\n  protected[database] def putAndAttach[A <: DocumentAbstraction](\n    d: A,\n    update: (A, Attached) => A,\n    contentType: ContentType,\n    docStream: Source[ByteString, _],\n    oldAttachment: Option[Attached])(implicit transid: TransactionId): Future[(DocInfo, Attached)]\n\n  /**\n   * Retrieves a saved attachment, streaming it into the provided Sink.\n   */\n  protected[core] def readAttachment[T](doc: DocInfo, attached: Attached, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T]\n\n  /**\n   * Deletes all attachments linked to given document\n   */\n  protected[core] def deleteAttachments[T](doc: DocInfo)(implicit transid: TransactionId): Future[Boolean]\n\n  /** Shut it down. After this invocation, every other call is invalid. */\n  def shutdown(): Unit\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/ArtifactStoreExceptions.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nsealed abstract class ArtifactStoreException(message: String) extends Exception(message)\n\ncase class NoDocumentException(message: String) extends ArtifactStoreException(message)\n\ncase class DocumentConflictException(message: String) extends ArtifactStoreException(message)\n\ncase class DocumentTypeMismatchException(message: String) extends ArtifactStoreException(message)\n\ncase class DocumentRevisionMismatchException(message: String) extends ArtifactStoreException(message)\n\ncase class DocumentUnreadable(message: String) extends ArtifactStoreException(message)\n\ncase class GetException(message: String) extends ArtifactStoreException(message)\n\ncase class PutException(message: String) extends ArtifactStoreException(message)\n\ncase class DeleteException(message: String) extends ArtifactStoreException(message)\n\ncase class QueryException(message: String) extends ArtifactStoreException(message)\n\nsealed abstract class ArtifactStoreRuntimeException(message: String) extends RuntimeException(message)\n\ncase class UnsupportedQueryKeys(message: String) extends ArtifactStoreRuntimeException(message)\n\ncase class UnsupportedView(message: String) extends ArtifactStoreRuntimeException(message)\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/ArtifactStoreProvider.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.apache.pekko.actor.ActorSystem\nimport com.typesafe.config.ConfigFactory\nimport spray.json.RootJsonFormat\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.spi.{Spi, SpiLoader}\nimport org.apache.openwhisk.core.entity.DocumentReader\n\nimport scala.reflect.ClassTag\n\n/**\n * An Spi for providing ArtifactStore implementations\n */\ntrait ArtifactStoreProvider extends Spi {\n  def makeStore[D <: DocumentSerializer: ClassTag](useBatching: Boolean = false)(implicit jsonFormat: RootJsonFormat[D],\n                                                                                 docReader: DocumentReader,\n                                                                                 actorSystem: ActorSystem,\n                                                                                 logging: Logging): ArtifactStore[D]\n\n  protected def getAttachmentStore[D <: DocumentSerializer: ClassTag]()(implicit\n                                                                        actorSystem: ActorSystem,\n                                                                        logging: Logging): Option[AttachmentStore] = {\n    if (ConfigFactory.load().hasPath(\"whisk.spi.AttachmentStoreProvider\")) {\n      Some(SpiLoader.get[AttachmentStoreProvider].makeStore[D]())\n    } else {\n      None\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/ArtifactWithFileStorageActivationStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.nio.file.Paths\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.{DocInfo, _}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\n\nimport scala.concurrent.Future\n\ncase class ArtifactWithFileStorageActivationStoreConfig(logFilePrefix: String,\n                                                        logPath: String,\n                                                        userIdField: String,\n                                                        writeResultToFile: Boolean)\n\nclass ArtifactWithFileStorageActivationStore(\n  actorSystem: ActorSystem,\n  logging: Logging,\n  config: ArtifactWithFileStorageActivationStoreConfig =\n    loadConfigOrThrow[ArtifactWithFileStorageActivationStoreConfig](ConfigKeys.activationStoreWithFileStorage))\n    extends ArtifactActivationStore(actorSystem, logging) {\n\n  private val activationFileStorage =\n    new ActivationFileStorage(\n      config.logFilePrefix,\n      Paths.get(config.logPath),\n      config.writeResultToFile,\n      actorSystem,\n      logging)\n\n  def getLogFile = activationFileStorage.getLogFile\n\n  override def store(activation: WhiskActivation, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo] = {\n    val additionalFieldsForLogs =\n      Map(config.userIdField -> context.user.namespace.uuid.toJson, \"namespace\" -> context.user.namespace.name.toJson)\n    val additionalFieldsForActivation = Map(config.userIdField -> context.user.namespace.uuid.toJson)\n\n    activationFileStorage.activationToFileExtended(\n      activation,\n      context,\n      additionalFieldsForLogs,\n      additionalFieldsForActivation)\n    super.store(activation, context)\n  }\n\n}\n\nobject ArtifactWithFileStorageActivationStoreProvider extends ActivationStoreProvider {\n  override def instance(actorSystem: ActorSystem, logging: Logging) =\n    new ArtifactWithFileStorageActivationStore(actorSystem, logging)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/AttachmentStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.openwhisk.core.database\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.ContentType\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.util.ByteString\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.entity.DocId\nimport org.apache.openwhisk.spi.Spi\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.reflect.ClassTag\n\ntrait AttachmentStoreProvider extends Spi {\n  def makeStore[D <: DocumentSerializer: ClassTag]()(implicit\n                                                     actorSystem: ActorSystem,\n                                                     logging: Logging): AttachmentStore\n}\n\ncase class AttachResult(digest: String, length: Long)\n\ntrait AttachmentStore {\n\n  /** Identifies the store type */\n  protected[core] def scheme: String\n\n  /** Execution context for futures */\n  protected[core] implicit val executionContext: ExecutionContext\n\n  /**\n   * Attaches a \"file\" of type `contentType` to an existing document.\n   */\n  protected[core] def attach(doc: DocId, name: String, contentType: ContentType, docStream: Source[ByteString, _])(\n    implicit transid: TransactionId): Future[AttachResult]\n\n  /**\n   * Retrieves a saved attachment, streaming it into the provided Sink.\n   */\n  protected[core] def readAttachment[T](doc: DocId, name: String, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T]\n\n  /**\n   * Deletes all attachments linked to given document\n   */\n  protected[core] def deleteAttachments(doc: DocId)(implicit transid: TransactionId): Future[Boolean]\n\n  /**\n   * Deletes specific attachment.\n   */\n  protected[core] def deleteAttachment(doc: DocId, name: String)(implicit transid: TransactionId): Future[Boolean]\n\n  /** Shut it down. After this invocation, every other call is invalid. */\n  def shutdown(): Unit\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/AttachmentSupport.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.util.Base64\n\nimport org.apache.pekko.NotUsed\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.{ContentType, Uri}\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.util.ByteString\nimport spray.json.DefaultJsonProtocol\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.AttachmentSupport.MemScheme\nimport org.apache.openwhisk.core.entity.Attachments.Attached\nimport org.apache.openwhisk.core.entity.{ByteSize, DocId, DocInfo, UUID}\n\nimport scala.concurrent.{ExecutionContext, Future}\n\nobject AttachmentSupport {\n\n  /**\n   * Scheme name for attachments which are inlined\n   */\n  val MemScheme: String = \"mem\"\n}\n\ncase class InliningConfig(maxInlineSize: ByteSize)\n\n/**\n * Provides support for inlining small attachments. Inlined attachment contents are encoded as part of attachment\n * name itself.\n */\ntrait AttachmentSupport[DocumentAbstraction <: DocumentSerializer] extends DefaultJsonProtocol {\n\n  protected def executionContext: ExecutionContext\n\n  /**\n   * Attachment scheme name to use for non inlined attachments\n   */\n  protected def attachmentScheme: String\n\n  protected def inliningConfig: InliningConfig\n\n  /**\n   * Attachments having size less than this would be inlined\n   */\n  def maxInlineSize: ByteSize = inliningConfig.maxInlineSize\n\n  /**\n   * See {{ ArtifactStore#put }}\n   */\n  protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo]\n\n  /**\n   * Given a ByteString source it determines if the source can be inlined or not by returning an\n   * Either - Left(byteString) containing all the bytes from the source or Right(Source[ByteString, _])\n   * if the source is large\n   */\n  protected[database] def inlineOrAttach(docStream: Source[ByteString, _],\n                                         previousPrefix: ByteString = ByteString.empty)(\n    implicit system: ActorSystem): Future[Either[ByteString, Source[ByteString, _]]] = {\n    implicit val ec = executionContext\n    docStream.prefixAndTail(1).runWith(Sink.head).flatMap {\n      case (Nil, _) =>\n        Future.successful(Left(previousPrefix))\n      case (Seq(prefix), tail) =>\n        val completePrefix = previousPrefix ++ prefix\n        if (completePrefix.size < maxInlineSize.toBytes) {\n          inlineOrAttach(tail, completePrefix)\n        } else {\n          Future.successful(Right(tail.prepend(Source.single(completePrefix))))\n        }\n    }\n  }\n\n  /**\n   * Constructs a URI for the attachment\n   *\n   * @param bytesOrSource either byteString or byteString source\n   * @param path function to generate the attachment name for non inlined case\n   * @return constructed uri. In case of inlined attachment the uri contains base64 encoded inlined attachment content\n   */\n  protected[database] def uriOf(bytesOrSource: Either[ByteString, Source[ByteString, _]], path: => String): Uri = {\n    bytesOrSource match {\n      case Left(bytes) => Uri.from(scheme = MemScheme, path = encode(bytes))\n      case Right(_)    => uriFrom(scheme = attachmentScheme, path = path)\n    }\n  }\n\n  //Not using Uri.from due to https://github.com/akka/akka-http/issues/2080\n  protected[database] def uriFrom(scheme: String, path: String): Uri = Uri(s\"$scheme:$path\")\n\n  /**\n   * Constructs a source from inlined attachment contents\n   */\n  protected[database] def memorySource(uri: Uri): Source[ByteString, NotUsed] = {\n    require(uri.scheme == MemScheme, s\"URI $uri scheme is not $MemScheme\")\n    Source.single(ByteString(decode(uri)))\n  }\n\n  protected[database] def isInlined(uri: Uri): Boolean = uri.scheme == MemScheme\n\n  /**\n   * Computes digest for passed bytes as hex encoded string\n   */\n  protected[database] def digest(bytes: TraversableOnce[Byte]): String = {\n    val digester = StoreUtils.emptyDigest()\n    digester.update(bytes.toArray)\n    StoreUtils.encodeDigest(digester.digest())\n  }\n\n  /**\n   * Attaches the passed source content to  an {{ AttachmentStore }}\n   *\n   * @param doc document with attachment\n   * @param update function to update the `Attached` state with attachment metadata\n   * @param contentType contentType of the attachment\n   * @param docStream attachment source\n   * @param oldAttachment old attachment in case of update. Required for deleting the old attachment\n   * @param attachmentStore attachmentStore where attachment needs to be stored\n   * @return a tuple of updated document info and attachment metadata\n   */\n  protected[database] def attachToExternalStore[A <: DocumentAbstraction](doc: A,\n                                                                          update: (A, Attached) => A,\n                                                                          contentType: ContentType,\n                                                                          docStream: Source[ByteString, _],\n                                                                          oldAttachment: Option[Attached],\n                                                                          attachmentStore: AttachmentStore)(\n    implicit transid: TransactionId,\n    actorSystem: ActorSystem): Future[(DocInfo, Attached)] = {\n\n    val asJson = doc.toDocumentRecord\n    val id = asJson.fields(\"_id\").convertTo[String].trim\n\n    implicit val ec = executionContext\n\n    for {\n      bytesOrSource <- inlineOrAttach(docStream)\n      uri = uriOf(bytesOrSource, UUID().asString)\n      attached <- {\n        // Upload if cannot be inlined\n        bytesOrSource match {\n          case Left(bytes) =>\n            Future.successful(Attached(uri.toString, contentType, Some(bytes.size), Some(digest(bytes))))\n          case Right(source) =>\n            attachmentStore\n              .attach(DocId(id), uri.path.toString, contentType, source)\n              .map(r => Attached(uri.toString, contentType, Some(r.length), Some(r.digest)))\n        }\n      }\n      i1 <- put(update(doc, attached))\n\n      //Remove old attachment if it was part of attachmentStore\n      _ <- oldAttachment\n        .map { old =>\n          val oldUri = Uri(old.attachmentName)\n          if (oldUri.scheme == attachmentStore.scheme) {\n            attachmentStore.deleteAttachment(DocId(id), oldUri.path.toString)\n          } else {\n            Future.successful(true)\n          }\n        }\n        .getOrElse(Future.successful(true))\n    } yield (i1, attached)\n  }\n\n  private def encode(bytes: Seq[Byte]): String = {\n    Base64.getUrlEncoder.encodeToString(bytes.toArray)\n  }\n\n  private def decode(uri: Uri): Array[Byte] = {\n    Base64.getUrlDecoder.decode(uri.path.toString())\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/Batcher.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.collection.immutable.Queue\nimport scala.concurrent.{ExecutionContext, Future, Promise}\nimport scala.util.{Failure, Success}\nimport org.apache.pekko.stream.{CompletionStrategy, OverflowStrategy}\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\n\n/**\n * Enables batching of a type T.\n *\n * Batches are being created using a maximum batchSize. If there is back-pressure (concurrency is\n * maxed out and it is waiting for the operations to complete) a batch will be build up and\n * then handled accordingly. Batching will only happen under back-pressure so there is no latency\n * being traded off for batching in a non back-pressured case.\n *\n * The given concurrency controls how many batches are handled in parallel. (example: How many\n * batches of records are written to the database in parallel.)\n *\n * The operation-function takes a batch of T and does something to it that results in a sequence\n * of the same size of the batch. (example: Writing a batch of database records results in a sequence\n * of database responses). These results will be assigned to the relevant element in the batch.\n * (example: Putting a database record in batches will give you the database response for each record\n * respectively)\n *\n * @param batchSize maximum size of a batch\n * @param concurrency number of batches being handled in parallel\n * @param operation operation taking the batch\n * @tparam T the type to be batched\n * @tparam R return type of a single element after operation\n */\nclass Batcher[T, R](batchSize: Int, concurrency: Int, retry: Int)(operation: (Seq[T], Int) => Future[Seq[R]])(\n  implicit\n  system: ActorSystem,\n  ec: ExecutionContext) {\n\n  val cm: PartialFunction[Any, CompletionStrategy] = {\n    case Done =>\n      CompletionStrategy.immediately\n  }\n\n  private val stream = Source\n    .actorRef[(T, Promise[R])](\n      completionMatcher = cm,\n      failureMatcher = PartialFunction.empty[Any, Throwable],\n      bufferSize = Int.MaxValue,\n      overflowStrategy = OverflowStrategy.dropBuffer)\n    .batch(batchSize, Queue(_))((queue, element) => queue :+ element)\n    .mapAsyncUnordered(concurrency) { els =>\n      val elements = els.map(_._1)\n      val promises = els.map(_._2)\n\n      val f = operation(elements, retry)\n      f.onComplete {\n        case Success(results) => results.zip(promises).foreach { case (result, p) => p.success(result) }\n        case Failure(e)       => promises.foreach(_.failure(e))\n      }\n      // Recover Future to not abort stream in case of a failure\n      f.recover { case _ => () }\n    }\n    .to(Sink.ignore)\n    .run()\n\n  /**\n   * Adds an element to be batch-processed.\n   *\n   * @param el the element to process\n   * @return a future containing the response of the database for this specific element\n   */\n  def put(el: T): Future[R] = {\n    val promise = Promise[R]()\n    stream ! (el, promise)\n    promise.future\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/ConcurrentMapBackedCache.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * Cache base implementation:\n * Copyright (C) 2011-2015 the spray project <http://spray.io>\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.util.concurrent.ConcurrentMap\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.control.NonFatal\n\n/**\n * A thread-safe implementation of [[spray.caching.cache]] backed by a plain\n * [[java.util.concurrent.ConcurrentMap]].\n *\n * The implementation is entirely copied from Spray's [[spray.caching.Cache]] and\n * [[spray.caching.SimpleLruCache]] respectively, the only difference being the store type.\n * Implementation otherwise is identical.\n */\nprivate class ConcurrentMapBackedCache[V](store: ConcurrentMap[Any, Future[V]]) {\n  val cache = this\n\n  def apply(key: Any) = new Keyed(key)\n\n  class Keyed(key: Any) {\n    def apply(magnet: => ValueMagnet[V])(implicit ec: ExecutionContext): Future[V] =\n      cache.apply(\n        key,\n        () =>\n          try magnet.future\n          catch { case NonFatal(e) => Future.failed(e) })\n  }\n\n  def apply(key: Any, genValue: () => Future[V])(implicit ec: ExecutionContext): Future[V] = {\n    store.computeIfAbsent(\n      key,\n      new java.util.function.Function[Any, Future[V]]() {\n        override def apply(key: Any): Future[V] = {\n          val future = genValue()\n          future.onComplete { value =>\n            // in case of exceptions we remove the cache entry (i.e. try again later)\n            if (value.isFailure) store.remove(key, future)\n          }\n          future\n        }\n      })\n  }\n\n  def remove(key: Any) = Option(store.remove(key))\n\n  def size = store.size\n}\n\nclass ValueMagnet[V](val future: Future[V])\nobject ValueMagnet {\n  import scala.language.implicitConversions\n\n  implicit def fromAny[V](block: V): ValueMagnet[V] = fromFuture(Future.successful(block))\n  implicit def fromFuture[V](future: Future[V]): ValueMagnet[V] = new ValueMagnet(future)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/CouchDbRestClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.net.URLEncoder\nimport java.nio.charset.StandardCharsets\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.headers._\nimport org.apache.pekko.stream.scaladsl._\nimport org.apache.pekko.util.ByteString\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.http.PoolingRestClient\nimport org.apache.openwhisk.http.PoolingRestClient._\n\nimport scala.concurrent.{ExecutionContext, Future}\n\n/**\n * A client implementing the CouchDb API.\n *\n * This client only handles communication to the respective endpoints and works in a Json-in -> Json-out fashion. It's\n * up to the client to interpret the results accordingly.\n */\nclass CouchDbRestClient(protocol: String, host: String, port: Int, username: String, password: String, db: String)(\n  implicit system: ActorSystem,\n  logging: Logging)\n    extends PoolingRestClient(protocol, host, port, 16 * 1024)(\n      system,\n      system.dispatchers.lookup(\"dispatchers.couch-dispatcher\")) {\n\n  protected implicit val context: ExecutionContext = system.dispatchers.lookup(\"dispatchers.couch-dispatcher\")\n\n  // Headers common to all requests.\n  protected val baseHeaders: List[HttpHeader] =\n    List(Authorization(BasicHttpCredentials(username, password)), Accept(MediaTypes.`application/json`))\n\n  private def revHeader(forRev: String) = List(`If-Match`(EntityTagRange(EntityTag(forRev))))\n\n  // Properly encodes the potential slashes in each segment.\n  protected def uri(segments: Any*): Uri = {\n    val encodedSegments = segments.map(s => URLEncoder.encode(s.toString, StandardCharsets.UTF_8.name))\n    Uri(s\"/${encodedSegments.mkString(\"/\")}\")\n  }\n\n  // http://docs.couchdb.org/en/1.6.1/api/document/common.html#put--db-docid\n  def putDoc(id: String, doc: JsObject): Future[Either[StatusCode, JsObject]] =\n    requestJson[JsObject](mkJsonRequest(HttpMethods.PUT, uri(db, id), doc, baseHeaders))\n\n  // http://docs.couchdb.org/en/1.6.1/api/document/common.html#put--db-docid\n  def putDoc(id: String, rev: String, doc: JsObject): Future[Either[StatusCode, JsObject]] =\n    requestJson[JsObject](mkJsonRequest(HttpMethods.PUT, uri(db, id), doc, baseHeaders ++ revHeader(rev)))\n\n  // http://docs.couchdb.org/en/2.1.0/api/database/bulk-api.html#inserting-documents-in-bulk\n  def putDocs(docs: Seq[JsObject]): Future[Either[StatusCode, JsArray]] =\n    requestJson[JsArray](\n      mkJsonRequest(HttpMethods.POST, uri(db, \"_bulk_docs\"), JsObject(\"docs\" -> docs.toJson), baseHeaders))\n\n  // http://docs.couchdb.org/en/1.6.1/api/document/common.html#get--db-docid\n  def getDoc(id: String): Future[Either[StatusCode, JsObject]] =\n    requestJson[JsObject](mkRequest(HttpMethods.GET, uri(db, id), headers = baseHeaders))\n\n  // http://docs.couchdb.org/en/1.6.1/api/document/common.html#get--db-docid\n  def getDoc(id: String, rev: String): Future[Either[StatusCode, JsObject]] =\n    requestJson[JsObject](mkRequest(HttpMethods.GET, uri(db, id), headers = baseHeaders ++ revHeader(rev)))\n\n  // http://docs.couchdb.org/en/1.6.1/api/document/common.html#delete--db-docid\n  def deleteDoc(id: String, rev: String): Future[Either[StatusCode, JsObject]] =\n    requestJson[JsObject](mkRequest(HttpMethods.DELETE, uri(db, id), headers = baseHeaders ++ revHeader(rev)))\n\n  // http://docs.couchdb.org/en/1.6.1/api/ddoc/views.html\n  def executeView(designDoc: String, viewName: String)(startKey: List[Any] = Nil,\n                                                       endKey: List[Any] = Nil,\n                                                       skip: Option[Int] = None,\n                                                       limit: Option[Int] = None,\n                                                       stale: StaleParameter = StaleParameter.No,\n                                                       includeDocs: Boolean = false,\n                                                       descending: Boolean = false,\n                                                       reduce: Boolean = false,\n                                                       group: Boolean = false): Future[Either[StatusCode, JsObject]] = {\n\n    require(reduce || !group, \"Parameter 'group=true' cannot be used together with the parameter 'reduce=false'.\")\n\n    def any2json(any: Any): JsValue = any match {\n      case b: Boolean => JsBoolean(b)\n      case i: Int     => JsNumber(i)\n      case l: Long    => JsNumber(l)\n      case d: Double  => JsNumber(d)\n      case f: Float   => JsNumber(f)\n      case s: String  => JsString(s)\n      case _ =>\n        logging.warn(\n          this,\n          s\"Serializing uncontrolled type '${any.getClass}' to string in JSON conversion ('${any.toString}').\")\n        JsString(any.toString)\n    }\n\n    def list2OptJson(lst: List[Any]): Option[JsValue] = {\n      lst match {\n        case Nil => None\n        case _   => Some(JsArray(lst.map(any2json): _*))\n      }\n    }\n\n    def bool2OptStr(bool: Boolean): Option[String] = if (bool) Some(\"true\") else None\n\n    val args = Seq[(String, Option[String])](\n      \"startkey\" -> list2OptJson(startKey).map(_.toString),\n      \"endkey\" -> list2OptJson(endKey).map(_.toString),\n      \"skip\" -> skip.filter(_ > 0).map(_.toString),\n      \"limit\" -> limit.filter(_ > 0).map(_.toString),\n      \"stale\" -> stale.value,\n      \"include_docs\" -> bool2OptStr(includeDocs),\n      \"descending\" -> bool2OptStr(descending),\n      \"reduce\" -> Some(reduce.toString),\n      \"group\" -> bool2OptStr(group))\n\n    // Throw out all undefined arguments.\n    val argMap: Map[String, String] = args\n      .collect({\n        case (l, Some(r)) => (l, r)\n      })\n      .toMap\n\n    val viewUri = uri(db, \"_design\", designDoc, \"_view\", viewName).withQuery(Uri.Query(argMap))\n\n    requestJson[JsObject](mkRequest(HttpMethods.GET, viewUri, headers = baseHeaders))\n  }\n\n  // Streams an attachment to the database\n  // http://docs.couchdb.org/en/1.6.1/api/document/attachments.html#put--db-docid-attname\n  def putAttachment(id: String,\n                    rev: String,\n                    attName: String,\n                    contentType: ContentType,\n                    source: Source[ByteString, _]): Future[Either[StatusCode, JsObject]] = {\n    val entity = HttpEntity.Chunked(contentType, source.map(bs => HttpEntity.ChunkStreamPart(bs)))\n    val request =\n      mkRequest(HttpMethods.PUT, uri(db, id, attName), Future.successful(entity), baseHeaders ++ revHeader(rev))\n    requestJson[JsObject](request)\n  }\n\n  // Retrieves and streams an attachment into a Sink, producing a result of type T.\n  // http://docs.couchdb.org/en/1.6.1/api/document/attachments.html#get--db-docid-attname\n  def getAttachment[T](id: String,\n                       rev: String,\n                       attName: String,\n                       sink: Sink[ByteString, Future[T]]): Future[Either[StatusCode, (ContentType, T)]] = {\n    val httpRequest = mkRequest(HttpMethods.GET, uri(db, id, attName), headers = baseHeaders ++ revHeader(rev))\n\n    request(httpRequest).flatMap { response =>\n      if (response.status.isSuccess) {\n        response.entity.withoutSizeLimit().dataBytes.runWith(sink).map(r => Right(response.entity.contentType, r))\n      } else {\n        response.discardEntityBytes().future.map(_ => Left(response.status))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/CouchDbRestStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging.ErrorLevel\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.stream.scaladsl._\nimport org.apache.pekko.util.ByteString\n\nimport scala.concurrent.Await\nimport scala.concurrent.duration._\nimport spray.json._\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, MetricEmitter, TransactionId}\nimport org.apache.openwhisk.core.database.StoreUtils._\nimport org.apache.openwhisk.core.entity.Attachments.Attached\nimport org.apache.openwhisk.core.entity.{BulkEntityResult, DocInfo, DocumentReader, UUID}\nimport org.apache.openwhisk.http.Messages\nimport pureconfig.loadConfigOrThrow\n\nimport scala.concurrent.Future\nimport scala.util.Try\n\n/**\n * Basic client to put and delete artifacts in a data store.\n *\n * @param dbProtocol the protocol to access the database with (http/https)\n * @param dbHost the host to access database from\n * @param dbPort the port on the host\n * @param dbUserName the user name to access database as\n * @param dbPassword the secret for the user name required to access the database\n * @param dbName the name of the database to operate on\n * @param serializerEvidence confirms the document abstraction is serializable to a Document with an id\n */\nclass CouchDbRestStore[DocumentAbstraction <: DocumentSerializer](dbProtocol: String,\n                                                                  dbHost: String,\n                                                                  dbPort: Int,\n                                                                  dbUsername: String,\n                                                                  dbPassword: String,\n                                                                  dbName: String,\n                                                                  useBatching: Boolean = false,\n                                                                  val inliningConfig: InliningConfig,\n                                                                  val attachmentStore: Option[AttachmentStore])(\n  implicit system: ActorSystem,\n  val logging: Logging,\n  jsonFormat: RootJsonFormat[DocumentAbstraction],\n  docReader: DocumentReader)\n    extends ArtifactStore[DocumentAbstraction]\n    with DefaultJsonProtocol\n    with AttachmentSupport[DocumentAbstraction] {\n\n  protected[core] implicit val executionContext = system.dispatchers.lookup(\"dispatchers.couch-dispatcher\")\n\n  private val couchScheme = \"couch\"\n  val attachmentScheme: String = attachmentStore.map(_.scheme).getOrElse(couchScheme)\n\n  private val client: CouchDbRestClient =\n    new CouchDbRestClient(dbProtocol, dbHost, dbPort.toInt, dbUsername, dbPassword, dbName)\n\n  // This the the amount of allowed parallel requests for each entity, before batching starts. If there are already maxOpenDbRequests\n  // and more documents need to be stored, then all arriving documents will be put into batches (if enabled) to avoid a long queue.\n  private val maxOpenDbRequests = system.settings.config.getInt(\"pekko.http.host-connection-pool.max-connections\") / 2\n\n  private val maxRetry = loadConfigOrThrow[Int](\"whisk.activation-store.retry-config.max-tries\")\n  private val batcher: Batcher[JsObject, Either[ArtifactStoreException, DocInfo]] =\n    new Batcher(500, maxOpenDbRequests, maxRetry)(put(_, _)(TransactionId.dbBatcher))\n\n  override protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo] = {\n    val asJson = d.toDocumentRecord\n\n    val id: String = asJson.fields(\"_id\").convertTo[String].trim\n    val rev: Option[String] = asJson.fields.get(\"_rev\").map(_.convertTo[String])\n    require(!id.isEmpty, \"document id must be defined\")\n\n    val docinfoStr = s\"id: $id, rev: ${rev.getOrElse(\"null\")}\"\n    val start = transid.started(this, LoggingMarkers.DATABASE_SAVE, s\"[PUT] '$dbName' saving document: '${docinfoStr}'\")\n\n    val f = if (useBatching) {\n      batcher.put(asJson).map { e =>\n        e match {\n          case Right(response) =>\n            transid.finished(this, start, s\"[PUT] '$dbName' completed document: '${docinfoStr}', response: '$response'\")\n            response\n\n          case Left(e: DocumentConflictException) =>\n            transid.finished(this, start, s\"[PUT] '$dbName', document: '${docinfoStr}'; conflict.\")\n            // For compatibility.\n            throw DocumentConflictException(\"conflict on 'put'\")\n\n          case Left(e: ArtifactStoreException) =>\n            transid.finished(this, start, s\"[PUT] '$dbName', document: '${docinfoStr}'; ${e.getMessage}.\")\n            throw PutException(\"error on 'put'\")\n        }\n      }\n    } else {\n      val request: CouchDbRestClient => Future[Either[StatusCode, JsObject]] = rev match {\n        case Some(r) =>\n          client =>\n            client.putDoc(id, r, asJson)\n        case None =>\n          client =>\n            client.putDoc(id, asJson)\n      }\n      request(client).map {\n        case Right(response) =>\n          transid.finished(this, start, s\"[PUT] '$dbName' completed document: '${docinfoStr}', response: '$response'\")\n          response.convertTo[DocInfo]\n        case Left(StatusCodes.Conflict) =>\n          transid.finished(this, start, s\"[PUT] '$dbName', document: '${docinfoStr}'; conflict.\")\n          // For compatibility.\n          throw DocumentConflictException(\"conflict on 'put'\")\n        case Left(code) =>\n          transid.failed(\n            this,\n            start,\n            s\"[PUT] '$dbName' failed to put document: '${docinfoStr}'; http status: '${code}'\",\n            ErrorLevel)\n          throw new PutException(\"Unexpected http response code: \" + code)\n      }\n    }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(this, start, s\"[PUT] '$dbName' internal error, failure: '${failure.getMessage}'\", ErrorLevel))\n  }\n\n  private def put(ds: Seq[JsObject], retry: Int)(\n    implicit transid: TransactionId): Future[Seq[Either[ArtifactStoreException, DocInfo]]] = {\n    val count = ds.size\n    val start = transid.started(this, LoggingMarkers.DATABASE_BULK_SAVE, s\"'$dbName' saving $count documents\")\n\n    MetricEmitter.emitHistogramMetric(LoggingMarkers.DATABASE_BATCH_SIZE, ds.size)\n\n    val f = client.putDocs(ds).map {\n      _ match {\n        case Right(response) =>\n          transid.finished(this, start, s\"'$dbName' completed $count documents\")\n\n          response.convertTo[Seq[BulkEntityResult]].map { singleResult =>\n            singleResult.error\n              .map {\n                case \"conflict\" => Left(DocumentConflictException(\"conflict on 'bulk_put'\"))\n                case e          => Left(PutException(s\"Unexpected $e: ${singleResult.reason.getOrElse(\"\")} on 'bulk_put'\"))\n              }\n              .getOrElse {\n                Right(singleResult.toDocInfo)\n              }\n          }\n\n        case Left(code) =>\n          transid.failed(this, start, s\"'$dbName' failed to put documents, http status: '${code}'\", ErrorLevel)\n          throw new PutException(\"Unexpected http response code: \" + code)\n      }\n    }\n\n    f.recoverWith {\n      case t: ArtifactStoreException => Future.failed(t)\n      case _ if retry > 0 =>\n        transid.failed(this, start, s\"failed to store an activation to CouchDB\")\n        put(ds, retry - 1)\n      case t =>\n        transid\n          .failed(this, start, s\"[PUT] '$dbName' internal error, failure: '${t.getMessage}'\", ErrorLevel)\n        Future.failed(t)\n    }\n  }\n\n  override protected[database] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] = {\n    require(doc != null && doc.rev.asString != null, \"doc revision required for delete\")\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_DELETE, s\"[DEL] '$dbName' deleting document: '$doc'\")\n\n    val f = client.deleteDoc(doc.id.id, doc.rev.rev).map { e =>\n      e match {\n        case Right(response) =>\n          transid.finished(this, start, s\"[DEL] '$dbName' completed document: '$doc', response: $response\")\n          response.fields(\"ok\").convertTo[Boolean]\n\n        case Left(StatusCodes.NotFound) =>\n          transid.finished(this, start, s\"[DEL] '$dbName', document: '${doc}'; not found.\")\n          // for compatibility\n          throw NoDocumentException(\"not found on 'delete'\")\n\n        case Left(StatusCodes.Conflict) =>\n          transid.finished(this, start, s\"[DEL] '$dbName', document: '${doc}'; conflict.\")\n          throw DocumentConflictException(\"conflict on 'delete'\")\n\n        case Left(code) =>\n          transid.failed(\n            this,\n            start,\n            s\"[DEL] '$dbName' failed to delete document: '${doc}'; http status: '${code}'\",\n            ErrorLevel)\n          throw new DeleteException(\"Unexpected http response code: \" + code)\n      }\n    }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(\n          this,\n          start,\n          s\"[DEL] '$dbName' internal error, doc: '$doc', failure: '${failure.getMessage}'\",\n          ErrorLevel))\n  }\n\n  override protected[database] def get[A <: DocumentAbstraction](doc: DocInfo,\n                                                                 attachmentHandler: Option[(A, Attached) => A] = None)(\n    implicit transid: TransactionId,\n    ma: Manifest[A]): Future[A] = {\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_GET, s\"[GET] '$dbName' finding document: '$doc'\")\n\n    require(doc != null, \"doc undefined\")\n    val request: CouchDbRestClient => Future[Either[StatusCode, JsObject]] = if (doc.rev.rev != null) { client =>\n      client.getDoc(doc.id.id, doc.rev.rev)\n    } else { client =>\n      client.getDoc(doc.id.id)\n    }\n\n    val f = request(client).map { e =>\n      e match {\n        case Right(response) =>\n          transid.finished(this, start, s\"[GET] '$dbName' completed: found document '$doc'\")\n          val deserializedDoc = deserialize[A, DocumentAbstraction](doc, response)\n          attachmentHandler.map(processAttachments(deserializedDoc, response, _)).getOrElse(deserializedDoc)\n        case Left(StatusCodes.NotFound) =>\n          transid.finished(this, start, s\"[GET] '$dbName', document: '${doc}'; not found.\")\n          // for compatibility\n          throw NoDocumentException(\"not found on 'get'\")\n\n        case Left(code) =>\n          transid.failed(this, start, s\"[GET] '$dbName' failed to get document: '${doc}'; http status: '${code}'\")\n          throw new GetException(\"Unexpected http response code: \" + code)\n      }\n    } recoverWith {\n      case e: DeserializationException => throw DocumentUnreadable(Messages.corruptedEntity)\n    }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(\n          this,\n          start,\n          s\"[GET] '$dbName' internal error, doc: '$doc', failure: '${failure.getMessage}'\",\n          ErrorLevel))\n  }\n\n  override protected[core] def query(table: String,\n                                     startKey: List[Any],\n                                     endKey: List[Any],\n                                     skip: Int,\n                                     limit: Int,\n                                     includeDocs: Boolean,\n                                     descending: Boolean,\n                                     reduce: Boolean,\n                                     stale: StaleParameter)(implicit transid: TransactionId): Future[List[JsObject]] = {\n\n    require(!(reduce && includeDocs), \"reduce and includeDocs cannot both be true\")\n    require(skip >= 0, \"skip should be non negative\")\n    require(limit >= 0, \"limit should be non negative\")\n\n    // Apparently you have to do that in addition to setting \"descending\"\n    val (realStartKey, realEndKey) = if (descending) {\n      (endKey, startKey)\n    } else {\n      (startKey, endKey)\n    }\n\n    val Array(firstPart, secondPart) = table.split(\"/\")\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s\"[QUERY] '$dbName' searching '$table\")\n\n    val f = client\n      .executeView(firstPart, secondPart)(\n        startKey = realStartKey,\n        endKey = realEndKey,\n        skip = Some(skip),\n        limit = Some(limit),\n        stale = stale,\n        includeDocs = includeDocs,\n        descending = descending,\n        reduce = reduce)\n      .map {\n        case Right(response) =>\n          val rows = response.fields(\"rows\").convertTo[List[JsObject]]\n\n          val out = if (reduce && !rows.isEmpty) {\n            assert(rows.length == 1, s\"result of reduced view contains more than one value: '$rows'\")\n            rows.head.fields(\"value\").convertTo[List[JsObject]]\n          } else if (reduce) {\n            List(JsObject.empty)\n          } else {\n            rows\n          }\n\n          transid.finished(this, start, s\"[QUERY] '$dbName' completed: matched ${out.size}\")\n          out\n\n        case Left(code) =>\n          transid.failed(this, start, s\"Unexpected http response code: $code\", ErrorLevel)\n          throw new QueryException(\"Unexpected http response code: \" + code)\n      }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(this, start, s\"[QUERY] '$dbName' internal error, failure: '${failure.getMessage}'\", ErrorLevel))\n  }\n\n  protected[core] def count(table: String, startKey: List[Any], endKey: List[Any], skip: Int, stale: StaleParameter)(\n    implicit transid: TransactionId): Future[Long] = {\n    require(skip >= 0, \"skip should be non negative\")\n\n    val Array(firstPart, secondPart) = table.split(\"/\")\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s\"[COUNT] '$dbName' searching '$table\")\n\n    val f = client\n      .executeView(firstPart, secondPart)(startKey = startKey, endKey = endKey, stale = stale, reduce = true)\n      .map {\n        case Right(response) =>\n          val rows = response.fields(\"rows\").convertTo[List[JsObject]]\n\n          val out = if (rows.nonEmpty) {\n            assert(rows.length == 1, s\"result of reduced view contains more than one value: '$rows'\")\n            val count = rows.head.fields(\"value\").convertTo[Long]\n            if (count > skip) count - skip else 0L\n          } else 0L\n\n          transid.finished(this, start, s\"[COUNT] '$dbName' completed: count $out\")\n          out\n\n        case Left(code) =>\n          transid.failed(this, start, s\"Unexpected http response code: $code\", ErrorLevel)\n          throw new QueryException(\"Unexpected http response code: \" + code)\n      }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(this, start, s\"[COUNT] '$dbName' internal error, failure: '${failure.getMessage}'\", ErrorLevel))\n  }\n\n  override protected[database] def putAndAttach[A <: DocumentAbstraction](\n    doc: A,\n    update: (A, Attached) => A,\n    contentType: ContentType,\n    docStream: Source[ByteString, _],\n    oldAttachment: Option[Attached])(implicit transid: TransactionId): Future[(DocInfo, Attached)] = {\n\n    attachmentStore match {\n      case Some(as) =>\n        attachToExternalStore(doc, update, contentType, docStream, oldAttachment, as)\n      case None =>\n        attachToCouch(doc, update, contentType, docStream)\n    }\n  }\n\n  private def attachToCouch[A <: DocumentAbstraction](\n    doc: A,\n    update: (A, Attached) => A,\n    contentType: ContentType,\n    docStream: Source[ByteString, _])(implicit transid: TransactionId): Future[(DocInfo, Attached)] = {\n\n    if (maxInlineSize.toBytes == 0) {\n      val uri = uriFrom(scheme = attachmentScheme, path = UUID().asString)\n      for {\n        attached <- Future.successful(Attached(uri.toString, contentType))\n        i1 <- put(update(doc, attached))\n        i2 <- attach(i1, uri.path.toString, attached.attachmentType, docStream)\n      } yield (i2, attached)\n    } else {\n      for {\n        bytesOrSource <- inlineOrAttach(docStream)\n        uri = uriOf(bytesOrSource, UUID().asString)\n        attached <- {\n          val a = bytesOrSource match {\n            case Left(bytes) => Attached(uri.toString, contentType, Some(bytes.size), Some(digest(bytes)))\n            case Right(_)    => Attached(uri.toString, contentType)\n          }\n          Future.successful(a)\n        }\n        i1 <- put(update(doc, attached))\n        i2 <- bytesOrSource match {\n          case Left(_)  => Future.successful(i1)\n          case Right(s) => attach(i1, uri.path.toString, attached.attachmentType, s)\n        }\n      } yield (i2, attached)\n    }\n  }\n\n  private def attach(doc: DocInfo, name: String, contentType: ContentType, docStream: Source[ByteString, _])(\n    implicit transid: TransactionId): Future[DocInfo] = {\n\n    val start = transid.started(\n      this,\n      LoggingMarkers.DATABASE_ATT_SAVE,\n      s\"[ATT_PUT] '$dbName' uploading attachment '$name' of document '$doc'\")\n\n    require(doc != null, \"doc undefined\")\n    require(doc.rev.rev != null, \"doc revision must be specified\")\n\n    val f = client.putAttachment(doc.id.id, doc.rev.rev, name, contentType, docStream).map { e =>\n      e match {\n        case Right(response) =>\n          transid\n            .finished(this, start, s\"[ATT_PUT] '$dbName' completed uploading attachment '$name' of document '$doc'\")\n          val id = response.fields(\"id\").convertTo[String]\n          val rev = response.fields(\"rev\").convertTo[String]\n          DocInfo ! (id, rev)\n\n        case Left(StatusCodes.NotFound) =>\n          transid\n            .finished(this, start, s\"[ATT_PUT] '$dbName' uploading attachment '$name' of document '$doc'; not found\")\n          throw NoDocumentException(\"Not found on 'readAttachment'.\")\n\n        case Left(code) =>\n          transid.failed(\n            this,\n            start,\n            s\"[ATT_PUT] '$dbName' failed to upload attachment '$name' of document '$doc'; http status '$code'\")\n          throw new PutException(\"Unexpected http response code: \" + code)\n      }\n    }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(\n          this,\n          start,\n          s\"[ATT_PUT] '$dbName' internal error, name: '$name', doc: '$doc', failure: '${failure.getMessage}'\",\n          ErrorLevel))\n  }\n\n  override protected[core] def readAttachment[T](doc: DocInfo, attached: Attached, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T] = {\n    val name = attached.attachmentName\n    val attachmentUri = Uri(name)\n    attachmentUri.scheme match {\n      case AttachmentSupport.MemScheme =>\n        memorySource(attachmentUri).runWith(sink)\n      case s if s == couchScheme || attachmentUri.isRelative =>\n        //relative case is for compatibility with earlier naming approach where attachment name would be like 'jarfile'\n        //Compared to current approach of '<scheme>:<name>'\n        readAttachmentFromCouch(doc, attachmentUri, sink)\n      case s if attachmentStore.isDefined && attachmentStore.get.scheme == s =>\n        attachmentStore.get.readAttachment(doc.id, attachmentUri.path.toString, sink)\n      case _ =>\n        throw new IllegalArgumentException(s\"Unknown attachment scheme in attachment uri $attachmentUri\")\n    }\n  }\n\n  private def readAttachmentFromCouch[T](doc: DocInfo, attachmentUri: Uri, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T] = {\n\n    val name = attachmentUri.path\n    val start = transid.started(\n      this,\n      LoggingMarkers.DATABASE_ATT_GET,\n      s\"[ATT_GET] '$dbName' finding attachment '$name' of document '$doc'\")\n\n    require(doc != null, \"doc undefined\")\n    require(doc.rev.rev != null, \"doc revision must be specified\")\n\n    val g =\n      client\n        .getAttachment[T](doc.id.id, doc.rev.rev, attachmentUri.path.toString, sink)\n        .map {\n          case Right((_, result)) =>\n            transid.finished(this, start, s\"[ATT_GET] '$dbName' completed: found attachment '$name' of document '$doc'\")\n            result\n\n          case Left(StatusCodes.NotFound) =>\n            transid.finished(\n              this,\n              start,\n              s\"[ATT_GET] '$dbName', retrieving attachment '$name' of document '$doc'; not found.\")\n            throw NoDocumentException(\"Not found on 'readAttachment'.\")\n\n          case Left(code) =>\n            transid.failed(\n              this,\n              start,\n              s\"[ATT_GET] '$dbName' failed to get attachment '$name' of document '$doc'; http status: '$code'\")\n            throw new GetException(\"Unexpected http response code: \" + code)\n        }\n\n    reportFailure(\n      g,\n      failure =>\n        transid.failed(\n          this,\n          start,\n          s\"[ATT_GET] '$dbName' internal error, name: '$name', doc: '$doc', failure: '${failure.getMessage}'\",\n          ErrorLevel))\n  }\n\n  override protected[core] def deleteAttachments[T](doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] =\n    attachmentStore\n      .map(as => as.deleteAttachments(doc.id))\n      .getOrElse(Future.successful(true)) // For CouchDB it is expected that the entire document is deleted.\n\n  override def shutdown(): Unit = {\n    Await.result(client.shutdown(), 30.seconds)\n    attachmentStore.foreach(_.shutdown())\n  }\n\n  private def processAttachments[A <: DocumentAbstraction](doc: A,\n                                                           js: JsObject,\n                                                           attachmentHandler: (A, Attached) => A): A = {\n    js.fields\n      .get(\"_attachments\")\n      .map {\n        case JsObject(fields) if fields.size == 1 =>\n          val (name, value) = fields.head\n          value.asJsObject.getFields(\"content_type\", \"digest\", \"length\") match {\n            case Seq(JsString(contentTypeValue), JsString(digest), JsNumber(length)) =>\n              val contentType = ContentType.parse(contentTypeValue) match {\n                case Right(ct) => ct\n                case Left(_)   => ContentTypes.NoContentType //Should not happen\n              }\n              attachmentHandler(\n                doc,\n                Attached(getAttachmentName(name), contentType, Some(length.longValue), Some(digest)))\n            case x =>\n              throw DeserializationException(\"Attachment json does not have required fields\" + x)\n\n          }\n        case x => throw DeserializationException(\"Multiple attachments found\" + x)\n      }\n      .getOrElse(doc)\n  }\n\n  /**\n   * Determines if the attachment scheme confirms to new UUID based scheme or not\n   * and generates the name based on that\n   */\n  private def getAttachmentName(name: String): String = {\n    Try(java.util.UUID.fromString(name))\n      .map(_ => uriFrom(scheme = attachmentScheme, path = name).toString)\n      .getOrElse(name)\n  }\n\n  private def reportFailure[T, U](f: Future[T], onFailure: Throwable => U): Future[T] = {\n    f.failed.foreach {\n      case _: ArtifactStoreException => // These failures are intentional and shouldn't trigger the catcher.\n      case x                         => onFailure(x)\n    }\n    f\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/CouchDbStoreProvider.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.apache.pekko.actor.ActorSystem\nimport spray.json.RootJsonFormat\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.DocumentReader\nimport org.apache.openwhisk.core.entity.size._\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.reflect.ClassTag\n\ncase class CouchDbConfig(provider: String,\n                         protocol: String,\n                         host: String,\n                         port: Int,\n                         username: String,\n                         password: String,\n                         databases: Map[String, String]) {\n  assume(Set(protocol, host, username, password).forall(_.nonEmpty), \"At least one expected property is missing\")\n\n  def databaseFor[D](implicit tag: ClassTag[D]): String = {\n    val entityType = tag.runtimeClass.getSimpleName\n    databases.get(entityType) match {\n      case Some(name) => name\n      case None       => throw new IllegalArgumentException(s\"Database name mapping not found for $entityType\")\n    }\n  }\n}\n\nobject CouchDbStoreProvider extends ArtifactStoreProvider {\n\n  def makeStore[D <: DocumentSerializer: ClassTag](useBatching: Boolean)(implicit jsonFormat: RootJsonFormat[D],\n                                                                         docReader: DocumentReader,\n                                                                         actorSystem: ActorSystem,\n                                                                         logging: Logging): ArtifactStore[D] =\n    makeArtifactStore(useBatching, getAttachmentStore())\n\n  def makeArtifactStore[D <: DocumentSerializer: ClassTag](useBatching: Boolean,\n                                                           attachmentStore: Option[AttachmentStore])(\n    implicit jsonFormat: RootJsonFormat[D],\n    docReader: DocumentReader,\n    actorSystem: ActorSystem,\n    logging: Logging): ArtifactStore[D] = {\n    val dbConfig = loadConfigOrThrow[CouchDbConfig](ConfigKeys.couchdb)\n    require(\n      dbConfig.provider == \"Cloudant\" || dbConfig.provider == \"CouchDB\",\n      s\"Unsupported db.provider: ${dbConfig.provider}\")\n\n    val inliningConfig = loadConfigOrThrow[InliningConfig](ConfigKeys.db)\n\n    new CouchDbRestStore[D](\n      dbConfig.protocol,\n      dbConfig.host,\n      dbConfig.port,\n      dbConfig.username,\n      dbConfig.password,\n      dbConfig.databaseFor[D],\n      useBatching,\n      inliningConfig,\n      attachmentStore)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/DocumentFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.io.InputStream\nimport java.io.OutputStream\n\nimport scala.concurrent.{Future, Promise}\nimport org.apache.pekko.http.scaladsl.model.ContentType\nimport org.apache.pekko.stream.IOResult\nimport org.apache.pekko.stream.scaladsl.StreamConverters\nimport spray.json.JsObject\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.Attachments.Attached\nimport org.apache.openwhisk.core.entity.CacheKey\nimport org.apache.openwhisk.core.entity.DocId\nimport org.apache.openwhisk.core.entity.DocInfo\nimport org.apache.openwhisk.core.entity.DocRevision\n\n/**\n * An interface for modifying the revision number on a document. Hides the details of\n * the revision to some extent while providing a marker interface for operations that\n * need to update the revision on a document.\n */\nprotected[core] trait DocumentRevisionProvider {\n\n  /** Gets the document id and revision as an instance of DocInfo. */\n  protected[database] def docinfo: DocInfo\n\n  /**\n   * Sets the revision number when a document is deserialized from datastore. The\n   * _rev is an opaque value, needed to update the record in the datastore. It is\n   * not part of the core properties of this class. It is not required when saving\n   * a new instance of this type to the datastore.\n   */\n  protected[core] final def revision[W](r: DocRevision): W = {\n    _rev = r\n    this.asInstanceOf[W]\n  }\n\n  protected[core] def rev = _rev\n\n  private var _rev: DocRevision = DocRevision.empty\n}\n\n/**\n * A common trait for all records that are serialized into raw documents for\n * the datastore, where the document id is a generated unique identifier.\n */\ntrait DocumentSerializer {\n\n  /**\n   * A JSON view including the document metadata, for writing to the datastore.\n   *\n   * @return JsObject\n   */\n  def toDocumentRecord: JsObject\n}\n\n/**\n * A common trait for all records that are deserialized from raw documents in the datastore\n *\n * The type parameter W represents the \"whisk\" type, the document abstraction to\n * use in core components. The trait is invariant in W\n * but the get permits a datastore of its super type so that a single datastore client\n * may be used for multiple types (because the types are stored in the same database for example).\n */\ntrait DocumentFactory[W <: DocumentRevisionProvider] extends MultipleReadersSingleWriterCache[W, DocInfo] {\n\n  /**\n   * Puts a record of type W in the datastore.\n   *\n   * The type parameters for the database are bounded from below to allow gets from a database that\n   * contains several different but related types (for example entities are stored in the same database\n   * and share common super types EntityRecord and WhiskEntity.\n   *\n   * @param db the datastore client to fetch entity from\n   * @param doc the entity to store\n   * @param transid the transaction id for logging\n   * @param notifier an optional callback when cache changes\n   * @param old an optional old document in case of update\n   * @return Future[DocInfo] with completion to DocInfo containing the save document id and revision\n   */\n  def put[Wsuper >: W](db: ArtifactStore[Wsuper], doc: W, old: Option[W])(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo] = {\n    implicit val logger = db.logging\n    implicit val ec = db.executionContext\n    cacheUpdate(doc, CacheKey(doc), db.put(doc) map { newDocInfo =>\n      doc.revision[W](newDocInfo.rev)\n      doc.docinfo\n    })\n  }\n\n  def putAndAttach[Wsuper >: W](db: ArtifactStore[Wsuper],\n                                doc: W,\n                                update: (W, Attached) => W,\n                                contentType: ContentType,\n                                bytes: InputStream,\n                                oldAttachment: Option[Attached],\n                                postProcess: Option[W => W] = None)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo] = {\n    implicit val logger = db.logging\n    implicit val ec = db.executionContext\n\n    val key = CacheKey(doc)\n    val src = StreamConverters.fromInputStream(() => bytes)\n\n    val p = Promise[W]\n    cacheUpdate(p.future, key, db.putAndAttach[W](doc, update, contentType, src, oldAttachment) map {\n      case (newDocInfo, attached) =>\n        val newDoc = update(doc, attached)\n        val cacheDoc = postProcess map { _(newDoc) } getOrElse newDoc\n        cacheDoc.revision[W](newDocInfo.rev)\n        p.success(cacheDoc)\n        newDocInfo\n    })\n  }\n\n  def del[Wsuper >: W](db: ArtifactStore[Wsuper], doc: DocInfo)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[Boolean] = {\n    implicit val logger = db.logging\n    implicit val ec = db.executionContext\n\n    val key = CacheKey(doc.id.asDocInfo)\n    cacheInvalidate(key, db.del(doc))\n  }\n\n  /**\n   * Fetches a raw record of type R from the datastore by its id (and revision if given)\n   * and converts it to Success(W) or Failure(Throwable) if there is an error fetching\n   * the record or deserializing it.\n   *\n   * The type parameters for the database are bounded from below to allow gets from a database that\n   * contains several different but related types (for example entities are stored in the same database\n   * and share common super types EntityRecord and WhiskEntity.\n   *\n   * @param db the datastore client to fetch entity from\n   * @param doc the entity document information (must contain a valid id)\n   * @param rev the document revision (optional)\n   * @param fromCache will only query cache if true (defaults to collection settings)\n   * @param transid the transaction id for logging\n   * @param mw a manifest for W (hint to compiler to preserve type R for runtime)\n   * @return Future[W] with completion to Success(W), or Failure(Throwable) if the raw record cannot be converted into W\n   */\n  def get[Wsuper >: W](\n    db: ArtifactStore[Wsuper],\n    doc: DocId,\n    rev: DocRevision = DocRevision.empty,\n    fromCache: Boolean = cacheEnabled,\n    ignoreMissingAttachment: Boolean = false)(implicit transid: TransactionId, mw: Manifest[W]): Future[W] = {\n    implicit val logger = db.logging\n    implicit val ec = db.executionContext\n    val key = doc.asDocInfo(rev)\n    cacheLookup(CacheKey(key), db.get[W](key, None), fromCache)\n  }\n\n  /**\n   *  Fetches document along with attachment. `postProcess` would be used to process the fetched document\n   *  before adding it to cache. This ensures that for documents having attachment the cache is updated only\n   *  post fetch of the attachment\n   */\n  protected def getWithAttachment[Wsuper >: W](\n    db: ArtifactStore[Wsuper],\n    doc: DocId,\n    rev: DocRevision = DocRevision.empty,\n    fromCache: Boolean,\n    attachmentHandler: (W, Attached) => W,\n    postProcess: W => Future[W])(implicit transid: TransactionId, mw: Manifest[W]): Future[W] = {\n    implicit val logger = db.logging\n    implicit val ec = db.executionContext\n    val key = doc.asDocInfo(rev)\n    cacheLookup(CacheKey(key), db.get[W](key, Some(attachmentHandler)).flatMap(postProcess), fromCache)\n  }\n\n  protected def getAttachment[Wsuper >: W](\n    db: ArtifactStore[Wsuper],\n    doc: W,\n    attached: Attached,\n    outputStream: OutputStream,\n    postProcess: Option[W => W] = None)(implicit transid: TransactionId, mw: Manifest[W]): Future[W] = {\n    implicit val ec = db.executionContext\n    implicit val notifier: Option[CacheChangeNotification] = None\n    implicit val logger = db.logging\n\n    val docInfo = doc.docinfo\n    val key = CacheKey(docInfo)\n    val sink = StreamConverters.fromOutputStream(() => outputStream)\n\n    db.readAttachment[IOResult](docInfo, attached, sink).map { _ =>\n      val cacheDoc = postProcess.map(_(doc)).getOrElse(doc)\n\n      cacheUpdate(cacheDoc, key, Future.successful(docInfo)) map { newDocInfo =>\n        cacheDoc.revision[W](newDocInfo.rev)\n      }\n      cacheDoc\n    }\n  }\n\n  def deleteAttachments[Wsuper >: W](db: ArtifactStore[Wsuper], doc: DocInfo)(\n    implicit transid: TransactionId): Future[Boolean] = {\n    implicit val ec = db.executionContext\n    db.deleteAttachments(doc)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/DocumentHandler.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.{DocId, UserLimits}\nimport org.apache.openwhisk.core.entity.EntityPath.PATHSEP\nimport org.apache.openwhisk.utils.JsHelpers\n\nimport scala.collection.immutable.Seq\nimport scala.concurrent.Future\nimport scala.concurrent.ExecutionContext\n\n/**\n * Simple abstraction allow accessing a document just by _id. This would be used\n * to perform queries related to join support\n */\ntrait DocumentProvider {\n  protected[database] def get(id: DocId)(implicit transid: TransactionId): Future[Option[JsObject]]\n}\n\ntrait DocumentHandler {\n\n  /**\n   * Returns a JsObject having computed fields. This is a substitution for fields\n   * computed in CouchDB views\n   */\n  def computedFields(js: JsObject): JsObject = JsObject.empty\n\n  /**\n   * Returns the set of field names (including sub document field) which needs to be fetched as part of\n   * query made for the given view.\n   */\n  def fieldsRequiredForView(ddoc: String, view: String): Set[String] = Set.empty\n\n  /**\n   * Transforms the query result instance from artifact store as per view requirements. Some view computation\n   * may result in performing a join operation.\n   *\n   * If the passed instance does not confirm to view conditions that transformed result would be None. This\n   * would be the case if view condition cannot be completed handled in query made to artifact store\n   */\n  def transformViewResult(\n    ddoc: String,\n    view: String,\n    startKey: List[Any],\n    endKey: List[Any],\n    includeDocs: Boolean,\n    js: JsObject,\n    provider: DocumentProvider)(implicit transid: TransactionId, ec: ExecutionContext): Future[Seq[JsObject]]\n\n  /**\n   * Determines if the complete document should be fetched even if `includeDocs` is set to true. For some view computation\n   * complete document (including sub documents) may be needed and for them its required that complete document must be\n   * fetched as part of query response\n   */\n  def shouldAlwaysIncludeDocs(ddoc: String, view: String): Boolean = false\n\n  def checkIfTableSupported(table: String): Unit = {\n    if (!supportedTables.contains(table)) {\n      throw UnsupportedView(table)\n    }\n  }\n\n  protected def supportedTables: Set[String]\n}\n\n/**\n * Base class for handlers which do not perform joins for computing views\n */\nabstract class SimpleHandler extends DocumentHandler {\n  override def transformViewResult(\n    ddoc: String,\n    view: String,\n    startKey: List[Any],\n    endKey: List[Any],\n    includeDocs: Boolean,\n    js: JsObject,\n    provider: DocumentProvider)(implicit transid: TransactionId, ec: ExecutionContext): Future[Seq[JsObject]] = {\n    //Query result from CouchDB have below object structure with actual result in `value` key\n    //So transform the result to confirm to that structure\n    val viewResult = JsObject(\n      \"id\" -> js.fields(\"_id\"),\n      \"key\" -> createKey(ddoc, view, startKey, js),\n      \"value\" -> computeView(ddoc, view, js))\n\n    val result = if (includeDocs) JsObject(viewResult.fields + (\"doc\" -> js)) else viewResult\n    Future.successful(Seq(result))\n  }\n\n  /**\n   * Computes the view as per viewName. Its passed either the projected object or actual\n   * object\n   */\n  def computeView(ddoc: String, view: String, js: JsObject): JsObject\n\n  /**\n   * Key is an array which matches the view query key\n   */\n  protected def createKey(ddoc: String, view: String, startKey: List[Any], js: JsObject): JsArray\n}\n\nobject ActivationHandler extends SimpleHandler {\n  val NS_PATH = \"nspath\"\n  private val commonFields =\n    Set(\"namespace\", \"name\", \"version\", \"publish\", \"annotations\", \"activationId\", \"start\", \"cause\")\n  private val fieldsForView = commonFields ++ Seq(\"end\", \"response.statusCode\")\n\n  protected val supportedTables =\n    Set(\"activations/byDate\", \"whisks-filters.v2.1.1/activations\", \"whisks.v2.1.0/activations\")\n\n  override def computedFields(js: JsObject): JsObject = {\n    val path = js.fields.get(\"namespace\") match {\n      case Some(JsString(namespace)) => JsString(namespace + PATHSEP + pathFilter(js))\n      case _                         => JsNull\n    }\n    val deleteLogs = annotationValue(js, \"kind\", { v =>\n      v.convertTo[String] != \"sequence\"\n    }, true)\n    dropNull((NS_PATH, path), (\"deleteLogs\", JsBoolean(deleteLogs)))\n  }\n\n  override def fieldsRequiredForView(ddoc: String, view: String): Set[String] = view match {\n    case \"activations\" => fieldsForView\n    case _             => throw UnsupportedView(s\"$ddoc/$view\")\n  }\n\n  def computeView(ddoc: String, view: String, js: JsObject): JsObject = view match {\n    case \"activations\" => computeActivationView(js)\n    case _             => throw UnsupportedView(s\"$ddoc/$view\")\n  }\n\n  def createKey(ddoc: String, view: String, startKey: List[Any], js: JsObject): JsArray = {\n    startKey match {\n      case (ns: String) :: Nil      => JsArray(Vector(JsString(ns)))\n      case (ns: String) :: _ :: Nil => JsArray(Vector(JsString(ns), js.fields(\"start\")))\n      case _                        => throw UnsupportedQueryKeys(\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n  }\n\n  private def computeActivationView(js: JsObject): JsObject = {\n    val common = js.fields.filterKeys(commonFields).toMap\n\n    val (endTime, duration) = js.getFields(\"end\", \"start\") match {\n      case Seq(JsNumber(end), JsNumber(start)) if end != 0 => (JsNumber(end), JsNumber(end - start))\n      case _                                               => (JsNull, JsNull)\n    }\n\n    val statusCode = JsHelpers.getFieldPath(js, \"response\", \"statusCode\").getOrElse(JsNull)\n\n    val result = common + (\"end\" -> endTime) + (\"duration\" -> duration) + (\"statusCode\" -> statusCode)\n    JsObject(result.filter(_._2 != JsNull))\n  }\n\n  protected[database] def pathFilter(js: JsObject): String = {\n    val name = js.fields(\"name\").convertTo[String]\n    annotationValue(js, \"path\", { v =>\n      val p = v.convertTo[String].split(PATHSEP)\n      if (p.length == 3) p(1) + PATHSEP + name else name\n    }, name)\n  }\n\n  /**\n   * Finds and transforms annotation with matching key.\n   *\n   * @param js js object having annotations array\n   * @param key annotation key\n   * @param vtr transformer function to map annotation value\n   * @param default default value to use if no matching annotation found\n   * @return annotation value matching given key\n   */\n  protected[database] def annotationValue[T](js: JsObject, key: String, vtr: JsValue => T, default: T): T = {\n    js.fields.get(\"annotations\") match {\n      case Some(JsArray(e)) =>\n        e.view\n          .map(_.asJsObject.getFields(\"key\", \"value\"))\n          .collectFirst {\n            case Seq(JsString(`key`), v: JsValue) => vtr(v) //match annotation with given key\n          }\n          .getOrElse(default)\n      case _ => default\n    }\n  }\n\n  private def dropNull(fields: JsField*) = JsObject(fields.filter(_._2 != JsNull): _*)\n}\n\nobject WhisksHandler extends SimpleHandler {\n  val ROOT_NS = \"rootns\"\n  private val commonFields = Set(\"namespace\", \"name\", \"version\", \"publish\", \"annotations\", \"updated\")\n  private val actionFields = commonFields ++ Set(\"limits\", \"exec.binary\")\n  private val packageFields = commonFields ++ Set(\"binding\")\n  private val packagePublicFields = commonFields\n  private val ruleFields = commonFields\n  private val triggerFields = commonFields\n\n  protected val supportedTables = Set(\n    \"whisks.v2.1.0/actions\",\n    \"whisks.v2.1.0/packages\",\n    \"whisks.v2.1.0/packages-public\",\n    \"whisks.v2.1.0/rules\",\n    \"whisks.v2.1.0/triggers\")\n\n  override def computedFields(js: JsObject): JsObject = {\n    js.fields.get(\"namespace\") match {\n      case Some(JsString(namespace)) =>\n        val ns = namespace.split(PATHSEP)\n        val rootNS = if (ns.length > 1) ns(0) else namespace\n        JsObject((ROOT_NS, JsString(rootNS)))\n      case _ => JsObject.empty\n    }\n  }\n\n  override def fieldsRequiredForView(ddoc: String, view: String): Set[String] = view match {\n    case \"actions\"         => actionFields\n    case \"packages\"        => packageFields\n    case \"packages-public\" => packagePublicFields\n    case \"rules\"           => ruleFields\n    case \"triggers\"        => triggerFields\n    case _                 => throw UnsupportedView(s\"$ddoc/$view\")\n  }\n\n  def computeView(ddoc: String, view: String, js: JsObject): JsObject = view match {\n    case \"actions\"         => computeActionView(js)\n    case \"packages\"        => computePackageView(js)\n    case \"packages-public\" => computePublicPackageView(js)\n    case \"rules\"           => computeRulesView(js)\n    case \"triggers\"        => computeTriggersView(js)\n    case _                 => throw UnsupportedView(s\"$ddoc/$view\")\n  }\n\n  def createKey(ddoc: String, view: String, startKey: List[Any], js: JsObject): JsArray = {\n    startKey match {\n      case (ns: String) :: Nil      => JsArray(Vector(JsString(ns)))\n      case (ns: String) :: _ :: Nil => JsArray(Vector(JsString(ns), js.fields(\"updated\")))\n      case _                        => throw UnsupportedQueryKeys(\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n  }\n\n  def getEntityTypeForDesignDoc(ddoc: String, view: String): String = view match {\n    case \"actions\"                      => \"action\"\n    case \"rules\"                        => \"rule\"\n    case \"triggers\"                     => \"trigger\"\n    case \"packages\" | \"packages-public\" => \"package\"\n    case _                              => throw UnsupportedView(s\"$ddoc/$view\")\n  }\n\n  private def computeTriggersView(js: JsObject): JsObject = {\n    JsObject(js.fields.filterKeys(commonFields).toMap)\n  }\n\n  private def computePublicPackageView(js: JsObject): JsObject = {\n    JsObject(js.fields.filterKeys(commonFields).toMap + (\"binding\" -> JsFalse))\n  }\n\n  private def computeRulesView(js: JsObject) = {\n    JsObject(js.fields.filterKeys(ruleFields).toMap)\n  }\n\n  private def computePackageView(js: JsObject): JsObject = {\n    val common = js.fields.filterKeys(commonFields).toMap\n    val binding = js.fields.get(\"binding\") match {\n      case Some(x: JsObject) if x.fields.nonEmpty => x\n      case _                                      => JsFalse\n    }\n    JsObject(common + (\"binding\" -> binding))\n  }\n\n  private def computeActionView(js: JsObject): JsObject = {\n    val base = js.fields.filterKeys(commonFields ++ Set(\"limits\")).toMap\n    val exec_binary = JsHelpers.getFieldPath(js, \"exec\", \"binary\")\n    JsObject(base + (\"exec\" -> JsObject(\"binary\" -> exec_binary.getOrElse(JsFalse))))\n  }\n}\n\nobject SubjectHandler extends DocumentHandler {\n\n  protected val supportedTables =\n    Set(\"subjects/identities\", \"subjects.v2.0.0/identities\", \"namespaceThrottlings/blockedNamespaces\")\n\n  override def shouldAlwaysIncludeDocs(ddoc: String, view: String): Boolean = {\n    (ddoc, view) match {\n      case (s, \"identities\") if s.startsWith(\"subjects\") => true\n      case (\"namespaceThrottlings\", \"blockedNamespaces\") => true\n      case _                                             => throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  override def transformViewResult(\n    ddoc: String,\n    view: String,\n    startKey: List[Any],\n    endKey: List[Any],\n    includeDocs: Boolean,\n    js: JsObject,\n    provider: DocumentProvider)(implicit transid: TransactionId, ec: ExecutionContext): Future[Seq[JsObject]] = {\n\n    val result = (ddoc, view) match {\n      case (s, \"identities\") if s.startsWith(\"subjects\") =>\n        require(includeDocs) //For subject/identities includeDocs is always true\n        computeSubjectView(startKey, js, provider)\n      case (\"namespaceThrottlings\", \"blockedNamespaces\") =>\n        Future.successful(computeBlacklistedNamespaces(js))\n      case _ =>\n        throw UnsupportedView(s\"$ddoc/$view\")\n    }\n    result\n  }\n\n  /**\n   * {{{\n   *   function (doc) {\n   *   if (doc._id.indexOf(\"/limits\") >= 0) {\n   *     if (doc.concurrentInvocations === 0 || doc.invocationsPerMinute === 0) {\n   *       var namespace = doc._id.replace(\"/limits\", \"\");\n   *       emit(namespace, 1);\n   *     }\n   *   } else if (doc.subject && doc.namespaces && doc.blocked) {\n   *     doc.namespaces.forEach(function(namespace) {\n   *       emit(namespace.name, 1);\n   *     });\n   *   }\n   * }\n   * }}}\n   */\n  private def computeBlacklistedNamespaces(js: JsObject): Seq[JsObject] = {\n    val id = js.fields(\"_id\")\n    val value = JsNumber(1)\n    id match {\n      case JsString(idv) if idv.endsWith(\"/limits\") =>\n        val limits = UserLimits.serdes.read(js)\n        if (limits.concurrentInvocations.contains(0) || limits.invocationsPerMinute.contains(0)) {\n          val ns = idv.substring(0, idv.indexOf(\"/limits\"))\n          Seq(JsObject(\"id\" -> id, \"key\" -> JsString(ns), \"value\" -> value))\n        } else Seq.empty\n      case _ =>\n        js.getFields(\"subject\", \"namespaces\", \"blocked\") match {\n          case Seq(_, namespaces: JsArray, JsTrue) =>\n            namespaces.elements.map { ns =>\n              val name = ns.asJsObject.fields(\"name\")\n              JsObject(\"id\" -> id, \"key\" -> name, \"value\" -> value)\n            }\n          case _ =>\n            Seq.empty\n        }\n    }\n  }\n\n  private def computeSubjectView(startKey: List[Any], js: JsObject, provider: DocumentProvider)(\n    implicit transid: TransactionId,\n    ec: ExecutionContext) = {\n    val subjectOpt = findMatchingSubject(startKey, js)\n    val result = subjectOpt match {\n      case Some(subject) =>\n        val limitDocId = s\"${subject.namespace}/limits\"\n        val viewJS = JsObject(\n          \"_id\" -> JsString(limitDocId),\n          \"namespace\" -> JsString(subject.namespace),\n          \"uuid\" -> JsString(subject.uuid),\n          \"key\" -> JsString(subject.key))\n        val result =\n          JsObject(\"id\" -> js.fields(\"_id\"), \"key\" -> createKey(startKey), \"value\" -> viewJS, \"doc\" -> JsNull)\n        if (subject.matchInNamespace) {\n          provider\n            .get(DocId(limitDocId))\n            .map(limits => Seq(JsObject(result.fields + (\"doc\" -> limits.getOrElse(JsNull)))))\n        } else {\n          Future.successful(Seq(result))\n        }\n      case None =>\n        Future.successful(Seq.empty)\n    }\n    result\n  }\n\n  def findMatchingSubject(startKey: List[Any], js: JsObject): Option[SubjectView] = {\n    startKey match {\n      case (ns: String) :: Nil => findMatchingSubject(js, s => s.namespace == ns && !s.blocked)\n      case (uuid: String) :: (key: String) :: Nil =>\n        findMatchingSubject(js, s => s.uuid == uuid && s.key == key && !s.blocked)\n      case _ => None\n    }\n  }\n\n  private def createKey(startKey: List[Any]): JsArray = {\n    startKey match {\n      case (ns: String) :: Nil                    => JsArray(Vector(JsString(ns))) //namespace or subject\n      case (uuid: String) :: (key: String) :: Nil => JsArray(Vector(JsString(uuid), JsString(key))) // uuid, key\n      case _                                      => throw UnsupportedQueryKeys(\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n  }\n\n  /**\n   * Computes the view as per logic below from (identities/subject) view\n   *\n   * {{{\n   *   function (doc) {\n   *      if(doc.uuid && doc.key && !doc.blocked) {\n   *        var v = {namespace: doc.subject, uuid: doc.uuid, key: doc.key};\n   *        emit([doc.subject], v);\n   *        emit([doc.uuid, doc.key], v);\n   *      }\n   *      if(doc.namespaces && !doc.blocked) {\n   *        doc.namespaces.forEach(function(namespace) {\n   *          var v = {_id: namespace.name + '/limits', namespace: namespace.name, uuid: namespace.uuid, key: namespace.key};\n   *          emit([namespace.name], v);\n   *          emit([namespace.uuid, namespace.key], v);\n   *        });\n   *      }\n   *    }\n   * }}}\n   *\n   * @param js subject json from db\n   * @param matches match predicate\n   */\n  private def findMatchingSubject(js: JsObject, matches: SubjectView => Boolean): Option[SubjectView] = {\n    val blocked = js.fields.get(\"blocked\") match {\n      case Some(JsTrue) => true\n      case _            => false\n    }\n\n    val r = js.getFields(\"subject\", \"uuid\", \"key\") match {\n      case Seq(JsString(ns), JsString(uuid), JsString(key)) => Some(SubjectView(ns, uuid, key, blocked)).filter(matches)\n      case _                                                => None\n    }\n\n    r.orElse {\n      val namespaces = js.fields.get(\"namespaces\") match {\n        case Some(JsArray(e)) =>\n          e.map(_.asJsObject.getFields(\"name\", \"uuid\", \"key\") match {\n            case Seq(JsString(ns), JsString(uuid), JsString(key)) =>\n              Some(SubjectView(ns, uuid, key, blocked, matchInNamespace = true))\n            case _ => None\n          })\n\n        case _ => Seq.empty\n      }\n      namespaces.flatMap(_.filter(matches)).headOption\n    }\n  }\n\n  case class SubjectView(namespace: String,\n                         uuid: String,\n                         key: String,\n                         blocked: Boolean = false,\n                         matchInNamespace: Boolean = false)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/MultipleReadersSingleWriterCache.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.util.concurrent.atomic.AtomicReference\nimport java.util.concurrent.{ConcurrentMap, TimeUnit}\n\nimport com.github.benmanes.caffeine.cache.Caffeine\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, TransactionId}\nimport org.apache.openwhisk.core.entity.CacheKey\n\nimport scala.concurrent.{ExecutionContext, Future, Promise}\nimport scala.util.{Failure, Success}\n\n/**\n * A cache that allows multiple readers, but only a single writer, at\n * a time. It will make a best effort attempt to coalesce reads, but\n * does not guarantee that all overlapping reads will be coalesced.\n *\n * The cache operates by bracketing all reads and writes. A read\n * imposes a lightweight read lock, by inserting an entry into the\n * cache with State.ReadInProgress. A write does the same, with\n * State.WriteInProgress.\n *\n * On read or write completion, the value transitions to State.Cached.\n *\n * State.Initial is represented implicitly by absence from the cache.\n *\n * The handshake for cache entry state transition is:\n *\n * 1. if entry is in an agreeable state, proceed\n *      where agreeable for reads is Initial, ReadInProgress, or Cached\n *                    and the read proceeds by ensuring the entry is State.ReadInProgress\n *      and agreeable for writes is Initial or Cached\n *                    and the write proceeds by ensuring the entry is State.WriteInProgress\n *      and agreeable for deletes is Cached\n *\n * 2. if entry is not in an agreeable state, then read- or write-around the cache;\n *    for deletions, we allow the delete to proceed, and mark the entry as InvalidateWhenDone\n *    the owning reader or writer is then responsible for ensuring that the entry is invalid\n *    when that read or write completes\n *\n * 3. to swap in the new state to an existing entry, we use an AtomicReference.compareAndSet\n *\n * 4. only if the db operation completes with success, atomically set the state to Cached.\n *\n * 5. lastly, for cache invalidations that race with, we mark the entry as\n *\n */\nprivate object MultipleReadersSingleWriterCache {\n\n  /** Each entry has a state, as explained in the class comment above. */\n  object State extends Enumeration {\n    type State = Value\n    val ReadInProgress, WriteInProgress, InvalidateInProgress, InvalidateWhenDone, Cached = Value\n  }\n\n  import State._\n\n  /** Failure modes, which will only occur if there is a bug in this implementation */\n  case class ConcurrentOperationUnderRead(actualState: State)\n      extends Exception(s\"Cache read started, but completion raced with a concurrent operation: $actualState.\")\n  case class ConcurrentOperationUnderUpdate(actualState: State)\n      extends Exception(s\"Cache update started, but completion raced with a concurrent operation: $actualState.\")\n  case class StaleRead(actualState: State) extends Exception(s\"Attempted read of invalid entry due to $actualState.\")\n}\n\ntrait CacheChangeNotification extends (CacheKey => Future[Unit])\n\nsealed trait EvictionPolicy\n\ncase object AccessTime extends EvictionPolicy\ncase object WriteTime extends EvictionPolicy\n\ntrait MultipleReadersSingleWriterCache[W, Winfo] {\n  import MultipleReadersSingleWriterCache.State._\n  import MultipleReadersSingleWriterCache._\n\n  /** Subclasses: Toggle this to enable/disable caching for your entity type. */\n  protected val cacheEnabled = true\n  protected val evictionPolicy: EvictionPolicy = AccessTime\n  protected val cacheExpirationTime: Long = 5\n  protected val cacheExpirationTimeUnit: TimeUnit = TimeUnit.MINUTES\n  protected val fixedCacheSize = 0\n\n  private object Entry {\n    def apply(transid: TransactionId, state: State, value: Option[Future[W]]): Entry = {\n      new Entry(transid, new AtomicReference(state), value)\n    }\n  }\n\n  /**\n   * The entries in the cache will be a triple of (transid, State, Future[W]?).\n   *\n   * We need the transid in order to detect whether we have won the race to add an entry to the cache.\n   */\n  private class Entry(@volatile private var transid: TransactionId,\n                      val state: AtomicReference[State],\n                      @volatile private var value: Option[Future[W]]) {\n\n    def invalidate(): Unit = {\n      state.set(InvalidateInProgress)\n    }\n\n    def unpack(): Future[W] = {\n      value getOrElse Future.failed(StaleRead(state.get))\n    }\n\n    def writeDone()(implicit logger: Logging): Boolean = {\n      logger.debug(this, \"write finished\")(transid)\n      trySet(WriteInProgress, Cached)\n    }\n\n    def readDone()(implicit logger: Logging): Boolean = {\n      logger.debug(this, \"read finished\")(transid)\n      trySet(ReadInProgress, Cached)\n    }\n\n    def trySet(expectedState: State, desiredState: State): Boolean = {\n      state.compareAndSet(expectedState, desiredState)\n    }\n\n    def grabWriteLock(newTransid: TransactionId, expectedState: State, newValue: Future[W]): Boolean = synchronized {\n      val swapped = trySet(expectedState, WriteInProgress)\n      if (swapped) {\n        value = Option(newValue)\n        transid = newTransid\n      }\n      swapped\n    }\n\n    def grabInvalidationLock() = state.set(InvalidateInProgress)\n\n    override def toString = s\"tid ${transid.meta.id}, state ${state.get}\"\n  }\n\n  /**\n   * This method posts a delete to the backing store, and either directly invalidates the cache entry\n   * or informs any outstanding transaction that it must invalidate the cache on completion.\n   */\n  protected def cacheInvalidate[R](key: CacheKey, invalidator: => Future[R])(\n    implicit ec: ExecutionContext,\n    transid: TransactionId,\n    logger: Logging,\n    notifier: Option[CacheChangeNotification]): Future[R] = {\n    if (cacheEnabled) {\n      logger.info(this, s\"invalidating $key on delete\")\n\n      // try inserting our desired entry...\n      val desiredEntry = Entry(transid, InvalidateInProgress, None)\n      cache(key)(desiredEntry) flatMap { actualEntry =>\n        // ... and see what we get back\n        val currentState = actualEntry.state.get\n\n        currentState match {\n          case Cached =>\n            // nobody owns the entry, forcefully grab ownership\n            // note: if a new cache lookup is received while\n            // the invalidator has not yet completed (and hence the actual entry\n            // removed from the cache), such lookup operations will still be able\n            // to return the value that is cached, and this is acceptable (under\n            // the eventual consistency model) as long as such lookups do not\n            // mutate the state of the cache to violate the invalidation that is\n            // about to occur (this is eventually consistent and NOT sequentially\n            // consistent since the cache lookup and the setting of the\n            // InvalidateInProgress bit are not atomic\n            invalidateEntryAfter(invalidator, key, actualEntry)\n\n          case ReadInProgress | WriteInProgress =>\n            if (actualEntry.trySet(currentState, InvalidateWhenDone)) {\n              // then the pre-existing owner will take care of the invalidation\n              invalidator\n            } else {\n              // the pre-existing reader or writer finished and so must\n              // explicitly invalidate here\n              invalidateEntryAfter(invalidator, key, actualEntry)\n            }\n\n          case InvalidateInProgress =>\n            if (actualEntry == desiredEntry) {\n              // we own the entry, so we are responsible for cleaning it up\n              invalidateEntryAfter(invalidator, key, actualEntry)\n            } else {\n              // someone else requested an invalidation already\n              invalidator\n            }\n\n          case InvalidateWhenDone =>\n            // a pre-existing owner will take care of the invalidation\n            invalidator\n        }\n      } andThen {\n        case _ => notifier.foreach(_(key))\n      }\n    } else invalidator // not caching\n  }\n\n  /**\n   * This method may initiate a read from the backing store, and potentially stores the result in the cache.\n   */\n  protected def cacheLookup[Wsuper >: W](key: CacheKey, generator: => Future[W], fromCache: Boolean = cacheEnabled)(\n    implicit ec: ExecutionContext,\n    transid: TransactionId,\n    logger: Logging): Future[W] = {\n    if (fromCache) {\n      val promise = Promise[W] // this promise completes with the generator value\n\n      // try inserting our desired entry...\n      val desiredEntry = Entry(transid, ReadInProgress, Some(promise.future))\n      cache(key)(desiredEntry) flatMap { actualEntry =>\n        // ... and see what we get back\n\n        actualEntry.state.get match {\n          case Cached =>\n            logger.debug(this, \"cached read\")\n            makeNoteOfCacheHit(key)\n            actualEntry.unpack\n\n          case ReadInProgress =>\n            if (actualEntry == desiredEntry) {\n              logger.debug(this, \"read initiated\");\n              makeNoteOfCacheMiss(key)\n              // updating the cache with the new value is done in the listener\n              // and will complete unless an invalidation request or an intervening\n              // write occur in the meantime\n              listenForReadDone(key, actualEntry, generator, promise)\n              actualEntry.unpack\n            } else {\n              logger.debug(this, \"coalesced read\")\n              makeNoteOfCacheHit(key)\n              actualEntry.unpack\n            }\n\n          case WriteInProgress | InvalidateInProgress =>\n            logger.debug(this, \"reading around an update in progress\")\n            makeNoteOfCacheMiss(key)\n            generator\n        }\n      }\n    } else generator // not caching\n  }\n\n  protected def cacheUpdate(doc: W, key: CacheKey, generator: => Future[Winfo])(\n    implicit ec: ExecutionContext,\n    transid: TransactionId,\n    logger: Logging,\n    notifier: Option[CacheChangeNotification]): Future[Winfo] = {\n    cacheUpdate(Future.successful(doc), key, generator)\n  }\n\n  /**\n   * This method posts an update to the backing store, and potentially stores the result in the cache.\n   */\n  protected def cacheUpdate(f: Future[W], key: CacheKey, generator: => Future[Winfo])(\n    implicit ec: ExecutionContext,\n    transid: TransactionId,\n    logger: Logging,\n    notifier: Option[CacheChangeNotification]): Future[Winfo] = {\n    if (cacheEnabled) {\n\n      // try inserting our desired entry...\n      val desiredEntry = Entry(transid, WriteInProgress, Some(f))\n      cache(key)(desiredEntry) flatMap { actualEntry =>\n        // ... and see what we get back\n\n        if (actualEntry == desiredEntry) {\n          // then this transaction won the race to insert a new entry in the cache\n          // and it is responsible for updating the cache entry...\n          logger.info(this, s\"write initiated on new cache entry\")\n          listenForWriteDone(key, actualEntry, generator)\n        } else {\n          // ... otherwise, some existing entry is in the way, so try to grab a write lock\n          val currentState = actualEntry.state.get\n          val allowedToAssumeCompletion = currentState == Cached || currentState == ReadInProgress\n\n          if (allowedToAssumeCompletion && actualEntry.grabWriteLock(transid, currentState, desiredEntry.unpack)) {\n            // this transaction is now responsible for updating the cache entry\n            logger.info(this, s\"write initiated on existing cache entry, invalidating $key, $actualEntry\")\n            listenForWriteDone(key, actualEntry, generator)\n          } else {\n            // there is a conflicting operation in progress on this key\n            logger.info(this, s\"write-around (i.e., not cached) under $currentState\")\n            invalidateEntryAfter(generator, key, actualEntry)\n          }\n        }\n      } andThen {\n        case _ => notifier.foreach(_(key))\n      }\n    } else generator // not caching\n  }\n\n  def cacheSize: Int = cache.size\n\n  /**\n   * This method removes an entry from the cache immediately. You can use this method\n   * if you do not need to perform any updates on the backing store but only to the cache.\n   */\n  protected[database] def removeId(key: CacheKey)(implicit ec: ExecutionContext): Unit = cache.remove(key)\n\n  /**\n   * Log a cache hit\n   *\n   */\n  private def makeNoteOfCacheHit(key: CacheKey)(implicit transid: TransactionId, logger: Logging) = {\n    transid.started(this, LoggingMarkers.DATABASE_CACHE_HIT, s\"[GET] serving from cache: $key\")(logger)\n  }\n\n  /**\n   * Log a cache miss\n   *\n   */\n  private def makeNoteOfCacheMiss(key: CacheKey)(implicit transid: TransactionId, logger: Logging) = {\n    transid.started(this, LoggingMarkers.DATABASE_CACHE_MISS, s\"[GET] serving from datastore: $key\")(logger)\n  }\n\n  /**\n   * We have initiated a read (in cacheLookup), now handle its completion:\n   * 1. either cache the result if there is no intervening delete or update, or\n   * 2. invalidate the cache because there was an intervening delete or update.\n   */\n  private def listenForReadDone(key: CacheKey, entry: Entry, generator: => Future[W], promise: Promise[W])(\n    implicit ec: ExecutionContext,\n    transid: TransactionId,\n    logger: Logging): Unit = {\n\n    generator onComplete {\n      case Success(value) =>\n        // if the datastore read was successful, then try to transition to the Cached state\n        logger.debug(this, \"read backend part done, now marking cache entry as done\")\n\n        // always complete the promise for the generator since the read listener is\n        // only created when reading directly from the database (hence, must complete\n        // promise with the generated value)\n        promise success value\n\n        // now update the cache line\n        if (entry.readDone()) {\n          // cache entry is still in ReadInProgress and successful transitioned to Cached\n          // hence the new value is cached; nothing left to do\n        } else {\n          val cachedLineState = entry.state.get\n\n          cachedLineState match {\n            case WriteInProgress | Cached =>\n              // do nothing: if there was a write in progress, the write has not yet\n              // finished, but that operation has assumed ownership of the cache line\n              // and will update it; otherwise the write has completed and the value\n              // is now cached\n              ()\n            case _ =>\n              // some transaction requested an invalidation so remove the key from the cache,\n              // or there is an error in which case invalidate anyway, defensively, but log a message\n              invalidateEntry(key, entry)\n              if (cachedLineState != InvalidateWhenDone) {\n                // this should not happen, but could if the callback on the generator\n                // is delayed - invalidate the cache entry as a result\n                val error = ConcurrentOperationUnderRead(cachedLineState)\n                logger.warn(this, error.toString)\n              }\n          }\n        }\n\n      case Failure(t) =>\n        // oops, the datastore read failed. invalidate the cache entry\n        // note: that this might be a perfectly legitimate failure,\n        // e.g. a lookup for a non-existent key; we need to pass the particular t through\n        invalidateEntry(key, entry)\n        promise.failure(t)\n    }\n  }\n\n  /**\n   * We have initiated a write, now handle its completion:\n   * 1. either cache the result if there is no intervening delete or update, or\n   * 2. invalidate the cache cache because there was an intervening delete or update\n   */\n  private def listenForWriteDone(key: CacheKey, entry: Entry, generator: => Future[Winfo])(\n    implicit ec: ExecutionContext,\n    transid: TransactionId,\n    logger: Logging): Future[Winfo] = {\n\n    generator andThen {\n      case Success(_) =>\n        // if the datastore write was successful, then transition to the Cached state\n        logger.debug(this, \"write backend part done, now marking cache entry as done\")\n\n        if (entry.writeDone()) {\n          // entry transitioned from WriteInProgress to Cached state\n          logger.info(this, s\"write all done, caching $key ${entry.state.get}\")\n        } else {\n          // state transition from WriteInProgress to Cached fails so invalidate\n          // the entry in the cache\n          val prevState = entry.state.get\n          if (prevState != InvalidateWhenDone) {\n            // this should not happen but could for example during a document\n            // update where the \"read\" that would fetch a previous instance of\n            // the document fails because the document does not exist, but the\n            // future callback to invalidate the cache entry is delayed; so it\n            // is possible the state here is InvalidateInProgress as a result.\n            // the end result is to invalidate the entry, which may be unnecessary;\n            // so this is a performance hit, not a correctness concern.\n            val error = ConcurrentOperationUnderUpdate(prevState)\n            logger.warn(this, error.toString)\n          } else {\n            logger.info(this, s\"write done, but invalidating cache entry as requested\")\n          }\n          invalidateEntry(key, entry)\n        }\n\n      case Failure(_) => invalidateEntry(key, entry) // datastore write failed, invalidate cache entry\n    }\n  }\n\n  /** Immediately invalidates the given entry. */\n  private def invalidateEntry(key: CacheKey, entry: Entry)(implicit transid: TransactionId, logger: Logging): Unit = {\n    logger.info(this, s\"invalidating $key\")\n    entry.invalidate()\n    cache remove key\n  }\n\n  /** Invalidates the given entry after a given invalidator completes. */\n  private def invalidateEntryAfter[R](invalidator: => Future[R], key: CacheKey, entry: Entry)(\n    implicit ec: ExecutionContext,\n    transid: TransactionId,\n    logger: Logging): Future[R] = {\n\n    entry.grabInvalidationLock()\n    invalidator andThen {\n      case _ => invalidateEntry(key, entry)\n    }\n  }\n\n  /** This is the backing store. */\n  private lazy val cache: ConcurrentMapBackedCache[Entry] = createCache()\n\n  private def createCache() = {\n    val b = Caffeine\n      .newBuilder()\n      .softValues()\n\n    evictionPolicy match {\n      case AccessTime => b.expireAfterAccess(cacheExpirationTime, cacheExpirationTimeUnit)\n      case _          => b.expireAfterWrite(cacheExpirationTime, cacheExpirationTimeUnit)\n    }\n\n    if (fixedCacheSize > 0) b.maximumSize(fixedCacheSize)\n    new ConcurrentMapBackedCache(b.build().asMap().asInstanceOf[ConcurrentMap[Any, Future[Entry]]])\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/RemoteCacheInvalidation.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.nio.charset.StandardCharsets\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration.DurationInt\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.actor.Props\nimport spray.json._\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.core.connector.Message\nimport org.apache.openwhisk.core.connector.MessageFeed\nimport org.apache.openwhisk.core.connector.MessagingProvider\nimport org.apache.openwhisk.core.entity.CacheKey\nimport org.apache.openwhisk.core.entity.ControllerInstanceId\nimport org.apache.openwhisk.core.entity.WhiskAction\nimport org.apache.openwhisk.core.entity.WhiskActionMetaData\nimport org.apache.openwhisk.core.entity.WhiskPackage\nimport org.apache.openwhisk.core.entity.WhiskRule\nimport org.apache.openwhisk.core.entity.WhiskTrigger\nimport org.apache.openwhisk.spi.SpiLoader\nimport pureconfig._\n\ncase class CacheInvalidationMessage(key: CacheKey, instanceId: String) extends Message {\n  override def serialize = CacheInvalidationMessage.serdes.write(this).compactPrint\n}\n\nobject CacheInvalidationMessage extends DefaultJsonProtocol {\n  def parse(msg: String) = Try(serdes.read(msg.parseJson))\n  implicit val serdes = jsonFormat(CacheInvalidationMessage.apply _, \"key\", \"instanceId\")\n}\n\nclass RemoteCacheInvalidation(config: WhiskConfig, component: String, instance: ControllerInstanceId)(\n  implicit logging: Logging,\n  as: ActorSystem) {\n  import RemoteCacheInvalidation._\n  implicit private val ec = as.dispatchers.lookup(\"dispatchers.kafka-dispatcher\")\n\n  private val instanceId = s\"$component${instance.asString}\"\n\n  private val msgProvider = SpiLoader.get[MessagingProvider]\n  private val cacheInvalidationConsumer =\n    msgProvider.getConsumer(config, s\"$cacheInvalidationTopic$instanceId\", cacheInvalidationTopic, maxPeek = 128)\n  private val cacheInvalidationProducer = msgProvider.getProducer(config)\n\n  def notifyOtherInstancesAboutInvalidation(key: CacheKey): Future[Unit] = {\n    cacheInvalidationProducer.send(cacheInvalidationTopic, CacheInvalidationMessage(key, instanceId)).map(_ => ())\n  }\n\n  private val invalidationFeed = as.actorOf(Props {\n    new MessageFeed(\n      \"cacheInvalidation\",\n      logging,\n      cacheInvalidationConsumer,\n      cacheInvalidationConsumer.maxPeek,\n      1.second,\n      removeFromLocalCache)\n  })\n\n  def invalidateWhiskActionMetaData(key: CacheKey) =\n    WhiskActionMetaData.removeId(key)\n\n  private def removeFromLocalCache(bytes: Array[Byte]): Future[Unit] = Future {\n    val raw = new String(bytes, StandardCharsets.UTF_8)\n\n    CacheInvalidationMessage.parse(raw) match {\n      case Success(msg: CacheInvalidationMessage) => {\n        if (msg.instanceId != instanceId) {\n          WhiskActionMetaData.removeId(msg.key)\n          WhiskAction.removeId(msg.key)\n          WhiskPackage.removeId(msg.key)\n          WhiskRule.removeId(msg.key)\n          WhiskTrigger.removeId(msg.key)\n        }\n      }\n      case Failure(t) => logging.error(this, s\"failed processing message: $raw with $t\")\n    }\n    invalidationFeed ! MessageFeed.Processed\n  }\n}\n\nobject RemoteCacheInvalidation {\n  val topicPrefix = loadConfigOrThrow[String](ConfigKeys.kafkaTopicsPrefix)\n  val cacheInvalidationTopic = topicPrefix + \"cacheInvalidation\"\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/StoreUtils.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.security.MessageDigest\n\nimport org.apache.pekko.event.Logging.ErrorLevel\nimport org.apache.pekko.stream.SinkShape\nimport org.apache.pekko.stream.scaladsl.{Broadcast, Flow, GraphDSL, Keep, Sink}\nimport org.apache.pekko.util.ByteString\nimport com.google.common.base.Throwables\nimport spray.json.DefaultJsonProtocol._\nimport spray.json.{JsObject, JsValue, RootJsonFormat}\nimport org.apache.openwhisk.common.{Logging, StartMarker, TransactionId}\nimport org.apache.openwhisk.core.entity.{DocInfo, DocRevision, DocumentReader, WhiskDocument}\n\nimport scala.concurrent.{ExecutionContext, Future}\n\nprivate[database] object StoreUtils {\n  private val digestAlgo = \"SHA-256\"\n  private val encodedAlgoName = digestAlgo.toLowerCase.replaceAllLiterally(\"-\", \"\")\n\n  def reportFailure[T](f: Future[T], start: StartMarker, failureMessage: Throwable => String)(\n    implicit transid: TransactionId,\n    logging: Logging,\n    ec: ExecutionContext): Future[T] = {\n    f.failed.foreach {\n      case _: ArtifactStoreException => // These failures are intentional and shouldn't trigger the catcher.\n      case x =>\n        transid.failed(\n          this,\n          start,\n          s\"${failureMessage(x)} [${x.getClass.getSimpleName}]\\n\" + Throwables.getStackTraceAsString(x),\n          ErrorLevel)\n    }\n    f\n  }\n\n  def checkDocHasRevision(doc: DocInfo): Unit = {\n    require(doc != null, \"doc undefined\")\n    require(doc.rev.rev != null, \"doc revision must be specified\")\n  }\n\n  def deserialize[A <: DocumentAbstraction, DocumentAbstraction](doc: DocInfo, js: JsObject)(\n    implicit docReader: DocumentReader,\n    ma: Manifest[A],\n    jsonFormat: RootJsonFormat[DocumentAbstraction]): A = {\n    val asFormat = try {\n      docReader.read(ma, js)\n    } catch {\n      case _: Exception => jsonFormat.read(js)\n    }\n\n    if (asFormat.getClass != ma.runtimeClass) {\n      throw DocumentTypeMismatchException(\n        s\"document type ${asFormat.getClass} did not match expected type ${ma.runtimeClass}.\")\n    }\n\n    val deserialized = asFormat.asInstanceOf[A]\n\n    val responseRev = js.fields(\"_rev\").convertTo[String]\n    if (doc.rev.rev != null && doc.rev.rev != responseRev) {\n      throw DocumentRevisionMismatchException(\n        s\"Returned revision should match original argument ${doc.rev.rev} ${responseRev}\")\n    }\n    // FIXME remove mutability from appropriate classes now that it is no longer required by GSON.\n    deserialized.asInstanceOf[WhiskDocument].revision(DocRevision(responseRev))\n  }\n\n  def combinedSink[T](dest: Sink[ByteString, Future[T]])(\n    implicit ec: ExecutionContext): Sink[ByteString, Future[AttachmentUploadResult[T]]] = {\n    Sink.fromGraph(GraphDSL.createGraph(digestSink(), lengthSink(), dest)(combineResult) {\n      implicit builder => (dgs, ls, dests) =>\n        import GraphDSL.Implicits._\n\n        val bcast = builder.add(Broadcast[ByteString](3))\n\n        bcast ~> dgs.in\n        bcast ~> ls.in\n        bcast ~> dests.in\n\n        SinkShape(bcast.in)\n    })\n  }\n\n  def emptyDigest(): MessageDigest = MessageDigest.getInstance(digestAlgo)\n\n  def encodeDigest(bytes: Array[Byte]): String = {\n    val digest = bytes.map(\"%02x\".format(_)).mkString\n    s\"$encodedAlgoName-$digest\"\n  }\n\n  /**\n   * Transforms a json object by adding and removing fields\n   *\n   * @param json base json object to transform\n   * @param fieldsToAdd list of fields to add. If the value provided is `None` then it would be ignored\n   * @param fieldsToRemove list of field names to remove\n   * @return transformed json\n   */\n  def transform(json: JsObject,\n                fieldsToAdd: Seq[(String, Option[JsValue])],\n                fieldsToRemove: Seq[String] = Seq.empty): JsObject = {\n    val fields = json.fields ++ fieldsToAdd.flatMap(f => f._2.map((f._1, _))) -- fieldsToRemove\n    JsObject(fields)\n  }\n\n  private def combineResult[T](digest: Future[String], length: Future[Long], upload: Future[T])(\n    implicit ec: ExecutionContext) = {\n    for {\n      d <- digest\n      l <- length\n      u <- upload\n    } yield AttachmentUploadResult(d, l, u)\n  }\n\n  case class AttachmentUploadResult[T](digest: String, length: Long, uploadResult: T)\n\n  private def digestSink(): Sink[ByteString, Future[String]] = {\n    Flow[ByteString]\n      .fold(emptyDigest())((digest, bytes) => { digest.update(bytes.toArray); digest })\n      .map(md => encodeDigest(md.digest()))\n      .toMat(Sink.head)(Keep.right)\n  }\n\n  private def lengthSink(): Sink[ByteString, Future[Long]] = {\n    Sink.fold[Long, ByteString](0)((length, bytes) => length + bytes.size)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/azblob/AzureBlobAttachmentStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.azblob\n\nimport java.time.OffsetDateTime\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport org.apache.pekko.http.scaladsl.model.{ContentType, HttpRequest, HttpResponse, Uri}\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.util.{ByteString, ByteStringBuilder}\nimport com.azure.storage.blob.sas.{BlobContainerSasPermission, BlobServiceSasSignatureValues}\nimport com.azure.storage.blob.{BlobContainerAsyncClient, BlobContainerClientBuilder, BlobUrlParts}\nimport com.azure.storage.common.StorageSharedKeyCredential\nimport com.azure.storage.common.policy.{RequestRetryOptions, RetryPolicyType}\nimport com.typesafe.config.Config\nimport org.apache.openwhisk.common.LoggingMarkers.{\n  DATABASE_ATTS_DELETE,\n  DATABASE_ATT_DELETE,\n  DATABASE_ATT_GET,\n  DATABASE_ATT_SAVE\n}\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.StoreUtils.{combinedSink, reportFailure}\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity.DocId\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport reactor.core.publisher.Flux\n\nimport scala.compat.java8.FutureConverters._\nimport scala.concurrent.duration.FiniteDuration\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.reflect.ClassTag\n\ncase class AzureCDNConfig(domainName: String)\ncase class AzBlobConfig(endpoint: String,\n                        accountKey: String,\n                        containerName: String,\n                        accountName: String,\n                        connectionString: Option[String],\n                        prefix: Option[String],\n                        retryConfig: AzBlobRetryConfig,\n                        azureCdnConfig: Option[AzureCDNConfig] = None) {\n  def prefixFor[D](implicit tag: ClassTag[D]): String = {\n    val className = tag.runtimeClass.getSimpleName.toLowerCase\n    prefix.map(p => s\"$p/$className\").getOrElse(className)\n  }\n}\ncase class AzBlobRetryConfig(retryPolicyType: RetryPolicyType,\n                             maxTries: Int,\n                             tryTimeout: FiniteDuration,\n                             retryDelay: FiniteDuration,\n                             secondaryHost: Option[String])\nobject AzureBlobAttachmentStoreProvider extends AttachmentStoreProvider {\n  override def makeStore[D <: DocumentSerializer: ClassTag]()(implicit actorSystem: ActorSystem,\n                                                              logging: Logging): AttachmentStore = {\n    makeStore[D](actorSystem.settings.config)\n  }\n\n  def makeStore[D <: DocumentSerializer: ClassTag](config: Config)(implicit actorSystem: ActorSystem,\n                                                                   logging: Logging): AttachmentStore = {\n    val azConfig = loadConfigOrThrow[AzBlobConfig](config, ConfigKeys.azBlob)\n    new AzureBlobAttachmentStore(createClient(azConfig), azConfig.prefixFor[D], azConfig)\n  }\n\n  def createClient(config: AzBlobConfig): BlobContainerAsyncClient = {\n    val builder = new BlobContainerClientBuilder()\n\n    //If connection string is specified then it would have all needed info\n    //Mostly used for testing using Azurite\n    config.connectionString match {\n      case Some(s) => builder.connectionString(s)\n      case _ =>\n        builder\n          .endpoint(config.endpoint)\n          .credential(new StorageSharedKeyCredential(config.accountName, config.accountKey))\n    }\n\n    builder\n      .containerName(config.containerName)\n      .retryOptions(new RequestRetryOptions(\n        config.retryConfig.retryPolicyType,\n        config.retryConfig.maxTries,\n        config.retryConfig.tryTimeout.toSeconds.toInt,\n        config.retryConfig.retryDelay.toMillis,\n        config.retryConfig.retryDelay.toMillis,\n        config.retryConfig.secondaryHost.orNull))\n      .buildAsyncClient()\n  }\n}\n\nclass AzureBlobAttachmentStore(client: BlobContainerAsyncClient, prefix: String, config: AzBlobConfig)(\n  implicit\n  system: ActorSystem,\n  logging: Logging)\n    extends AttachmentStore {\n  override protected[core] def scheme: String = \"az\"\n\n  override protected[core] implicit val executionContext: ExecutionContext = system.dispatcher\n\n  override protected[core] def attach(\n    docId: DocId,\n    name: String,\n    contentType: ContentType,\n    docStream: Source[ByteString, _])(implicit transid: TransactionId): Future[AttachResult] = {\n    require(name != null, \"name undefined\")\n    val start =\n      transid.started(this, DATABASE_ATT_SAVE, s\"[ATT_PUT] uploading attachment '$name' of document 'id: $docId'\")\n    val blobClient = getBlobClient(docId, name)\n\n    //TODO Use BlobAsyncClient#upload(Flux<ByteBuffer>, com.azure.storage.blob.models.ParallelTransferOptions, boolean)\n    val uploadSink = Sink.fold[ByteStringBuilder, ByteString](new ByteStringBuilder)((builder, b) => builder ++= b)\n\n    val f = docStream.runWith(combinedSink(uploadSink))\n    val g = f.flatMap { r =>\n      val buff = r.uploadResult.result().compact\n      val uf = blobClient.upload(Flux.fromArray(Array(buff.asByteBuffer)), buff.size).toFuture.toScala\n      uf.map(_ => AttachResult(r.digest, r.length))\n    }\n\n    g.foreach(_ =>\n      transid\n        .finished(this, start, s\"[ATT_PUT] '$prefix' completed uploading attachment '$name' of document 'id: $docId'\"))\n\n    reportFailure(\n      g,\n      start,\n      failure => s\"[ATT_PUT] '$prefix' internal error, name: '$name', doc: '$docId', failure: '${failure.getMessage}'\")\n  }\n\n  override protected[core] def readAttachment[T](docId: DocId, name: String, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T] = {\n    require(name != null, \"name undefined\")\n    val start =\n      transid.started(\n        this,\n        DATABASE_ATT_GET,\n        s\"[ATT_GET] '$prefix' finding attachment '$name' of document 'id: $docId'\")\n    val source = getAttachmentSource(objectKey(docId, name), config)\n\n    val f = source.flatMap {\n      case Some(x) => x.runWith(sink)\n      case None    => Future.failed(NoDocumentException(\"Not found on 'readAttachment'.\"))\n    }\n\n    val g = f.transform(\n      { s =>\n        transid\n          .finished(this, start, s\"[ATT_GET] '$prefix' completed: found attachment '$name' of document 'id: $docId'\")\n        s\n      }, {\n        case e: NoDocumentException =>\n          transid\n            .finished(\n              this,\n              start,\n              s\"[ATT_GET] '$prefix', retrieving attachment '$name' of document 'id: $docId'; not found.\",\n              logLevel = Logging.ErrorLevel)\n          e\n        case e => e\n      })\n\n    reportFailure(\n      g,\n      start,\n      failure =>\n        s\"[ATT_GET] '$prefix' internal error, name: '$name', doc: 'id: $docId', failure: '${failure.getMessage}'\")\n  }\n\n  override protected[core] def deleteAttachments(docId: DocId)(implicit transid: TransactionId): Future[Boolean] = {\n    val start =\n      transid.started(\n        this,\n        DATABASE_ATTS_DELETE,\n        s\"[ATTS_DELETE] deleting attachments of document 'id: $docId' with prefix ${objectKeyPrefix(docId)}\")\n\n    var count = 0\n    val f = Source\n      .fromPublisher(client.listBlobsByHierarchy(objectKeyPrefix(docId)))\n      .mapAsync(1) { b =>\n        count += 1\n        val startDelete =\n          transid.started(\n            this,\n            DATABASE_ATT_DELETE,\n            s\"[ATT_DELETE] deleting attachment '${b.getName}' of document 'id: $docId'\")\n        client\n          .getBlobAsyncClient(b.getName)\n          .delete()\n          .toFuture\n          .toScala\n          .map(\n            _ =>\n              transid.finished(\n                this,\n                startDelete,\n                s\"[ATT_DELETE] completed: deleting attachment '${b.getName}' of document 'id: $docId'\"))\n          .recover {\n            case t =>\n              transid.failed(\n                this,\n                startDelete,\n                s\"[ATT_DELETE] failed: deleting attachment '${b.getName}' of document 'id: $docId' error: $t\")\n          }\n\n      }\n      .recover {\n        case t =>\n          logging.error(this, s\"[ATT_DELETE] :error in delete ${t}\")\n          throw t\n      }\n      .runWith(Sink.seq)\n      .map(_ => true)\n\n    f.foreach(\n      _ =>\n        transid.finished(\n          this,\n          start,\n          s\"[ATTS_DELETE] completed: deleting ${count} attachments of document 'id: $docId'\",\n          InfoLevel))\n\n    reportFailure(\n      f,\n      start,\n      failure => s\"[ATTS_DELETE] '$prefix' internal error, doc: '$docId', failure: '${failure.getMessage}'\")\n  }\n\n  override protected[core] def deleteAttachment(docId: DocId, name: String)(implicit\n                                                                            transid: TransactionId): Future[Boolean] = {\n    val start =\n      transid.started(this, DATABASE_ATT_DELETE, s\"[ATT_DELETE] deleting attachment '$name' of document 'id: $docId'\")\n\n    val f = getBlobClient(docId, name).delete().toFuture.toScala.map(_ => true)\n\n    f.foreach(_ =>\n      transid.finished(this, start, s\"[ATT_DELETE] completed: deleting attachment '$name' of document 'id: $docId'\"))\n\n    reportFailure(\n      f,\n      start,\n      failure => s\"[ATT_DELETE] '$prefix' internal error, doc: '$docId', failure: '${failure.getMessage}'\")\n  }\n\n  override def shutdown(): Unit = {}\n\n  private def objectKey(id: DocId, name: String): String = s\"$prefix/${id.id}/$name\"\n\n  private def objectKeyPrefix(id: DocId): String =\n    s\"$prefix/${id.id}/\" //must end with a slash so that \".../<package>/<action>other\" does not match for \"<package>/<action>\"\n\n  private def getBlobClient(docId: DocId, name: String) =\n    client.getBlobAsyncClient(objectKey(docId, name)).getBlockBlobAsyncClient\n\n  private def getAttachmentSource(objectKey: String, config: AzBlobConfig)(\n    implicit\n    tid: TransactionId): Future[Option[Source[ByteString, Any]]] = {\n    val blobClient = client.getBlobAsyncClient(objectKey).getBlockBlobAsyncClient\n\n    config.azureCdnConfig match {\n      case Some(cdnConfig) =>\n        //setup sas token\n        def expiryTime = OffsetDateTime.now().plusDays(1)\n        def permissions =\n          new BlobContainerSasPermission()\n            .setReadPermission(true)\n        val sigValues = new BlobServiceSasSignatureValues(expiryTime, permissions)\n        val sas = blobClient.generateSas(sigValues)\n        //parse the url, and reset the host\n        val parts = BlobUrlParts.parse(blobClient.getBlobUrl)\n        val url = parts.setHost(cdnConfig.domainName)\n        logging.info(\n          this,\n          s\"[ATT_GET] '$prefix' downloading attachment from azure cdn '$objectKey' with url (sas params not displayed) ${url}\")\n        //append the sas params to the url before downloading\n        val cdnUrlWithSas = s\"${url.toUrl.toString}?$sas\"\n        getUrlContent(cdnUrlWithSas)\n      case None =>\n        blobClient.exists().toFuture.toScala.map { exists =>\n          if (exists) {\n            val bbFlux = blobClient.downloadStream()\n            Some(Source.fromPublisher(bbFlux).map(ByteString.fromByteBuffer))\n          } else {\n            throw NoDocumentException(\"Not found on 'readAttachment'.\")\n          }\n        }\n    }\n  }\n  private def getUrlContent(uri: Uri): Future[Option[Source[ByteString, Any]]] = {\n    val future = Http().singleRequest(HttpRequest(uri = uri))\n    future.flatMap {\n      case HttpResponse(status, _, entity, _) if status.isSuccess() && !status.isRedirection() =>\n        Future.successful(Some(entity.dataBytes))\n      case HttpResponse(status, _, entity, _) =>\n        if (status == NotFound) {\n          entity.discardBytes()\n          throw NoDocumentException(\"Not found on 'readAttachment'.\")\n        } else {\n          Unmarshal(entity).to[String].map { err =>\n            throw new Exception(s\"failed to download ${uri} status was ${status} response was ${err}\")\n          }\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/CollectionResourceUsage.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport org.apache.commons.io.FileUtils\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.SizeUnits.KB\n\ncase class CollectionResourceUsage(documentsSize: Option[ByteSize],\n                                   collectionSize: Option[ByteSize],\n                                   documentsCount: Option[Long],\n                                   indexingProgress: Option[Int],\n                                   documentsSizeQuota: Option[ByteSize]) {\n  def indexSize: Option[ByteSize] = {\n    for {\n      ds <- documentsSize\n      cs <- collectionSize\n    } yield cs - ds\n  }\n\n  def asString: String = {\n    List(\n      documentsSize.map(ds => s\"documentSize: ${displaySize(ds)}\"),\n      indexSize.map(is => s\"indexSize: ${displaySize(is)}\"),\n      documentsCount.map(dc => s\"documentsCount: $dc\"),\n      documentsSizeQuota.map(dq => s\"collectionSizeQuota: ${displaySize(dq)}\")).flatten.mkString(\",\")\n  }\n\n  private def displaySize(b: ByteSize) = FileUtils.byteCountToDisplaySize(b.toBytes)\n}\n\nobject CollectionResourceUsage {\n  val quotaHeader = \"x-ms-resource-quota\"\n  val usageHeader = \"x-ms-resource-usage\"\n  val indexHeader = \"x-ms-documentdb-collection-index-transformation-progress\"\n\n  def apply(responseHeaders: Map[String, String]): Option[CollectionResourceUsage] = {\n    for {\n      quota <- responseHeaders.get(quotaHeader).map(headerValueToMap)\n      usage <- responseHeaders.get(usageHeader).map(headerValueToMap)\n    } yield {\n      CollectionResourceUsage(\n        usage.get(\"documentsSize\").map(_.toLong).map(ByteSize(_, KB)),\n        usage.get(\"collectionSize\").map(_.toLong).map(ByteSize(_, KB)),\n        usage.get(\"documentsCount\").map(_.toLong),\n        responseHeaders.get(indexHeader).map(_.toInt),\n        quota.get(\"collectionSize\").map(_.toLong).map(ByteSize(_, KB)))\n    }\n  }\n\n  private def headerValueToMap(value: String): Map[String, String] = {\n    //storedProcedures=100;triggers=25;functions=25;documentsCount=-1;documentsSize=xxx;collectionSize=xxx\n    val pairs = value.split(\"=|;\").grouped(2)\n    pairs.map { case Array(k, v) => k -> v }.toMap\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBArtifactStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport _root_.rx.RxReactiveStreams\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.pekko.http.scaladsl.model.{ContentType, StatusCodes, Uri}\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.util.ByteString\nimport com.microsoft.azure.cosmosdb._\nimport com.microsoft.azure.cosmosdb.internal.Constants.Properties\nimport com.microsoft.azure.cosmosdb.rx.AsyncDocumentClient\nimport kamon.metric.MeasurementUnit\nimport org.apache.openwhisk.common.{LogMarkerToken, Logging, LoggingMarkers, MetricEmitter, Scheduler, TransactionId}\nimport org.apache.openwhisk.core.database.StoreUtils._\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.database.cosmosdb.CosmosDBArtifactStoreProvider.DocumentClientRef\nimport org.apache.openwhisk.core.database.cosmosdb.CosmosDBConstants._\nimport org.apache.openwhisk.core.entity.Attachments.Attached\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.Messages\nimport spray.json._\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.concurrent.duration._\nimport scala.util.Success\n\nclass CosmosDBArtifactStore[DocumentAbstraction <: DocumentSerializer](protected val collName: String,\n                                                                       protected val config: CosmosDBConfig,\n                                                                       clientRef: DocumentClientRef,\n                                                                       documentHandler: DocumentHandler,\n                                                                       protected val viewMapper: CosmosDBViewMapper,\n                                                                       val inliningConfig: InliningConfig,\n                                                                       val attachmentStore: Option[AttachmentStore])(\n  implicit system: ActorSystem,\n  val logging: Logging,\n  jsonFormat: RootJsonFormat[DocumentAbstraction],\n  docReader: DocumentReader)\n    extends ArtifactStore[DocumentAbstraction]\n    with DefaultJsonProtocol\n    with DocumentProvider\n    with CosmosDBSupport\n    with AttachmentSupport[DocumentAbstraction] {\n\n  private val cosmosScheme = \"cosmos\"\n  val attachmentScheme: String = attachmentStore.map(_.scheme).getOrElse(cosmosScheme)\n\n  protected val client: AsyncDocumentClient = clientRef.get.client\n  private[cosmosdb] val (database, collection) = initialize()\n\n  private val putToken = createToken(\"put\", read = false)\n  private val delToken = createToken(\"del\", read = false)\n  private val getToken = createToken(\"get\")\n  private val queryToken = createToken(\"query\")\n  private val countToken = createToken(\"count\")\n  private val docSizeToken = createDocSizeToken()\n\n  private val documentsSizeToken = createUsageToken(\"documentsSize\", MeasurementUnit.information.kilobytes)\n  private val indexSizeToken = createUsageToken(\"indexSize\", MeasurementUnit.information.kilobytes)\n  private val documentCountToken = createUsageToken(\"documentCount\")\n\n  private val softDeleteTTL = config.softDeleteTTL.map(_.toSeconds.toInt)\n\n  private val clusterIdValue = config.clusterId.map(JsString(_))\n\n  logging.info(\n    this,\n    s\"Initializing CosmosDBArtifactStore for collection [$collName]. Service endpoint [${client.getServiceEndpoint}], \" +\n      s\"Read endpoint [${client.getReadEndpoint}], Write endpoint [${client.getWriteEndpoint}], Connection Policy [${client.getConnectionPolicy}], \" +\n      s\"Time to live [${collection.getDefaultTimeToLive} secs, clusterId [${config.clusterId}], soft delete TTL [${config.softDeleteTTL}], \" +\n      s\"Consistency Level [${config.consistencyLevel}], Usage Metric Frequency [${config.recordUsageFrequency}]\")\n\n  private val usageMetricRecorder = config.recordUsageFrequency.map { f =>\n    Scheduler.scheduleWaitAtLeast(f, 10.seconds)(() => recordResourceUsage())\n  }\n\n  //Clone the returned instance as these are mutable\n  def documentCollection(): DocumentCollection = new DocumentCollection(collection.toJson)\n\n  override protected[core] implicit val executionContext: ExecutionContext = system.dispatcher\n\n  override protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo] = {\n    val asJson = d.toDocumentRecord\n\n    val (doc, docSize) = toCosmosDoc(asJson)\n    val id = doc.getId\n    val docinfoStr = s\"id: $id, rev: ${doc.getETag}\"\n    val start = transid.started(this, LoggingMarkers.DATABASE_SAVE, s\"[PUT] '$collName' saving document: '$docinfoStr'\")\n\n    val o = if (isNewDocument(doc)) {\n      client.createDocument(collection.getSelfLink, doc, newRequestOption(id), true)\n    } else {\n      client.replaceDocument(doc, matchRevOption(id, doc.getETag))\n    }\n\n    val f = o\n      .head()\n      .recoverWith {\n        case e: DocumentClientException if isConflict(e) && isNewDocument(doc) =>\n          val docId = DocId(asJson.fields(_id).convertTo[String])\n          //Fetch existing document and check if its deleted\n          getRaw(docId).flatMap {\n            case Some(js) =>\n              if (isSoftDeleted(js)) {\n                //Existing document is soft deleted. So can be replaced. Use the etag of document\n                //and replace it with document we are trying to add\n                val etag = js.fields(Properties.E_TAG).convertTo[String]\n                client.replaceDocument(doc, matchRevOption(id, etag)).head()\n              } else {\n                //Trying to create a new document and found an existing\n                //Document which is valid (not soft delete) then conflict is a valid outcome\n                throw e\n              }\n            case None =>\n              //Document not found. Should not happen unless someone else removed\n              //Propagate existing exception\n              throw e\n          }\n      }\n      .transform(\n        { r =>\n          docSizeToken.histogram.record(docSize)\n          transid.finished(\n            this,\n            start,\n            s\"[PUT] '$collName' completed document: '$docinfoStr', size=$docSize, ru=${r.getRequestCharge}${extraLogs(r)}\",\n            InfoLevel)\n          collectMetrics(putToken, r.getRequestCharge)\n          toDocInfo(r.getResource)\n        }, {\n          case e: DocumentClientException if isConflict(e) =>\n            transid.finished(this, start, s\"[PUT] '$collName', document: '$docinfoStr'; conflict.\")\n            DocumentConflictException(\"conflict on 'put'\")\n          case e => e\n        })\n\n    reportFailure(f, start, failure => s\"[PUT] '$collName' internal error, failure: '${failure.getMessage}'\")\n  }\n\n  override protected[database] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] = {\n    checkDocHasRevision(doc)\n    val start = transid.started(this, LoggingMarkers.DATABASE_DELETE, s\"[DEL] '$collName' deleting document: '$doc'\")\n    val f = softDeleteTTL match {\n      case Some(_) => softDelete(doc)\n      case None    => hardDelete(doc)\n    }\n    val g = f\n      .transform(\n        { r =>\n          transid.finished(this, start, s\"[DEL] '$collName' completed document: '$doc'${extraLogs(r)}\", InfoLevel)\n          true\n        }, {\n          case e: DocumentClientException if isNotFound(e) =>\n            transid.finished(this, start, s\"[DEL] '$collName', document: '$doc'; not found.\")\n            NoDocumentException(\"not found on 'delete'\")\n          case e: DocumentClientException if isConflict(e) =>\n            transid.finished(this, start, s\"[DEL] '$collName', document: '$doc'; conflict.\")\n            DocumentConflictException(\"conflict on 'delete'\")\n          case e => e\n        })\n\n    reportFailure(\n      g,\n      start,\n      failure => s\"[DEL] '$collName' internal error, doc: '$doc', failure: '${failure.getMessage}'\")\n  }\n\n  private def hardDelete(doc: DocInfo) = {\n    val f = client\n      .deleteDocument(selfLinkOf(doc.id), matchRevOption(doc))\n      .head()\n    f.foreach(r => collectMetrics(delToken, r.getRequestCharge))\n    f\n  }\n\n  private def softDelete(doc: DocInfo)(implicit transid: TransactionId) = {\n    for {\n      js <- getAsWhiskJson(doc.id)\n      r <- softDeletePut(doc, js)\n    } yield r\n  }\n\n  private def softDeletePut(docInfo: DocInfo, js: JsObject)(implicit transid: TransactionId) = {\n    val deletedJs = transform(js, Seq((deleted, Some(JsTrue))))\n    val (doc, _) = toCosmosDoc(deletedJs)\n    softDeleteTTL.foreach(doc.setTimeToLive(_))\n    val f = client.replaceDocument(doc, matchRevOption(docInfo)).head()\n    f.foreach(r => collectMetrics(putToken, r.getRequestCharge))\n    f\n  }\n\n  override protected[database] def get[A <: DocumentAbstraction](doc: DocInfo,\n                                                                 attachmentHandler: Option[(A, Attached) => A] = None)(\n    implicit transid: TransactionId,\n    ma: Manifest[A]): Future[A] = {\n    val start = transid.started(this, LoggingMarkers.DATABASE_GET, s\"[GET] '$collName' finding document: '$doc'\")\n\n    require(doc != null, \"doc undefined\")\n    val f =\n      client\n        .readDocument(selfLinkOf(doc.id), newRequestOption(doc.id))\n        .head()\n        .transform(\n          { rr =>\n            collectMetrics(getToken, rr.getRequestCharge)\n            if (isSoftDeleted(rr.getResource)) {\n              transid.finished(this, start, s\"[GET] '$collName', document: '$doc'; not found.\")\n              // for compatibility\n              throw NoDocumentException(\"not found on 'get'\")\n            } else {\n              val (js, docSize) = getResultToWhiskJsonDoc(rr.getResource)\n              transid\n                .finished(\n                  this,\n                  start,\n                  s\"[GET] '$collName' completed: found document '$doc',size=$docSize, ru=${rr.getRequestCharge}${extraLogs(rr)}\",\n                  InfoLevel)\n              deserialize[A, DocumentAbstraction](doc, js)\n            }\n          }, {\n            case e: DocumentClientException if isNotFound(e) =>\n              transid.finished(this, start, s\"[GET] '$collName', document: '$doc'; not found.\")\n              // for compatibility\n              throw NoDocumentException(\"not found on 'get'\")\n            case e => e\n          })\n        .recoverWith {\n          case _: DeserializationException => throw DocumentUnreadable(Messages.corruptedEntity)\n        }\n\n    reportFailure(\n      f,\n      start,\n      failure => s\"[GET] '$collName' internal error, doc: '$doc', failure: '${failure.getMessage}'\")\n\n  }\n\n  override protected[database] def get(id: DocId)(implicit transid: TransactionId): Future[Option[JsObject]] = {\n    val start = transid.started(this, LoggingMarkers.DATABASE_GET, s\"[GET_BY_ID] '$collName' finding document: '$id'\")\n\n    val f = client\n      .readDocument(selfLinkOf(id), newRequestOption(id))\n      .head()\n      .map { rr =>\n        collectMetrics(getToken, rr.getRequestCharge)\n        if (isSoftDeleted(rr.getResource)) {\n          transid.finished(this, start, s\"[GET_BY_ID] '$collName' completed: '$id' not found\")\n          None\n        } else {\n          val (js, _) = getResultToWhiskJsonDoc(rr.getResource)\n          transid.finished(this, start, s\"[GET_BY_ID] '$collName' completed: found document '$id'\")\n          Some(js)\n        }\n      }\n      .recoverWith {\n        case e: DocumentClientException if isNotFound(e) =>\n          transid.finished(this, start, s\"[GET_BY_ID] '$collName' completed: '$id' not found\")\n          Future.successful(None)\n      }\n\n    reportFailure(\n      f,\n      start,\n      failure => s\"[GET_BY_ID] '$collName' internal error, doc: '$id', failure: '${failure.getMessage}'\")\n  }\n\n  /**\n   * Method exposed for test cases to access the raw json returned by CosmosDB\n   */\n  private[cosmosdb] def getRaw(id: DocId): Future[Option[JsObject]] = {\n    client\n      .readDocument(selfLinkOf(id), newRequestOption(id))\n      .head()\n      .map { rr =>\n        val js = rr.getResource.toJson.parseJson.asJsObject\n        Some(js)\n      }\n      .recoverWith {\n        case e: DocumentClientException if isNotFound(e) => Future.successful(None)\n      }\n  }\n\n  private def getAsWhiskJson(id: DocId): Future[JsObject] = {\n    client\n      .readDocument(selfLinkOf(id), newRequestOption(id))\n      .head()\n      .map { rr =>\n        val (js, _) = getResultToWhiskJsonDoc(rr.getResource)\n        collectMetrics(getToken, rr.getRequestCharge)\n        js\n      }\n  }\n\n  override protected[core] def query(table: String,\n                                     startKey: List[Any],\n                                     endKey: List[Any],\n                                     skip: Int,\n                                     limit: Int,\n                                     includeDocs: Boolean,\n                                     descending: Boolean,\n                                     reduce: Boolean,\n                                     stale: StaleParameter)(implicit transid: TransactionId): Future[List[JsObject]] = {\n    require(!(reduce && includeDocs), \"reduce and includeDocs cannot both be true\")\n    require(!reduce, \"Reduce scenario not supported\") //TODO Investigate reduce\n    require(skip >= 0, \"skip should be non negative\")\n    require(limit >= 0, \"limit should be non negative\")\n    documentHandler.checkIfTableSupported(table)\n\n    val Array(ddoc, viewName) = table.split(\"/\")\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s\"[QUERY] '$collName' searching '$table'\")\n    val realIncludeDocs = includeDocs | documentHandler.shouldAlwaysIncludeDocs(ddoc, viewName)\n    val realLimit = if (limit > 0) skip + limit else limit\n\n    val querySpec = viewMapper.prepareQuery(ddoc, viewName, startKey, endKey, realLimit, realIncludeDocs, descending)\n\n    val options = newFeedOptions()\n    val queryMetrics = scala.collection.mutable.Buffer[QueryMetrics]()\n    if (transid.meta.extraLogging) {\n      options.setPopulateQueryMetrics(true)\n      options.setEmitVerboseTracesInQuery(true)\n    }\n\n    def collectQueryMetrics(r: FeedResponse[Document]): Unit = {\n      collectMetrics(queryToken, r.getRequestCharge)\n      queryMetrics.appendAll(r.getQueryMetrics.values().asScala)\n    }\n\n    val publisher =\n      RxReactiveStreams.toPublisher(client.queryDocuments(collection.getSelfLink, querySpec, options))\n    val f = Source\n      .fromPublisher(publisher)\n      .wireTap(collectQueryMetrics(_))\n      .mapConcat(asVector)\n      .drop(skip)\n      .map(queryResultToWhiskJsonDoc)\n      .map(js =>\n        documentHandler\n          .transformViewResult(ddoc, viewName, startKey, endKey, realIncludeDocs, js, CosmosDBArtifactStore.this))\n      .mapAsync(1)(identity)\n      .mapConcat(identity)\n      .runWith(Sink.seq)\n      .map(_.toList)\n      .map(l => if (limit > 0) l.take(limit) else l)\n\n    val g = f.andThen {\n      case Success(queryResult) =>\n        if (queryMetrics.nonEmpty) {\n          val combinedMetrics = QueryMetrics.ZERO.add(queryMetrics.toSeq: _*)\n          logging.debug(\n            this,\n            s\"[QueryMetricsEnabled] Collection [$collName] - Query [${querySpec.getQueryText}].\\nQueryMetrics\\n[$combinedMetrics]\")\n        }\n        val stats = viewMapper.recordQueryStats(ddoc, viewName, descending, querySpec.getParameters, queryResult)\n        val statsToLog = stats.map(s => \" \" + s).getOrElse(\"\")\n        transid.finished(\n          this,\n          start,\n          s\"[QUERY] '$collName' completed: matched ${queryResult.size}$statsToLog\",\n          InfoLevel)\n    }\n    reportFailure(g, start, failure => s\"[QUERY] '$collName' internal error, failure: '${failure.getMessage}'\")\n  }\n\n  override protected[core] def count(table: String,\n                                     startKey: List[Any],\n                                     endKey: List[Any],\n                                     skip: Int,\n                                     stale: StaleParameter)(implicit transid: TransactionId): Future[Long] = {\n    require(skip >= 0, \"skip should be non negative\")\n    val Array(ddoc, viewName) = table.split(\"/\")\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s\"[COUNT] '$collName' searching '$table\")\n    val querySpec = viewMapper.prepareCountQuery(ddoc, viewName, startKey, endKey)\n\n    //For aggregates the value is in _aggregates fields\n    val f = client\n      .queryDocuments(collection.getSelfLink, querySpec, newFeedOptions())\n      .head()\n      .map { r =>\n        val count = r.getResults.asScala.head.getLong(aggregate).longValue\n        transid.finished(this, start, s\"[COUNT] '$collName' completed: count $count\")\n        collectMetrics(countToken, r.getRequestCharge)\n        if (count > skip) count - skip else 0L\n      }\n\n    reportFailure(f, start, failure => s\"[COUNT] '$collName' internal error, failure: '${failure.getMessage}'\")\n  }\n\n  override protected[database] def putAndAttach[A <: DocumentAbstraction](\n    doc: A,\n    update: (A, Attached) => A,\n    contentType: ContentType,\n    docStream: Source[ByteString, _],\n    oldAttachment: Option[Attached])(implicit transid: TransactionId): Future[(DocInfo, Attached)] = {\n    attachmentStore match {\n      case Some(as) =>\n        attachToExternalStore(doc, update, contentType, docStream, oldAttachment, as)\n      case None =>\n        Future.failed(new IllegalArgumentException(\n          s\" '$cosmosScheme' is now not supported. You must configure an external AttachmentStore for storing attachments\"))\n    }\n  }\n\n  override protected[core] def readAttachment[T](doc: DocInfo, attached: Attached, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T] = {\n    val name = attached.attachmentName\n    val attachmentUri = Uri(name)\n    attachmentUri.scheme match {\n      case AttachmentSupport.MemScheme =>\n        memorySource(attachmentUri).runWith(sink)\n      case s if s == cosmosScheme || attachmentUri.isRelative =>\n        //relative case is for compatibility with earlier naming approach where attachment name would be like 'jarfile'\n        //Compared to current approach of '<scheme>:<name>'\n        Future.failed(new IllegalArgumentException(\n          s\" '$cosmosScheme' is now not supported. You must configure an external AttachmentStore for storing attachments\"))\n      case s if attachmentStore.isDefined && attachmentStore.get.scheme == s =>\n        attachmentStore.get.readAttachment(doc.id, attachmentUri.path.toString, sink)\n      case _ =>\n        throw new IllegalArgumentException(s\"Unknown attachment scheme in attachment uri $attachmentUri\")\n    }\n  }\n\n  override protected[core] def deleteAttachments[T](doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] =\n    attachmentStore\n      .map(as => as.deleteAttachments(doc.id))\n      .getOrElse(Future.successful(true)) // For CosmosDB it is expected that the entire document is deleted.\n\n  override def shutdown(): Unit = {\n    //Its async so a chance exist for next scheduled job to still trigger\n    usageMetricRecorder.foreach(system.stop)\n    attachmentStore.foreach(_.shutdown())\n    clientRef.close()\n  }\n\n  def getResourceUsage(): Future[Option[CollectionResourceUsage]] = {\n    val opts = new RequestOptions\n    opts.setPopulateQuotaInfo(true)\n    client\n      .readCollection(collection.getSelfLink, opts)\n      .head()\n      .map(rr => CollectionResourceUsage(rr.getResponseHeaders.asScala.toMap))\n  }\n\n  private def recordResourceUsage() = {\n    getResourceUsage().map { o =>\n      o.foreach { u =>\n        u.documentsCount.foreach(documentCountToken.gauge.update(_))\n        u.documentsSize.foreach(ds => documentsSizeToken.gauge.update(ds.toKB))\n        u.indexSize.foreach(is => indexSizeToken.gauge.update(is.toKB))\n        logging.info(this, s\"Collection usage stats for [$collName] are ${u.asString}\")\n        u.indexingProgress.foreach { i =>\n          if (i < 100) logging.info(this, s\"Indexing for collection [$collName] is at $i%\")\n        }\n      }\n      o\n    }\n  }\n\n  private def isNotFound[A <: DocumentAbstraction](e: DocumentClientException) =\n    e.getStatusCode == StatusCodes.NotFound.intValue\n\n  private def isConflict(e: DocumentClientException) = {\n    e.getStatusCode == StatusCodes.Conflict.intValue || e.getStatusCode == StatusCodes.PreconditionFailed.intValue\n  }\n\n  private def toCosmosDoc(json: JsObject): (Document, Int) = {\n    val computedJs = documentHandler.computedFields(json)\n    val computedOpt = if (computedJs.fields.nonEmpty) Some(computedJs) else None\n    val fieldsToAdd =\n      Seq(\n        (cid, Some(JsString(escapeId(json.fields(_id).convertTo[String])))),\n        (etag, json.fields.get(_rev)),\n        (computed, computedOpt),\n        (clusterId, clusterIdValue))\n    val fieldsToRemove = Seq(_id, _rev)\n    val mapped = transform(json, fieldsToAdd, fieldsToRemove)\n    val jsonString = mapped.compactPrint\n    val doc = new Document(jsonString)\n    doc.set(selfLink, createSelfLink(doc.getId))\n    doc.setTimeToLive(null) //Disable any TTL if in effect for earlier revision\n    (doc, jsonString.length)\n  }\n\n  private def queryResultToWhiskJsonDoc(doc: Document): JsObject = {\n    val docJson = doc.toJson.parseJson.asJsObject\n    //If includeDocs is true then document json is to be used\n    val js = if (doc.has(alias)) docJson.fields(alias).asJsObject else docJson\n    val id = js.fields(cid).convertTo[String]\n    toWhiskJsonDoc(js, id, None)\n  }\n\n  private def getResultToWhiskJsonDoc(doc: Document): (JsObject, Int) = {\n    checkDoc(doc)\n    val jsString = doc.toJson\n    val js = jsString.parseJson.asJsObject\n    val whiskDoc = toWhiskJsonDoc(js, doc.getId, Some(JsString(doc.getETag)))\n    (whiskDoc, jsString.length)\n  }\n\n  private def toDocInfo[T <: Resource](doc: T) = {\n    checkDoc(doc)\n    DocInfo(DocId(unescapeId(doc.getId)), DocRevision(doc.getETag))\n  }\n\n  private def selfLinkOf(id: DocId) = createSelfLink(escapeId(id.id))\n\n  private def createSelfLink(id: String) = s\"dbs/${database.getId}/colls/${collection.getId}/docs/$id\"\n\n  private def matchRevOption(info: DocInfo): RequestOptions = matchRevOption(escapeId(info.id.id), info.rev.rev)\n\n  private def matchRevOption(id: String, etag: String): RequestOptions = {\n    val options = newRequestOption(id)\n    val condition = new AccessCondition\n    condition.setCondition(etag)\n    options.setAccessCondition(condition)\n    options\n  }\n\n  //Using DummyImplicit to allow overloading work with type erasure of DocId AnyVal\n  private def newRequestOption(id: DocId)(implicit i: DummyImplicit): RequestOptions = newRequestOption(escapeId(id.id))\n\n  private def newRequestOption(id: String) = {\n    val options = new RequestOptions\n    options.setPartitionKey(new PartitionKey(id))\n    options\n  }\n\n  private def newFeedOptions() = {\n    val options = new FeedOptions()\n    options.setEnableCrossPartitionQuery(true)\n    options\n  }\n\n  private def checkDoc[T <: Resource](doc: T): Unit = {\n    require(doc.getId != null, s\"$doc does not have id field set\")\n    require(doc.getETag != null, s\"$doc does not have etag field set\")\n  }\n\n  private def collectMetrics(token: LogMarkerToken, charge: Double): Unit = {\n    MetricEmitter.emitCounterMetric(token, Math.round(charge))\n  }\n\n  private def createToken(action: String, read: Boolean = true): LogMarkerToken = {\n    val mode = if (read) \"read\" else \"write\"\n    val tags = Map(\"action\" -> action, \"mode\" -> mode, \"collection\" -> collName)\n    if (TransactionId.metricsKamonTags) LogMarkerToken(\"cosmosdb\", \"ru\", \"used\", tags = tags)(MeasurementUnit.none)\n    else LogMarkerToken(\"cosmosdb\", \"ru\", collName, Some(action))(MeasurementUnit.none)\n  }\n\n  private def createUsageToken(name: String, unit: MeasurementUnit = MeasurementUnit.none): LogMarkerToken = {\n    val tags = Map(\"collection\" -> collName)\n    if (TransactionId.metricsKamonTags) LogMarkerToken(\"cosmosdb\", name, \"used\", tags = tags)(unit)\n    else LogMarkerToken(\"cosmosdb\", name, collName)(unit)\n  }\n\n  private def createDocSizeToken(): LogMarkerToken = {\n    val unit = MeasurementUnit.information.bytes\n    val name = \"doc\"\n    val tags = Map(\"collection\" -> collName)\n    if (TransactionId.metricsKamonTags) LogMarkerToken(\"cosmosdb\", name, \"size\", tags = tags)(unit)\n    else LogMarkerToken(\"cosmosdb\", name, collName)(unit)\n  }\n\n  private def isSoftDeleted(doc: Document) = doc.getBoolean(deleted) == true\n\n  private def isSoftDeleted(js: JsObject) = js.fields.get(deleted).contains(JsTrue)\n\n  private def isNewDocument(doc: Document) = doc.getETag == null\n\n  private def extraLogs(r: ResourceResponse[_])(implicit tid: TransactionId): String = {\n    if (tid.meta.extraLogging) {\n      \" \" + r.getRequestDiagnosticsString\n    } else \"\"\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBArtifactStoreProvider.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport java.io.Closeable\n\nimport org.apache.pekko.actor.ActorSystem\nimport com.microsoft.azure.cosmosdb.rx.AsyncDocumentClient\nimport com.typesafe.config.ConfigFactory\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.{DocumentReader, WhiskActivation, WhiskAuth, WhiskEntity}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json.RootJsonFormat\n\nimport scala.reflect.ClassTag\n\ncase class ClientHolder(client: AsyncDocumentClient) extends Closeable {\n  override def close(): Unit = client.close()\n}\n\nobject CosmosDBArtifactStoreProvider extends ArtifactStoreProvider {\n  type DocumentClientRef = ReferenceCounted[ClientHolder]#CountedReference\n  private val clients = collection.mutable.Map[CosmosDBConfig, ReferenceCounted[ClientHolder]]()\n\n  RetryMetricsCollector.registerIfEnabled()\n\n  override def makeStore[D <: DocumentSerializer: ClassTag](useBatching: Boolean)(\n    implicit jsonFormat: RootJsonFormat[D],\n    docReader: DocumentReader,\n    actorSystem: ActorSystem,\n    logging: Logging): ArtifactStore[D] = {\n    val tag = implicitly[ClassTag[D]]\n    val config = CosmosDBConfig(ConfigFactory.load(), tag.runtimeClass.getSimpleName)\n    makeStoreForClient(config, getOrCreateReference(config), getAttachmentStore())\n  }\n\n  def makeArtifactStore[D <: DocumentSerializer: ClassTag](config: CosmosDBConfig,\n                                                           attachmentStore: Option[AttachmentStore])(\n    implicit jsonFormat: RootJsonFormat[D],\n    docReader: DocumentReader,\n    actorSystem: ActorSystem,\n    logging: Logging): CosmosDBArtifactStore[D] = {\n\n    makeStoreForClient(config, createReference(config).reference(), attachmentStore)\n  }\n\n  private def makeStoreForClient[D <: DocumentSerializer: ClassTag](config: CosmosDBConfig,\n                                                                    clientRef: DocumentClientRef,\n                                                                    attachmentStore: Option[AttachmentStore])(\n    implicit jsonFormat: RootJsonFormat[D],\n    docReader: DocumentReader,\n    actorSystem: ActorSystem,\n    logging: Logging): CosmosDBArtifactStore[D] = {\n\n    val classTag = implicitly[ClassTag[D]]\n    val (dbName, handler, viewMapper) = handlerAndMapper(classTag)\n\n    new CosmosDBArtifactStore(\n      dbName,\n      config,\n      clientRef,\n      handler,\n      viewMapper,\n      loadConfigOrThrow[InliningConfig](ConfigKeys.db),\n      attachmentStore)\n  }\n\n  private def handlerAndMapper[D](entityType: ClassTag[D])(\n    implicit actorSystem: ActorSystem,\n    logging: Logging): (String, DocumentHandler, CosmosDBViewMapper) = {\n    val entityClass = entityType.runtimeClass\n    if (entityClass == classOf[WhiskEntity]) (\"whisks\", WhisksHandler, WhisksViewMapper)\n    else if (entityClass == classOf[WhiskActivation]) (\"activations\", ActivationHandler, ActivationViewMapper)\n    else if (entityClass == classOf[WhiskAuth]) (\"subjects\", SubjectHandler, SubjectViewMapper)\n    else throw new IllegalArgumentException(s\"Unsupported entity type $entityType\")\n  }\n\n  /*\n   * This method ensures that all store instances share same client instance and thus the underlying connection pool.\n   * Synchronization is required to ensure concurrent init of various store instances share same ref instance\n   */\n  private def getOrCreateReference[D <: DocumentSerializer: ClassTag](config: CosmosDBConfig) = synchronized {\n    val clientRef = clients.getOrElseUpdate(config, createReference(config))\n    if (clientRef.isClosed) {\n      val newRef = createReference(config)\n      clients.put(config, newRef)\n      newRef.reference()\n    } else {\n      clientRef.reference()\n    }\n  }\n\n  private def createReference[D <: DocumentSerializer: ClassTag](config: CosmosDBConfig) = {\n    val clazz = implicitly[ClassTag[D]].runtimeClass\n    if (clazz != classOf[WhiskActivation]) {\n      require(config.timeToLive.isEmpty, s\"'timeToLive' should not  be specified for ${clazz.getSimpleName}\")\n    }\n    new ReferenceCounted[ClientHolder](ClientHolder(config.createClient()))\n  }\n\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBConfig.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\nimport com.microsoft.azure.cosmosdb.rx.AsyncDocumentClient\nimport com.microsoft.azure.cosmosdb.{\n  ConnectionMode,\n  ConsistencyLevel,\n  ConnectionPolicy => JConnectionPolicy,\n  RetryOptions => JRetryOptions\n}\nimport com.typesafe.config.Config\nimport com.typesafe.config.ConfigUtil.joinPath\nimport org.apache.openwhisk.core.ConfigKeys\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.duration._\n\ncase class CosmosDBConfig(endpoint: String,\n                          key: String,\n                          db: String,\n                          throughput: Int,\n                          consistencyLevel: ConsistencyLevel,\n                          connectionPolicy: ConnectionPolicy,\n                          timeToLive: Option[Duration],\n                          clusterId: Option[String],\n                          softDeleteTTL: Option[FiniteDuration],\n                          recordUsageFrequency: Option[FiniteDuration]) {\n\n  def createClient(): AsyncDocumentClient = {\n    new AsyncDocumentClient.Builder()\n      .withServiceEndpoint(endpoint)\n      .withMasterKeyOrResourceToken(key)\n      .withConsistencyLevel(consistencyLevel)\n      .withConnectionPolicy(connectionPolicy.asJava)\n      .build()\n  }\n}\n\ncase class ConnectionPolicy(maxPoolSize: Int,\n                            preferredLocations: Seq[String],\n                            usingMultipleWriteLocations: Boolean,\n                            retryOptions: RetryOptions,\n                            connectionMode: ConnectionMode) {\n  def asJava: JConnectionPolicy = {\n    val p = new JConnectionPolicy\n    p.setMaxPoolSize(maxPoolSize)\n    p.setUsingMultipleWriteLocations(usingMultipleWriteLocations)\n    p.setPreferredLocations(preferredLocations.asJava)\n    p.setRetryOptions(retryOptions.asJava)\n    p.setConnectionMode(connectionMode)\n    p\n  }\n}\n\ncase class RetryOptions(maxRetryAttemptsOnThrottledRequests: Int, maxRetryWaitTime: Duration) {\n  def asJava: JRetryOptions = {\n    val o = new JRetryOptions\n    o.setMaxRetryAttemptsOnThrottledRequests(maxRetryAttemptsOnThrottledRequests)\n    o.setMaxRetryWaitTimeInSeconds(maxRetryWaitTime.toSeconds.toInt)\n    o\n  }\n}\n\nobject CosmosDBConfig {\n  val collections = \"collections\"\n\n  def apply(globalConfig: Config, entityTypeName: String): CosmosDBConfig = {\n    val config = globalConfig.getConfig(ConfigKeys.cosmosdb)\n    val specificConfigPath = joinPath(collections, entityTypeName)\n\n    //Merge config specific to entity with common config\n    val entityConfig = if (config.hasPath(specificConfigPath)) {\n      config.getConfig(specificConfigPath).withFallback(config)\n    } else {\n      config\n    }\n    loadConfigOrThrow[CosmosDBConfig](entityConfig)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBSupport.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport com.microsoft.azure.cosmosdb.{\n  Database,\n  DocumentCollection,\n  FeedResponse,\n  RequestOptions,\n  Resource,\n  SqlParameter,\n  SqlParameterCollection,\n  SqlQuerySpec\n}\nimport com.microsoft.azure.cosmosdb.rx.AsyncDocumentClient\nimport org.apache.openwhisk.common.Logging\n\nimport scala.collection.JavaConverters._\n\nprivate[cosmosdb] trait CosmosDBSupport extends RxObservableImplicits with CosmosDBUtil {\n  protected def config: CosmosDBConfig\n  protected def collName: String\n  protected def client: AsyncDocumentClient\n  protected def viewMapper: CosmosDBViewMapper\n\n  def initialize()(implicit logging: Logging): (Database, DocumentCollection) = {\n    val db = getOrCreateDatabase()\n    (db, getOrCreateCollection(db))\n  }\n\n  private def getOrCreateDatabase()(implicit logging: Logging): Database = {\n    client\n      .queryDatabases(querySpec(config.db), null)\n      .blockingOnlyResult()\n      .getOrElse {\n        client.createDatabase(newDatabase, null).blockingResult()\n      }\n  }\n\n  private def getOrCreateCollection(database: Database)(implicit logging: Logging) = {\n    client\n      .queryCollections(database.getSelfLink, querySpec(collName), null)\n      .blockingOnlyResult()\n      .map { coll =>\n        val expectedIndexingPolicy = viewMapper.indexingPolicy\n        val existingIndexingPolicy = IndexingPolicy(coll.getIndexingPolicy)\n        if (!IndexingPolicy.isSame(expectedIndexingPolicy, existingIndexingPolicy)) {\n          logging.warn(\n            this,\n            s\"Indexing policy for collection [$collName] found to be different.\" +\n              s\"\\nExpected - ${expectedIndexingPolicy.asJava().toJson}\" +\n              s\"\\nExisting - ${existingIndexingPolicy.asJava().toJson}\")\n        }\n        coll\n      }\n      .getOrElse {\n        client.createCollection(database.getSelfLink, newDatabaseCollection, dbOptions).blockingResult()\n      }\n  }\n\n  private def newDatabaseCollection = {\n    val defn = new DocumentCollection\n    defn.setId(collName)\n    defn.setIndexingPolicy(viewMapper.indexingPolicy.asJava())\n    defn.setPartitionKey(viewMapper.partitionKeyDefn)\n    val ttl = config.timeToLive.map(_.toSeconds.toInt).getOrElse(-1)\n    defn.setDefaultTimeToLive(ttl)\n    defn\n  }\n\n  private def dbOptions = {\n    val opts = new RequestOptions\n    opts.setOfferThroughput(config.throughput)\n    opts\n  }\n\n  private def newDatabase = {\n    val databaseDefinition = new Database\n    databaseDefinition.setId(config.db)\n    databaseDefinition\n  }\n\n  /**\n   * Prepares a query for fetching any resource by id\n   */\n  protected def querySpec(id: String) =\n    new SqlQuerySpec(\"SELECT * FROM root r WHERE r.id=@id\", new SqlParameterCollection(new SqlParameter(\"@id\", id)))\n\n  protected def asVector[T <: Resource](r: FeedResponse[T]): Vector[T] = r.getResults.asScala.toVector\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBUtil.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport com.microsoft.azure.cosmosdb.internal.Constants.Properties.{AGGREGATE, E_TAG, ID, SELF_LINK}\nimport org.apache.openwhisk.core.database.cosmosdb.CosmosDBConstants._\nimport org.apache.openwhisk.core.database.StoreUtils.transform\nimport spray.json.{JsObject, JsString}\n\nprivate[cosmosdb] object CosmosDBConstants {\n\n  /**\n   * Stores the computed properties required for view related queries\n   */\n  val computed: String = \"_c\"\n\n  val alias: String = \"view\"\n\n  val cid: String = ID\n\n  val etag: String = E_TAG\n\n  val aggregate: String = AGGREGATE\n\n  val selfLink: String = SELF_LINK\n\n  /**\n   * Records the clusterId which performed changed in any document. This can vary over\n   * lifetime of a document as different clusters may change the same document at different times\n   */\n  val clusterId: String = \"_clusterId\"\n\n  /**\n   * Property indicating that document has been marked as deleted with ttl\n   */\n  val deleted: String = \"_deleted\"\n}\n\nprivate[cosmosdb] trait CosmosDBUtil {\n\n  /**\n   * Name of `id` field as used in WhiskDocument\n   */\n  val _id: String = \"_id\"\n\n  /**\n   * Name of revision field as used in WhiskDocument\n   */\n  val _rev: String = \"_rev\"\n\n  /**\n   * Prepares the json like select clause\n   * {{{\n   *   Seq(\"a\", \"b\", \"c.d.e\") =>\n   *   { \"a\" : r['a'], \"b\" : r['b'], \"c\" : { \"d\" : { \"e\" : r['c']['d']['e']}}, \"id\" : r['id']} AS view\n   * }}}\n   * Here it uses {{{r['keyName']}}} notation to avoid issues around using reserved words as field name\n   */\n  def prepareFieldClause(fields: Set[String]): String = {\n    val json = (fields + cid)\n      .map { field =>\n        val split = field.split('.')\n\n        val selector = \"r\" + split.mkString(\"['\", \"']['\", \"']\")\n        val prefix = split.map(k => s\"\"\"\"$k\":\"\"\").mkString(\"{\")\n        val suffix = split.drop(1).map(_ => \"}\").mkString\n\n        prefix + selector + suffix\n      }\n      .mkString(\"{\", \",\", \"}\")\n    s\"$json AS $alias\"\n  }\n\n  /**\n   * CosmosDB id considers '/', '\\' , '?' and '#' as invalid. EntityNames can include '/' so\n   * that need to be escaped. For that we use '|' as the replacement char\n   */\n  def escapeId(id: String): String = {\n    require(!id.contains(\"|\"), s\"Id [$id] should not contain '|'\")\n    id.replace(\"/\", \"|\")\n  }\n\n  def unescapeId(id: String): String = {\n    require(!id.contains(\"/\"), s\"Escaped Id [$id] should not contain '/'\")\n    id.replace(\"|\", \"/\")\n  }\n\n  def toWhiskJsonDoc(js: JsObject, id: String, etag: Option[JsString]): JsObject = {\n    val fieldsToAdd = Seq((_id, Some(JsString(unescapeId(id)))), (_rev, etag))\n    transform(stripInternalFields(js), fieldsToAdd, Seq.empty)\n  }\n\n  private def stripInternalFields(js: JsObject) = {\n    //Strip out all field name starting with '_' which are considered as db specific internal fields\n    JsObject(js.fields.filter { case (k, _) => !k.startsWith(\"_\") && k != cid })\n  }\n\n}\n\nprivate[cosmosdb] object CosmosDBUtil extends CosmosDBUtil\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBViewMapper.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport java.util.Collections\n\nimport com.microsoft.azure.cosmosdb.DataType.{Number, String}\nimport com.microsoft.azure.cosmosdb.IndexKind.Range\nimport com.microsoft.azure.cosmosdb.{PartitionKeyDefinition, SqlParameter, SqlParameterCollection, SqlQuerySpec}\nimport kamon.metric.MeasurementUnit\nimport org.apache.openwhisk.common.{LogMarkerToken, TransactionId, WhiskInstants}\nimport org.apache.openwhisk.core.database.ActivationHandler.NS_PATH\nimport org.apache.openwhisk.core.database.WhisksHandler.ROOT_NS\nimport org.apache.openwhisk.core.database.cosmosdb.CosmosDBConstants.{alias, computed, deleted}\nimport org.apache.openwhisk.core.database.{\n  ActivationHandler,\n  DocumentHandler,\n  SubjectHandler,\n  UnsupportedQueryKeys,\n  UnsupportedView,\n  WhisksHandler\n}\nimport org.apache.openwhisk.core.entity.WhiskQueries.TOP\nimport org.apache.openwhisk.utils.JsHelpers\nimport spray.json.{JsNumber, JsObject}\n\nimport scala.collection.JavaConverters._\n\nprivate[cosmosdb] trait CosmosDBViewMapper {\n  protected val NOTHING = \"\"\n  protected val ALL_FIELDS = \"*\"\n  protected val notDeleted = s\"(NOT(IS_DEFINED(r.$deleted)) OR r.$deleted = false)\"\n  protected def handler: DocumentHandler\n\n  def prepareQuery(ddoc: String,\n                   viewName: String,\n                   startKey: List[Any],\n                   endKey: List[Any],\n                   limit: Int,\n                   includeDocs: Boolean,\n                   descending: Boolean): SqlQuerySpec\n\n  def prepareCountQuery(ddoc: String, viewName: String, startKey: List[Any], endKey: List[Any]): SqlQuerySpec\n\n  def indexingPolicy: IndexingPolicy\n\n  val partitionKeyDefn: PartitionKeyDefinition = {\n    val defn = new PartitionKeyDefinition\n    defn.setPaths(Collections.singletonList(\"/id\"))\n    defn\n  }\n\n  protected def checkKeys(startKey: List[Any], endKey: List[Any]): Unit = {\n    require(startKey.nonEmpty)\n    require(endKey.nonEmpty)\n    require(startKey.head == endKey.head, s\"First key should be same => ($startKey) - ($endKey)\")\n  }\n\n  protected def prepareSpec(query: String, params: List[(String, Any)]): SqlQuerySpec = {\n    val paramColl = new SqlParameterCollection\n    params.foreach { case (k, v) => paramColl.add(new SqlParameter(k, v)) }\n\n    new SqlQuerySpec(query, paramColl)\n  }\n\n  /**\n   *  Records query related stats based on result returned and arguments passed\n   *\n   * @return an optional string representation of stats for logging purpose\n   */\n  def recordQueryStats(ddoc: String,\n                       viewName: String,\n                       descending: Boolean,\n                       queryParams: SqlParameterCollection,\n                       result: List[JsObject]): Option[String] = None\n}\n\nprivate[cosmosdb] abstract class SimpleMapper extends CosmosDBViewMapper {\n\n  def prepareQuery(ddoc: String,\n                   viewName: String,\n                   startKey: List[Any],\n                   endKey: List[Any],\n                   limit: Int,\n                   includeDocs: Boolean,\n                   descending: Boolean): SqlQuerySpec = {\n    checkKeys(startKey, endKey)\n\n    val selectClause = select(ddoc, viewName, limit, includeDocs)\n    val whereClause = where(ddoc, viewName, startKey, endKey)\n    val orderField = orderByField(ddoc, viewName)\n    val order = if (descending) \"DESC\" else NOTHING\n\n    val query = s\"SELECT $selectClause FROM root r WHERE $notDeleted AND ${whereClause._1} ORDER BY $orderField $order\"\n\n    prepareSpec(query, whereClause._2)\n  }\n\n  def prepareCountQuery(ddoc: String, viewName: String, startKey: List[Any], endKey: List[Any]): SqlQuerySpec = {\n    checkKeys(startKey, endKey)\n\n    val whereClause = where(ddoc, viewName, startKey, endKey)\n    val query = s\"SELECT TOP 1 VALUE COUNT(r) FROM root r WHERE ${whereClause._1}\"\n\n    prepareSpec(query, whereClause._2)\n  }\n\n  private def select(ddoc: String, viewName: String, limit: Int, includeDocs: Boolean): String = {\n    val fieldClause = if (includeDocs) ALL_FIELDS else prepareFieldClause(ddoc, viewName)\n    s\"${top(limit)} $fieldClause\"\n  }\n\n  private def top(limit: Int): String = {\n    if (limit > 0) s\"TOP $limit\" else NOTHING\n  }\n\n  private def prepareFieldClause(ddoc: String, viewName: String) =\n    CosmosDBUtil.prepareFieldClause(handler.fieldsRequiredForView(ddoc, viewName))\n\n  protected def where(ddoc: String,\n                      viewName: String,\n                      startKey: List[Any],\n                      endKey: List[Any]): (String, List[(String, Any)])\n\n  protected def orderByField(ddoc: String, viewName: String): String\n}\n\nprivate[cosmosdb] object WhisksViewMapper extends SimpleMapper {\n  private val NS = \"namespace\"\n  private val ROOT_NS_C = s\"$computed.$ROOT_NS\"\n  private val TYPE = \"entityType\"\n  private val UPDATED = \"updated\"\n  private val PUBLISH = \"publish\"\n  private val BINDING = \"binding\"\n\n  val handler = WhisksHandler\n\n  override def indexingPolicy: IndexingPolicy =\n    IndexingPolicy(\n      includedPaths = Set(\n        IncludedPath(s\"/$TYPE/?\", Index(Range, String, -1)),\n        IncludedPath(s\"/$NS/?\", Index(Range, String, -1)),\n        IncludedPath(s\"/$computed/$ROOT_NS/?\", Index(Range, String, -1)),\n        IncludedPath(s\"/$UPDATED/?\", Index(Range, Number, -1))))\n\n  override protected def where(ddoc: String,\n                               view: String,\n                               startKey: List[Any],\n                               endKey: List[Any]): (String, List[(String, Any)]) = {\n    val entityType = WhisksHandler.getEntityTypeForDesignDoc(ddoc, view)\n    val namespace = startKey.head\n\n    val (vc, vcParams) =\n      viewConditions(ddoc, view).map(q => (s\"${q._1} AND\", q._2)).getOrElse((NOTHING, Nil))\n\n    val params = (\"@entityType\", entityType) :: (\"@namespace\", namespace) :: vcParams\n    val baseCondition = s\"$vc r.$TYPE = @entityType AND (r.$NS = @namespace OR r.$ROOT_NS_C = @namespace)\"\n\n    (startKey, endKey) match {\n      case (_ :: Nil, _ :: `TOP` :: Nil) =>\n        (baseCondition, params)\n\n      case (_ :: (since: Number) :: Nil, _ :: `TOP` :: `TOP` :: Nil) =>\n        (s\"$baseCondition AND r.$UPDATED >= @since\", (\"@since\", since) :: params)\n\n      case (_ :: (since: Number) :: Nil, _ :: (upto: Number) :: `TOP` :: Nil) =>\n        (s\"$baseCondition AND (r.$UPDATED BETWEEN @since AND @upto)\", (\"@upto\", upto) :: (\"@since\", since) :: params)\n\n      case _ => throw UnsupportedQueryKeys(s\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n  }\n\n  private def viewConditions(ddoc: String, view: String): Option[(String, List[(String, Any)])] = {\n    view match {\n      case \"packages-public\" if ddoc.startsWith(\"whisks\") =>\n        Some(s\"r.$PUBLISH = true AND (NOT IS_OBJECT(r.$BINDING) OR r.$BINDING = {})\", Nil)\n      case _ => None\n    }\n  }\n\n  override protected def orderByField(ddoc: String, view: String): String = view match {\n    case \"actions\" | \"rules\" | \"triggers\" | \"packages\" | \"packages-public\" if ddoc.startsWith(\"whisks\") =>\n      s\"r.$UPDATED\"\n    case _ => throw UnsupportedView(s\"$ddoc/$view\")\n  }\n\n}\nprivate[cosmosdb] object ActivationViewMapper extends SimpleMapper with WhiskInstants {\n  import CosmosDBViewMapper._\n  private val NS = \"namespace\"\n  private val NS_WITH_PATH = s\"$computed.$NS_PATH\"\n  private val START = \"start\"\n\n  val handler = ActivationHandler\n\n  override def indexingPolicy: IndexingPolicy =\n    IndexingPolicy(\n      includedPaths = Set(\n        IncludedPath(s\"/$NS/?\", Index(Range, String, -1)),\n        IncludedPath(s\"/$computed/$NS_PATH/?\", Index(Range, String, -1)),\n        IncludedPath(s\"/$START/?\", Index(Range, Number, -1)),\n        IncludedPath(s\"/$deleted/?\", Index(Range, Number, -1))))\n\n  override protected def where(ddoc: String,\n                               view: String,\n                               startKey: List[Any],\n                               endKey: List[Any]): (String, List[(String, Any)]) = {\n    val nsValue = startKey.head.asInstanceOf[String]\n    view match {\n      //whisks-filters ddoc uses namespace + invoking action path as first key\n      case \"activations\" if ddoc.startsWith(\"whisks-filters\") =>\n        filterActivation(NS_WITH_PATH, nsValue, startKey, endKey)\n      //whisks ddoc uses namespace as first key\n      case \"activations\" if ddoc.startsWith(\"whisks\") => filterActivation(NS, nsValue, startKey, endKey)\n      case _                                          => throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  private def filterActivation(nsKey: String,\n                               nsValue: String,\n                               startKey: List[Any],\n                               endKey: List[Any]): (String, List[(String, Any)]) = {\n    val params = (\"@nsvalue\", nsValue) :: Nil\n    val filter = (startKey, endKey) match {\n      case (_ :: Nil, _ :: `TOP` :: Nil) =>\n        (s\"r.$nsKey = @nsvalue\", params)\n      case (_ :: (since: Number) :: Nil, _ :: `TOP` :: `TOP` :: Nil) =>\n        (s\"r.$nsKey = @nsvalue AND r.$START >= @start\", (\"@start\", since) :: params)\n      case (_ :: (since: Number) :: Nil, _ :: (upto: Number) :: `TOP` :: Nil) =>\n        (s\"r.$nsKey = @nsvalue AND (r.$START BETWEEN @start AND @upto)\", (\"@upto\", upto) :: (\"@start\", since) :: params)\n      case _ => throw UnsupportedQueryKeys(s\"$startKey, $endKey\")\n    }\n    filter\n  }\n\n  override protected def orderByField(ddoc: String, view: String): String = view match {\n    case \"activations\" if ddoc.startsWith(\"whisks\") => s\"r.$START\"\n    case _                                          => throw UnsupportedView(s\"$ddoc/$view\")\n  }\n\n  private val resultDeltaToken = createStatsToken(\"activations\", \"resultDelta\", \"activations\")\n  private val sinceDeltaToken = createStatsToken(\"activations\", \"sinceDelta\", \"activations\")\n\n  override def recordQueryStats(ddoc: String,\n                                viewName: String,\n                                descending: Boolean,\n                                queryParams: SqlParameterCollection,\n                                result: List[JsObject]): Option[String] = {\n    val stat = if (viewName == \"activations\" && descending) {\n      // Collect stats for the delta between\n      // 1. now and start time of last activation\n      // 2. now and start time as specific in query for `since` parameter\n      // These stats would help in determining how much old activations are being queried for list query (used in activation poll)\n      val uptoOpt = paramValue(queryParams, \"upto\", classOf[Number])\n      val startOpt = paramValue(queryParams, \"start\", classOf[Number])\n\n      // Result json has structure { id: \"\", \"key\": [], \"value\": {activation}}\n      // So fetch value of start via `value.start` path\n      val lastOpt = result.lastOption.flatMap(js => JsHelpers.getFieldPath(js, \"value\", \"start\"))\n\n      (uptoOpt, startOpt, lastOpt) match {\n        //Go for case which does not specify upto as that would be the case with poll based query\n        case (None, Some(startFromQuery), Some(JsNumber(start))) =>\n          val now = nowInMillis().toEpochMilli\n          val resultStartDelta = (now - start.longValue).max(0)\n          val queryStartDelta = (now - startFromQuery.longValue).max(0)\n          resultDeltaToken.histogram.record(resultStartDelta)\n          sinceDeltaToken.histogram.record(queryStartDelta)\n          Some(s\"resultDelta=$resultStartDelta, sinceDelta=$queryStartDelta\")\n        case _ => None\n      }\n    } else None\n    stat\n  }\n}\nprivate[cosmosdb] object SubjectViewMapper extends CosmosDBViewMapper {\n  private val UUID = \"uuid\"\n  private val KEY = \"key\"\n  private val NSS = \"namespaces\"\n  private val CONCURRENT_INVOCATIONS = \"concurrentInvocations\"\n  private val INVOCATIONS_PER_MIN = \"invocationsPerMinute\"\n  private val BLOCKED = \"blocked\"\n  private val SUBJECT = \"subject\"\n  private val NAME = \"name\"\n  private val notBlocked = s\"(NOT(IS_DEFINED(r.$BLOCKED)) OR r.$BLOCKED = false)\"\n\n  val handler = SubjectHandler\n\n  override def indexingPolicy: IndexingPolicy =\n    //Booleans are indexed by default\n    //Specifying less precision for key as match on uuid should be sufficient\n    //and keys are bigger\n    IndexingPolicy(\n      includedPaths = Set(\n        IncludedPath(s\"/$UUID/?\", Index(Range, String, -1)),\n        IncludedPath(s\"/$NSS/[]/$NAME/?\", Index(Range, String, -1)),\n        IncludedPath(s\"/$SUBJECT/?\", Index(Range, String, -1)),\n        IncludedPath(s\"/$NSS/[]/$UUID/?\", Index(Range, String, -1)),\n        IncludedPath(s\"/$CONCURRENT_INVOCATIONS/?\", Index(Range, Number, -1)),\n        IncludedPath(s\"/$INVOCATIONS_PER_MIN/?\", Index(Range, Number, -1))))\n\n  override def prepareQuery(ddoc: String,\n                            view: String,\n                            startKey: List[Any],\n                            endKey: List[Any],\n                            limit: Int,\n                            includeDocs: Boolean,\n                            descending: Boolean): SqlQuerySpec =\n    prepareQuery(ddoc, view, startKey, endKey, count = false)\n\n  override def prepareCountQuery(ddoc: String, view: String, startKey: List[Any], endKey: List[Any]): SqlQuerySpec =\n    prepareQuery(ddoc, view, startKey, endKey, count = true)\n\n  private def prepareQuery(ddoc: String,\n                           view: String,\n                           startKey: List[Any],\n                           endKey: List[Any],\n                           count: Boolean): SqlQuerySpec = {\n    require(startKey == endKey, s\"startKey: $startKey and endKey: $endKey must be same for $ddoc/$view\")\n    (ddoc, view) match {\n      case (s, \"identities\") if s.startsWith(\"subjects\") =>\n        queryForMatchingSubjectOrNamespace(ddoc, view, startKey, endKey, count)\n      case (\"namespaceThrottlings\", \"blockedNamespaces\") =>\n        queryForBlacklistedNamespace(count)\n      case _ =>\n        throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  private def queryForMatchingSubjectOrNamespace(ddoc: String,\n                                                 view: String,\n                                                 startKey: List[Any],\n                                                 endKey: List[Any],\n                                                 count: Boolean): SqlQuerySpec = {\n    val (where, params) = startKey match {\n      case (ns: String) :: Nil =>\n        (\n          s\"$notDeleted AND $notBlocked AND ((r.$SUBJECT = @name AND IS_DEFINED(r.$KEY)) OR n.$NAME = @name)\",\n          (\"@name\", ns) :: Nil)\n      case (uuid: String) :: (key: String) :: Nil =>\n        (\n          s\"$notDeleted AND $notBlocked AND ((r.$UUID = @uuid AND r.$KEY = @key) OR (n.$UUID = @uuid AND n.$KEY = @key))\",\n          (\"@uuid\", uuid) :: (\"@key\", key) :: Nil)\n      case _ => throw UnsupportedQueryKeys(s\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n    prepareSpec(s\"SELECT ${selectClause(count)} AS $alias FROM root r JOIN n in r.namespaces WHERE $where\", params)\n  }\n\n  private def queryForBlacklistedNamespace(count: Boolean): SqlQuerySpec =\n    prepareSpec(\n      s\"\"\"SELECT ${selectClause(count)} AS $alias\n                  FROM   root r\n                  WHERE  (r.$BLOCKED = true\n                          OR r.$CONCURRENT_INVOCATIONS = 0\n                          OR r.$INVOCATIONS_PER_MIN = 0) AND $notDeleted \"\"\",\n      Nil)\n\n  private def selectClause(count: Boolean) = if (count) \"TOP 1 VALUE COUNT(r)\" else \"r\"\n}\n\nobject CosmosDBViewMapper {\n\n  def paramValue[T](params: SqlParameterCollection, key: String, clazz: Class[T]): Option[T] = {\n    val name = \"@\" + key\n    params.iterator().asScala.find(_.getName == name).map(_.getValue(clazz).asInstanceOf[T])\n  }\n\n  def createStatsToken(viewName: String, statName: String, collName: String): LogMarkerToken = {\n    val unit = MeasurementUnit.time.milliseconds\n    val tags = Map(\"view\" -> viewName, \"collection\" -> collName)\n    if (TransactionId.metricsKamonTags) LogMarkerToken(\"cosmosdb\", \"query\", statName, tags = tags)(unit)\n    else LogMarkerToken(\"cosmosdb\", \"query\", collName, Some(statName))(unit)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/IndexingPolicy.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport com.microsoft.azure.cosmosdb.{\n  DataType,\n  HashIndex,\n  IndexKind,\n  RangeIndex,\n  ExcludedPath => JExcludedPath,\n  IncludedPath => JIncludedPath,\n  Index => JIndex,\n  IndexingPolicy => JIndexingPolicy\n}\n\nimport scala.collection.JavaConverters._\n\n/**\n * Scala based IndexingPolicy type which maps to java based IndexingPolicy. This is done for 2 reasons\n *\n *  - Simplify constructing policy instance\n *  - Enable custom comparison between existing policy and desired policy as policy instances\n *    obtained from CosmosDB have extra index type configured per included path. Hence the comparison\n *    needs to be customized\n *\n */\ncase class IndexingPolicy(includedPaths: Set[IncludedPath],\n                          excludedPaths: Set[ExcludedPath] = Set(ExcludedPath(\"/*\"))) {\n\n  def asJava(): JIndexingPolicy = {\n    val policy = new JIndexingPolicy()\n    policy.setIncludedPaths(includedPaths.map(_.asJava()).asJava)\n    policy.setExcludedPaths(excludedPaths.map(_.asJava()).asJava)\n    policy\n  }\n}\n\nobject IndexingPolicy {\n  def apply(policy: JIndexingPolicy): IndexingPolicy =\n    IndexingPolicy(\n      policy.getIncludedPaths.asScala.map(IncludedPath(_)).toSet,\n      policy.getExcludedPaths.asScala.map(ExcludedPath(_)).toSet)\n\n  /**\n   * IndexingPolicy fetched from CosmosDB contains extra entries. So need to check\n   * that at least what we expect is present\n   */\n  def isSame(expected: IndexingPolicy, current: IndexingPolicy): Boolean = {\n    epaths(expected.excludedPaths) == epaths(current.excludedPaths) &&\n    ipaths(expected.includedPaths) == ipaths(current.includedPaths)\n  }\n\n  private def ipaths(included: Set[IncludedPath]) = included.map(_.path)\n\n  //CosmosDB seems to add _etag by default in excluded path. So explicitly ignore that in comparison\n  private def epaths(excluded: Set[ExcludedPath]) = excluded.map(_.path).filterNot(_.contains(\"_etag\"))\n}\n\ncase class IncludedPath(path: String, indexes: Set[Index]) {\n  def asJava(): JIncludedPath = {\n    val includedPath = new JIncludedPath()\n    includedPath.setIndexes(indexes.map(_.asJava()).asJava)\n    includedPath.setPath(path)\n    includedPath\n  }\n}\n\nobject IncludedPath {\n  def apply(ip: JIncludedPath): IncludedPath = IncludedPath(ip.getPath, ip.getIndexes.asScala.map(Index(_)).toSet)\n\n  def apply(path: String, index: Index): IncludedPath = IncludedPath(path, Set(index))\n}\n\ncase class ExcludedPath(path: String) {\n  def asJava(): JExcludedPath = {\n    val excludedPath = new JExcludedPath()\n    excludedPath.setPath(path)\n    excludedPath\n  }\n}\n\nobject ExcludedPath {\n  def apply(ep: JExcludedPath): ExcludedPath = ExcludedPath(ep.getPath)\n}\n\ncase class Index(kind: IndexKind, dataType: DataType, precision: Int) {\n  def asJava(): JIndex = kind match {\n    case IndexKind.Hash  => JIndex.Hash(dataType, precision)\n    case IndexKind.Range => JIndex.Range(dataType, precision)\n    case _               => throw new RuntimeException(s\"Unsupported kind $kind\")\n  }\n}\n\nobject Index {\n  def apply(index: JIndex): Index = index match {\n    case i: HashIndex  => Index(i.getKind, i.getDataType, i.getPrecision)\n    case i: RangeIndex => Index(i.getKind, i.getDataType, i.getPrecision)\n    case _             => throw new RuntimeException(s\"Unsupported kind $index\")\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/ReferenceCounted.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}\n\nprivate[cosmosdb] case class ReferenceCounted[T <: AutoCloseable](private val inner: T) {\n  private val count = new AtomicInteger(0)\n\n  private def inc(): Unit = count.incrementAndGet()\n\n  private def dec(): Unit = {\n    val newCount = count.decrementAndGet()\n    if (newCount <= 0) {\n      inner.close()\n      //Turn count to negative to ensure future reference call fail\n      count.decrementAndGet()\n    }\n  }\n\n  def isClosed: Boolean = count.get() < 0\n\n  def reference(): CountedReference = {\n    require(count.get >= 0, \"Reference is already closed\")\n    new CountedReference\n  }\n\n  class CountedReference extends AutoCloseable {\n    private val closed = new AtomicBoolean()\n    inc()\n    override def close(): Unit = if (closed.compareAndSet(false, true)) dec()\n\n    def get: T = inner\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/RetryMetricsCollector.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport org.apache.pekko.event.slf4j.SLF4JLogging\nimport ch.qos.logback.classic.LoggerContext\nimport ch.qos.logback.classic.spi.ILoggingEvent\nimport ch.qos.logback.core.AppenderBase\nimport com.microsoft.azure.cosmosdb.rx.internal.ResourceThrottleRetryPolicy\nimport org.apache.openwhisk.common.{Counter => WhiskCounter}\nimport kamon.metric.{Counter, MeasurementUnit}\nimport org.apache.openwhisk.common.{LogMarkerToken, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.slf4j.LoggerFactory\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.util.Try\n\nobject CosmosDBAction extends Enumeration {\n  val Create, Query, Get, Others = Value\n}\n\nobject RetryMetricsCollector extends AppenderBase[ILoggingEvent] with SLF4JLogging {\n  import CosmosDBAction._\n  private val tokens =\n    Map(Create -> Token(Create), Query -> Token(Query), Get -> Token(Get), Others -> Token(Others))\n\n  val retryCounter = new WhiskCounter\n  private[cosmosdb] def registerIfEnabled(): Unit = {\n    val enabled = loadConfigOrThrow[Boolean](s\"${ConfigKeys.cosmosdb}.retry-stats-enabled\")\n    if (enabled) {\n      log.info(\"Enabling retry metrics collector\")\n      register()\n    }\n  }\n\n  /**\n   * CosmosDB uses below log message\n   * ```\n   * logger.warn(\n   * \"Operation will be retried after {} milliseconds. Current attempt {}, Cumulative delay {}\",\n   *                     retryDelay.toMillis(),\n   *                     this.currentAttemptCount,\n   *                     this.cumulativeRetryDelay,\n   * exception);\n   * ```\n   *\n   */\n  override def append(e: ILoggingEvent): Unit = {\n    val msg = e.getMessage\n    val errorMsg = Option(e.getThrowableProxy).map(_.getMessage).getOrElse(msg)\n    for {\n      success <- isSuccessOrFailedRetry(msg)\n      token <- tokens.get(operationType(errorMsg))\n    } {\n      if (success) {\n        token.success.counter.increment()\n        //Element 1 has the count\n        val attemptCount = getRetryAttempt(e.getArgumentArray, 1)\n        token.success.histogram.record(attemptCount)\n\n        //Used mostly for test mode where tags may be disabled\n        //and test need to determine if count is increased\n        if (!TransactionId.metricsKamonTags) {\n          retryCounter.next()\n        }\n      } else {\n        token.failed.counter.increment()\n      }\n    }\n  }\n\n  def getCounter(opType: CosmosDBAction.Value, retryPassed: Boolean = true): Option[Counter] = {\n    tokens.get(opType).map(t => if (retryPassed) t.success else t.failed).map { _.counter }\n  }\n\n  private def getRetryAttempt(args: Array[AnyRef], index: Int) = {\n    val t = Try {\n      if (args != null & args.length > index) {\n        args(index) match {\n          case n: Number => n.intValue\n          case _         => 0\n        }\n      } else 0\n    }\n    t.getOrElse(0)\n  }\n\n  private def register(): Unit = {\n    val logCtx = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]\n    val retryLogger = logCtx.getLogger(classOf[ResourceThrottleRetryPolicy].getName)\n    start()\n    retryLogger.addAppender(this)\n  }\n\n  private def isSuccessOrFailedRetry(msg: String) = {\n    if (msg.startsWith(\"Operation will be retried after\")) Some(true)\n    else if (msg.startsWith(\"Operation will NOT be retried\")) Some(false)\n    else None\n  }\n\n  private def operationType(errorMsg: String) = {\n    if (errorMsg.contains(\"OperationType: Query\")) Query\n    else if (errorMsg.contains(\"OperationType: Create\")) Create\n    else if (errorMsg.contains(\"OperationType: Get\")) Get\n    else Others\n  }\n\n  private def createToken(opType: String, retryPassed: Boolean): LogMarkerToken = {\n    val action = if (retryPassed) \"success\" else \"failed\"\n    val tags = Map(\"type\" -> opType)\n    if (TransactionId.metricsKamonTags) LogMarkerToken(\"cosmosdb\", \"retry\", action, tags = tags)(MeasurementUnit.none)\n    else LogMarkerToken(\"cosmosdb\", \"retry\", action, Some(opType))(MeasurementUnit.none)\n  }\n\n  private case class Token(success: LogMarkerToken, failed: LogMarkerToken)\n\n  private object Token {\n    def apply(opType: CosmosDBAction.Value): Token =\n      new Token(createToken(opType.toString, retryPassed = true), createToken(opType.toString, retryPassed = false))\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/RxObservableImplicits.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport com.microsoft.azure.cosmosdb.{FeedResponse, Resource, ResourceResponse}\nimport rx.Observable\nimport rx.functions.Action1\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.{Future, Promise}\n\nprivate[cosmosdb] trait RxObservableImplicits {\n\n  implicit class RxScalaObservable[T](observable: Observable[T]) {\n\n    /**\n     * Returns the head of the [[Observable]] in a [[scala.concurrent.Future]].\n     *\n     * @return the head result of the [[Observable]].\n     */\n    def head(): Future[T] = {\n      def toHandler[P](f: (P) => Unit): Action1[P] = (t: P) => f(t)\n\n      val promise = Promise[T]()\n      observable.single.subscribe(toHandler(promise.success), toHandler(promise.failure))\n      promise.future\n    }\n  }\n\n  implicit class RxScalaResourceObservable[T <: Resource](observable: Observable[ResourceResponse[T]]) {\n    def blockingResult(): T = observable.toBlocking.single.getResource\n  }\n\n  implicit class RxScalaFeedObservable[T <: Resource](observable: Observable[FeedResponse[T]]) {\n    def blockingOnlyResult(): Option[T] = {\n      val value = observable.toBlocking.single\n      val results = value.getResults.asScala\n      require(results.isEmpty || results.size == 1, s\"More than one result found $results\")\n      results.headOption\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/elasticsearch/ElasticSearchActivationStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.elasticsearch\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging.ErrorLevel\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.stream.scaladsl.Flow\nimport com.google.common.base.Throwables\nimport com.sksamuel.elastic4s.http.search.SearchHit\nimport com.sksamuel.elastic4s.http.{ElasticClient, ElasticProperties, NoOpRequestConfigCallback}\nimport com.sksamuel.elastic4s.indexes.IndexRequest\nimport com.sksamuel.elastic4s.searches.queries.RangeQuery\nimport com.sksamuel.elastic4s.searches.queries.matches.MatchPhrase\nimport org.apache.http\nimport org.apache.http.auth.{AuthScope, UsernamePasswordCredentials}\nimport org.apache.http.conn.ConnectionKeepAliveStrategy\nimport org.apache.http.impl.client.BasicCredentialsProvider\nimport org.apache.http.impl.nio.client.HttpAsyncClientBuilder\nimport org.apache.http.protocol.HttpContext\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.logging.ElasticSearchJsonProtocol._\nimport org.apache.openwhisk.core.database.StoreUtils._\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.Messages\nimport org.elasticsearch.client.RestClientBuilder.HttpClientConfigCallback\nimport pureconfig.loadConfigOrThrow\nimport pureconfig.generic.auto._\nimport spray.json._\n\nimport java.time.Instant\nimport java.util.concurrent.TimeUnit\nimport scala.concurrent.duration.FiniteDuration\nimport scala.concurrent.{ExecutionContextExecutor, Future, Promise}\nimport scala.language.postfixOps\nimport scala.util.Try\n\ncase class ElasticSearchActivationStoreConfig(protocol: String,\n                                              hosts: String,\n                                              indexPattern: String,\n                                              username: String,\n                                              password: String)\n\nclass ElasticSearchActivationStore(\n  httpFlow: Option[Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), Any]] = None,\n  elasticSearchConfig: ElasticSearchActivationStoreConfig,\n  useBatching: Boolean = false)(implicit actorSystem: ActorSystem, override val logging: Logging)\n    extends ActivationStore {\n\n  import ElasticSearchActivationStore.{generateIndex, httpClientCallback}\n  import com.sksamuel.elastic4s.http.ElasticDsl._\n\n  private implicit val executionContextExecutor: ExecutionContextExecutor = actorSystem.dispatcher\n\n  private val client =\n    ElasticClient(\n      ElasticProperties(s\"${elasticSearchConfig.protocol}://${elasticSearchConfig.hosts}\"),\n      NoOpRequestConfigCallback,\n      httpClientCallback)\n\n  private val esType = \"_doc\"\n  private val maxOpenDbRequests = actorSystem.settings.config\n    .getInt(\"pekko.http.host-connection-pool.max-connections\") / 2\n  private val maxRetry = loadConfigOrThrow[Int](\"whisk.activation-store.retry-config.max-tries\")\n  private val batcher: Batcher[IndexRequest, Either[ArtifactStoreException, DocInfo]] =\n    new Batcher(500, maxOpenDbRequests, maxRetry)(doStore(_, _)(TransactionId.dbBatcher))\n\n  private val minStart = 0L\n  private val maxStart = Instant.now.toEpochMilli + TimeUnit.DAYS.toMillis(365 * 100) //100 years from now\n\n  override def store(activation: WhiskActivation, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo] = {\n\n    val start =\n      transid.started(this, LoggingMarkers.DATABASE_SAVE, s\"[PUT] 'activations' document: '${activation.docid}'\")\n\n    val bindingPath = activation.annotations\n      .getAs[String](WhiskActivation.bindingAnnotation)\n      .toOption\n      .map(binding => s\"$binding/${activation.name}\")\n\n    val path = bindingPath.getOrElse(\n      activation.annotations\n        .getAs[String](WhiskActivation.pathAnnotation)\n        .getOrElse(s\"${activation.namespace}/${activation.name}\"))\n\n    // Escape `_id` field as it's not permitted in ElasticSearch, add `path` field for search, and\n    // convert annotations to JsObject as ElasticSearch doesn't support array with mixed types\n    // response.result can be any type ElasticSearch also doesn't support that, so convert it to a string\n    val response = JsObject(\n      activation.response.toJsonObject.fields\n        .updated(\"result\", JsString(activation.response.result.toJson.compactPrint)))\n    val payload = JsObject(\n      activation.toDocumentRecord.fields - \"_id\" ++ Map(\n        \"path\" -> JsString(path),\n        \"@timestamp\" -> JsString(activation.start.toString),\n        \"annotations\" -> activation.annotations.toJsObject,\n        \"response\" -> response))\n\n    val index = generateIndex(activation.namespace.namespace)\n    val op = indexInto(index, esType).doc(payload.toString).id(activation.docid.asString)\n\n    // always use batching\n    val res = batcher.put(op).map {\n      case Right(docInfo) =>\n        transid\n          .finished(this, start, s\"[PUT] 'activations' completed document: '${activation.docid}', response: '$docInfo'\")\n        docInfo\n      case Left(e: ArtifactStoreException) =>\n        transid.failed(\n          this,\n          start,\n          s\"[PUT] 'activations' failed to put document: '${activation.docid}'; ${e.getMessage}.\",\n          ErrorLevel)\n        throw PutException(\"error on 'put'\")\n    }\n\n    res\n  }\n\n  private def doStore(ops: Seq[IndexRequest], retry: Int)(\n    implicit transid: TransactionId): Future[Seq[Either[ArtifactStoreException, DocInfo]]] = {\n    val count = ops.size\n    val start = transid.started(this, LoggingMarkers.DATABASE_BULK_SAVE, s\"'activations' saving $count documents\")\n    val res = client\n      .execute {\n        bulk(ops)\n      }\n      .map { res =>\n        if (res.status == StatusCodes.OK.intValue || res.status == StatusCodes.Created.intValue) {\n          res.result.items.map { bulkRes =>\n            if (bulkRes.status == StatusCodes.OK.intValue || bulkRes.status == StatusCodes.Created.intValue) {\n              transid\n                .finished(\n                  this,\n                  start,\n                  s\"[PUT] 'activations' completed document: '${bulkRes.id}', response: '${DocInfo(bulkRes.id)}'\")\n              Right(DocInfo(bulkRes.id))\n            } else {\n              transid.failed(\n                this,\n                start,\n                s\"'activations' failed to put documents, http status: '${bulkRes.status}'\",\n                ErrorLevel)\n              Left(PutException(\n                s\"Unexpected error: ${bulkRes.error.map(e => s\"${e.`type`}:${e.reason}\").getOrElse(\"unknown\")}, code: ${bulkRes.status} on 'bulk_put'\"))\n            }\n          }\n        } else {\n          transid.failed(\n            this,\n            start,\n            s\"'activations' failed to put documents, http status: '${res.status}'\",\n            ErrorLevel)\n          throw PutException(\"Unexpected http response code: \" + res.status)\n        }\n      }\n\n    res.recoverWith {\n      case t: ArtifactStoreException => Future.failed(t)\n      case _ if retry > 0 =>\n        transid.failed(this, start, s\"store activation to ElasticSearch failed\")\n        doStore(ops, retry - 1)\n      case t =>\n        transid.failed(\n          this,\n          start,\n          s\"[PUT] 'activations' internal error, failure: '${t.getMessage}' [${t.getClass.getSimpleName}]\\n\" + Throwables\n            .getStackTraceAsString(t),\n          ErrorLevel)\n        Future.failed(t)\n    }\n  }\n\n  override def get(activationId: ActivationId, context: UserContext)(\n    implicit transid: TransactionId): Future[WhiskActivation] = {\n\n    val start =\n      transid.started(this, LoggingMarkers.DATABASE_GET, s\"[GET] 'activations' finding activation: '$activationId'\")\n\n    val index = generateIndex(extractNamespace(activationId))\n    val res = client\n      .execute {\n        search(index) query { termQuery(\"_id\", activationId.asString) }\n      }\n      .map { res =>\n        if (res.status == StatusCodes.OK.intValue) {\n          if (res.result.hits.total == 0) {\n            transid.finished(this, start, s\"[GET] 'activations', document: '$activationId'; not found.\")\n            throw NoDocumentException(\"not found on 'get'\")\n          } else {\n            transid.finished(this, start, s\"[GET] 'activations' completed: found activation '$activationId'\")\n            deserializeHitToWhiskActivation(res.result.hits.hits(0))\n          }\n        } else if (res.status == StatusCodes.NotFound.intValue) {\n          transid.finished(this, start, s\"[GET] 'activations', document: '$activationId'; not found.\")\n          throw NoDocumentException(\"not found on 'get'\")\n        } else {\n          transid\n            .finished(\n              this,\n              start,\n              s\"[GET] 'activations' failed to get document: '$activationId'; http status: '${res.status}'\")\n          throw GetException(\"Unexpected http response code: \" + res.status)\n        }\n      } recoverWith {\n      case _: DeserializationException =>\n        transid\n          .finished(this, start, s\"[GET] 'activations' failed to get document: '$activationId'; failed to deserialize\")\n        throw DocumentUnreadable(Messages.corruptedEntity)\n    }\n\n    reportFailure(\n      res,\n      start,\n      failure => s\"[GET] 'activations' internal error, doc: '$activationId', failure: '${failure.getMessage}'\")\n  }\n\n  override def delete(activationId: ActivationId, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[Boolean] = {\n\n    val start =\n      transid.started(this, LoggingMarkers.DATABASE_DELETE, s\"[DEL] 'activations' deleting document: '$activationId'\")\n\n    val index = generateIndex(extractNamespace(activationId))\n\n    val res = client\n      .execute {\n        deleteByQuery(index, esType, termQuery(\"_id\", activationId.asString))\n      }\n      .map { res =>\n        if (res.status == StatusCodes.OK.intValue) {\n          if (res.result.deleted == 0) {\n            transid.finished(this, start, s\"[DEL] 'activations', document: '$activationId'; not found.\")\n            throw NoDocumentException(\"not found on 'delete'\")\n          } else {\n            transid\n              .finished(\n                this,\n                start,\n                s\"[DEL] 'activations' completed document: '$activationId', response: ${res.result}\")\n            true\n          }\n        } else if (res.status == StatusCodes.NotFound.intValue) {\n          transid.finished(this, start, s\"[DEL] 'activations', document: '$activationId'; not found.\")\n          throw NoDocumentException(\"not found on 'delete'\")\n        } else {\n          transid.failed(\n            this,\n            start,\n            s\"[DEL] 'activations' failed to delete document: '$activationId'; http status: '${res.status}'\",\n            ErrorLevel)\n          throw DeleteException(\"Unexpected http response code: \" + res.status)\n        }\n      }\n\n    reportFailure(\n      res,\n      start,\n      failure => s\"[DEL] 'activations' internal error, doc: '$activationId', failure: '${failure.getMessage}'\")\n  }\n\n  override def countActivationsInNamespace(namespace: EntityPath,\n                                           name: Option[EntityPath] = None,\n                                           skip: Int,\n                                           since: Option[Instant] = None,\n                                           upto: Option[Instant] = None,\n                                           context: UserContext)(implicit transid: TransactionId): Future[JsObject] = {\n    require(skip >= 0, \"skip should be non negative\")\n    val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s\"[COUNT] 'activations'\")\n\n    val nameQuery = name\n      .map { path =>\n        matchPhraseQuery(\"path\", namespace.addPath(path).asString)\n      }\n      .getOrElse {\n        matchPhraseQuery(\"namespace\", namespace.asString)\n      }\n    val startRange = generateRangeQuery(\"start\", since, upto)\n\n    val index = generateIndex(namespace.namespace)\n\n    val res = client\n      .execute {\n        count(index) query { must(nameQuery, startRange) }\n      }\n      .map { res =>\n        if (res.status == StatusCodes.OK.intValue) {\n          val out = if (res.result.count > skip) res.result.count - skip else 0L\n          transid.finished(this, start, s\"[COUNT] 'activations' completed: count $out\")\n          JsObject(WhiskActivation.collectionName -> JsNumber(out))\n        } else {\n          transid.failed(this, start, s\"Unexpected http response code: ${res.status}\", ErrorLevel)\n          throw QueryException(\"Unexpected http response code: \" + res.status)\n        }\n      }\n\n    reportFailure(res, start, failure => s\"[COUNT] 'activations' internal error, failure: '${failure.getMessage}'\")\n  }\n\n  override def listActivationsMatchingName(\n    namespace: EntityPath,\n    name: EntityPath,\n    skip: Int,\n    limit: Int,\n    includeDocs: Boolean = false,\n    since: Option[Instant] = None,\n    upto: Option[Instant] = None,\n    context: UserContext)(implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]] = {\n\n    val nameQuery = matchPhraseQuery(\"path\", namespace.addPath(name).asString)\n    listActivations(namespace, skip, limit, nameQuery, includeDocs, since, upto, context)\n  }\n\n  override def listActivationsInNamespace(\n    namespace: EntityPath,\n    skip: Int,\n    limit: Int,\n    includeDocs: Boolean = false,\n    since: Option[Instant] = None,\n    upto: Option[Instant] = None,\n    context: UserContext)(implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]] = {\n\n    val nameQuery = matchPhraseQuery(\"namespace\", namespace.asString)\n    listActivations(namespace, skip, limit, nameQuery, includeDocs, since, upto, context)\n  }\n\n  private def listActivations(\n    namespace: EntityPath,\n    skip: Int,\n    limit: Int,\n    nameQuery: MatchPhrase,\n    includeDocs: Boolean = false,\n    since: Option[Instant] = None,\n    upto: Option[Instant] = None,\n    context: UserContext)(implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]] = {\n\n    require(skip >= 0, \"skip should be non negative\")\n    require(limit >= 0, \"limit should be non negative\")\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s\"[QUERY] 'activations'\")\n    val startRange = generateRangeQuery(\"start\", since, upto)\n    val index = generateIndex(namespace.namespace)\n\n    val res = client\n      .execute {\n        search(index) query { must(nameQuery, startRange) } sortByFieldDesc \"start\" limit limit from skip\n      }\n      .map { res =>\n        if (res.status == StatusCodes.OK.intValue) {\n          val out =\n            if (includeDocs)\n              Right(res.result.hits.hits.map(deserializeHitToWhiskActivation).toList)\n            else\n              Left(res.result.hits.hits.map(deserializeHitToWhiskActivation(_).summaryAsJson).toList)\n          transid.finished(this, start, s\"[QUERY] 'activations' completed: matched ${res.result.hits.total}\")\n          out\n\n        } else {\n          transid.failed(this, start, s\"Unexpected http response code: ${res.status}\", ErrorLevel)\n          throw QueryException(\"Unexpected http response code: \" + res.status)\n        }\n      }\n\n    reportFailure(res, start, failure => s\"failed to query activation with error ${failure.getMessage}\")\n  }\n\n  private def deserializeHitToWhiskActivation(hit: SearchHit): WhiskActivation = {\n    restoreAnnotations(restoreResponse(hit.sourceAsString.parseJson.asJsObject)).convertTo[WhiskActivation]\n  }\n\n  private def restoreAnnotations(js: JsValue): JsObject = {\n    val annotations = js.asJsObject.fields\n      .get(\"annotations\")\n      .map { anno =>\n        Try {\n          JsArray(anno.asJsObject.fields map { p =>\n            JsObject(\"key\" -> JsString(p._1), \"value\" -> p._2)\n          } toSeq: _*)\n        }.getOrElse(JsArray.empty)\n      }\n      .getOrElse(JsArray.empty)\n    JsObject(js.asJsObject.fields.updated(\"annotations\", annotations))\n  }\n\n  private def restoreResponse(js: JsObject): JsValue = {\n    val response = js.fields\n      .get(\"response\")\n      .map { res =>\n        val temp = res.asJsObject.fields\n        Try {\n          val result = temp\n            .get(\"result\")\n            .map { r =>\n              val JsString(data) = r\n              data.parseJson match {\n                case JsArray(elements) => JsArray(elements)\n                case _                 => data.parseJson.asJsObject\n              }\n            }\n            .getOrElse(JsObject.empty)\n          JsObject(temp.updated(\"result\", result))\n        }.getOrElse(JsObject(temp - \"result\"))\n      }\n      .getOrElse(JsObject.empty)\n    JsObject(js.fields.updated(\"response\", response))\n  }\n\n  private def extractNamespace(activationId: ActivationId): String = {\n    activationId.toString.split(\"/\")(0)\n  }\n\n  private def generateRangeQuery(key: String, since: Option[Instant], upto: Option[Instant]): RangeQuery = {\n    rangeQuery(key)\n      .gte(since.map(_.toEpochMilli).getOrElse(minStart))\n      .lte(upto.map(_.toEpochMilli).getOrElse(maxStart))\n  }\n}\n\nobject ElasticSearchActivationStore {\n  val elasticSearchConfig: ElasticSearchActivationStoreConfig =\n    loadConfigOrThrow[ElasticSearchActivationStoreConfig](ConfigKeys.elasticSearchActivationStore)\n\n  val httpClientCallback = new HttpClientConfigCallback {\n    override def customizeHttpClient(httpClientBuilder: HttpAsyncClientBuilder): HttpAsyncClientBuilder = {\n      val provider = new BasicCredentialsProvider\n      provider.setCredentials(\n        AuthScope.ANY,\n        new UsernamePasswordCredentials(elasticSearchConfig.username, elasticSearchConfig.password))\n      httpClientBuilder.setDefaultCredentialsProvider(provider)\n      httpClientBuilder.setKeepAliveStrategy(new CustomKeepAliveStrategy())\n    }\n  }\n\n  def generateIndex(namespace: String): String = {\n    elasticSearchConfig.indexPattern.dropWhile(_ == '/') format namespace.toLowerCase\n  }\n}\n\nobject ElasticSearchActivationStoreProvider extends ActivationStoreProvider {\n  import ElasticSearchActivationStore.elasticSearchConfig\n\n  override def instance(actorSystem: ActorSystem, logging: Logging) =\n    new ElasticSearchActivationStore(elasticSearchConfig = elasticSearchConfig, useBatching = true)(\n      actorSystem,\n      logging)\n}\n\nclass CustomKeepAliveStrategy extends ConnectionKeepAliveStrategy {\n  override def getKeepAliveDuration(response: http.HttpResponse, context: HttpContext): Long = {\n    loadConfigOrThrow[FiniteDuration](\"whisk.activation-store.elasticsearch.keep-alive\").toMillis\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/memory/MemoryArtifactStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.memory\n\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.{ContentType, Uri}\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.util.ByteString\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.StoreUtils._\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity.Attachments.Attached\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json.{DefaultJsonProtocol, DeserializationException, JsObject, JsString, RootJsonFormat}\n\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.reflect.ClassTag\nimport scala.util.{Failure, Success, Try}\n\nobject MemoryArtifactStoreProvider extends ArtifactStoreProvider {\n  private val stores = new TrieMap[String, MemoryArtifactStore[_]]()\n  override def makeStore[D <: DocumentSerializer: ClassTag](useBatching: Boolean)(\n    implicit jsonFormat: RootJsonFormat[D],\n    docReader: DocumentReader,\n    actorSystem: ActorSystem,\n    logging: Logging): ArtifactStore[D] = {\n    makeArtifactStore(MemoryAttachmentStoreProvider.makeStore())\n  }\n\n  def makeArtifactStore[D <: DocumentSerializer: ClassTag](attachmentStore: AttachmentStore)(\n    implicit jsonFormat: RootJsonFormat[D],\n    docReader: DocumentReader,\n    actorSystem: ActorSystem,\n    logging: Logging): ArtifactStore[D] = {\n\n    val classTag = implicitly[ClassTag[D]]\n    val (dbName, handler, viewMapper) = handlerAndMapper(classTag)\n    val inliningConfig = loadConfigOrThrow[InliningConfig](ConfigKeys.db)\n    val storeFactory = () => new MemoryArtifactStore(dbName, handler, viewMapper, inliningConfig, attachmentStore)\n    stores.getOrElseUpdate(dbName, storeFactory.apply()).asInstanceOf[ArtifactStore[D]]\n  }\n\n  def purgeAll(): Unit = stores.clear()\n\n  private def handlerAndMapper[D](entityType: ClassTag[D])(\n    implicit actorSystem: ActorSystem,\n    logging: Logging): (String, DocumentHandler, MemoryViewMapper) = {\n    entityType.runtimeClass match {\n      case x if x == classOf[WhiskEntity] =>\n        (\"whisks\", WhisksHandler, WhisksViewMapper)\n      case x if x == classOf[WhiskActivation] =>\n        (\"activations\", ActivationHandler, ActivationViewMapper)\n      case x if x == classOf[WhiskAuth] =>\n        (\"subjects\", SubjectHandler, SubjectViewMapper)\n    }\n  }\n}\n\n/**\n * In-memory ArtifactStore implementation to enable test setups without requiring a running CouchDB instance\n * It also serves as a canonical example of how an ArtifactStore can implemented with all the support for CRUD\n * operations and Queries etc\n */\nclass MemoryArtifactStore[DocumentAbstraction <: DocumentSerializer](dbName: String,\n                                                                     documentHandler: DocumentHandler,\n                                                                     viewMapper: MemoryViewMapper,\n                                                                     val inliningConfig: InliningConfig,\n                                                                     val attachmentStore: AttachmentStore)(\n  implicit system: ActorSystem,\n  val logging: Logging,\n  jsonFormat: RootJsonFormat[DocumentAbstraction],\n  docReader: DocumentReader)\n    extends ArtifactStore[DocumentAbstraction]\n    with DefaultJsonProtocol\n    with DocumentProvider\n    with AttachmentSupport[DocumentAbstraction] {\n\n  logging.info(this, s\"Created MemoryStore for [$dbName]\")\n\n  override protected[core] implicit val executionContext: ExecutionContext = system.dispatcher\n\n  private val artifacts = new TrieMap[String, Artifact]\n\n  private val _id = \"_id\"\n  private val _rev = \"_rev\"\n  val attachmentScheme: String = attachmentStore.scheme\n\n  override protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo] = {\n    val asJson = d.toDocumentRecord\n\n    val id = asJson.fields(_id).convertTo[String].trim\n    require(!id.isEmpty, \"document id must be defined\")\n\n    val (oldRev, newRev) = computeRevision(asJson)\n    val docinfoStr = s\"id: $id, rev: ${oldRev.getOrElse(\"null\")}\"\n    val start = transid.started(this, LoggingMarkers.DATABASE_SAVE, s\"[PUT] '$dbName' saving document: '$docinfoStr'\")\n\n    val updated = Artifact(id, newRev, asJson)\n    val t = Try[DocInfo] {\n      oldRev match {\n        case Some(rev) =>\n          val existing = Artifact(id, rev, asJson)\n          if (artifacts.replace(id, existing, updated)) {\n            updated.docInfo\n          } else {\n            throw DocumentConflictException(\"conflict on 'put'\")\n          }\n        case None =>\n          artifacts.putIfAbsent(id, updated) match {\n            case Some(_) => throw DocumentConflictException(\"conflict on 'put'\")\n            case None    => updated.docInfo\n          }\n      }\n    }\n\n    val f = Future.fromTry(t)\n    f.onComplete {\n      case Success(_) => transid.finished(this, start, s\"[PUT] '$dbName' completed document: '$docinfoStr'\")\n      case Failure(_: DocumentConflictException) =>\n        transid.finished(this, start, s\"[PUT] '$dbName', document: '$docinfoStr'; conflict.\")\n      case Failure(_) =>\n    }\n\n    reportFailure(f, start, failure => s\"[PUT] '$dbName' internal error, failure: '${failure.getMessage}'\")\n  }\n\n  override protected[database] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] = {\n    checkDocHasRevision(doc)\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_DELETE, s\"[DEL] '$dbName' deleting document: '$doc'\")\n    val t = Try[Boolean] {\n      if (artifacts.remove(doc.id.id, Artifact(doc))) {\n        transid.finished(this, start, s\"[DEL] '$dbName' completed document: '$doc'\")\n        true\n      } else if (artifacts.contains(doc.id.id)) {\n        //Indicates that document exist but revision does not match\n        transid.finished(this, start, s\"[DEL] '$dbName', document: '$doc'; conflict.\")\n        throw DocumentConflictException(\"conflict on 'delete'\")\n      } else {\n        transid.finished(this, start, s\"[DEL] '$dbName', document: '$doc'; not found.\")\n        // for compatibility\n        throw NoDocumentException(\"not found on 'delete'\")\n      }\n    }\n\n    val f = Future.fromTry(t)\n\n    reportFailure(f, start, failure => s\"[DEL] '$dbName' internal error, doc: '$doc', failure: '${failure.getMessage}'\")\n  }\n\n  override protected[database] def get[A <: DocumentAbstraction](doc: DocInfo,\n                                                                 attachmentHandler: Option[(A, Attached) => A] = None)(\n    implicit transid: TransactionId,\n    ma: Manifest[A]): Future[A] = {\n    val start = transid.started(this, LoggingMarkers.DATABASE_GET, s\"[GET] '$dbName' finding document: '$doc'\")\n\n    require(doc != null, \"doc undefined\")\n\n    val t = Try[A] {\n      artifacts.get(doc.id.id) match {\n        case Some(a) =>\n          //Revision matching is enforced in deserilization logic\n          transid.finished(this, start, s\"[GET] '$dbName' completed: found document '$doc'\")\n          deserialize[A, DocumentAbstraction](doc, a.doc)\n        case _ =>\n          transid.finished(this, start, s\"[GET] '$dbName', document: '$doc'; not found.\")\n          // for compatibility\n          throw NoDocumentException(\"not found on 'get'\")\n      }\n    }\n\n    val f = Future.fromTry(t).recoverWith {\n      case _: DeserializationException => throw DocumentUnreadable(Messages.corruptedEntity)\n    }\n\n    reportFailure(f, start, failure => s\"[GET] '$dbName' internal error, doc: '$doc', failure: '${failure.getMessage}'\")\n  }\n\n  override protected[core] def query(table: String,\n                                     startKey: List[Any],\n                                     endKey: List[Any],\n                                     skip: Int,\n                                     limit: Int,\n                                     includeDocs: Boolean,\n                                     descending: Boolean,\n                                     reduce: Boolean,\n                                     stale: StaleParameter)(implicit transid: TransactionId): Future[List[JsObject]] = {\n    require(!(reduce && includeDocs), \"reduce and includeDocs cannot both be true\")\n    require(!reduce, \"Reduce scenario not supported\") //TODO Investigate reduce\n    require(skip >= 0, \"skip should be non negative\")\n    require(limit >= 0, \"limit should be non negative\")\n\n    documentHandler.checkIfTableSupported(table)\n\n    val Array(ddoc, viewName) = table.split(\"/\")\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s\"[QUERY] '$dbName' searching '$table\")\n\n    val s = artifacts.toStream\n      .map(_._2)\n      .filter(a => viewMapper.filter(ddoc, viewName, startKey, endKey, a.doc, a.computed))\n      .map(_.doc)\n      .toList\n\n    val sorted = viewMapper.sort(ddoc, viewName, descending, s)\n\n    val out = if (limit > 0) sorted.slice(skip, skip + limit) else sorted.drop(skip)\n\n    val realIncludeDocs = includeDocs | documentHandler.shouldAlwaysIncludeDocs(ddoc, viewName)\n\n    val r = out.map { js =>\n      documentHandler.transformViewResult(\n        ddoc,\n        viewName,\n        startKey,\n        endKey,\n        realIncludeDocs,\n        js,\n        MemoryArtifactStore.this)\n    }.toList\n\n    val f = Future.sequence(r).map(_.flatten)\n    f.foreach(_ => transid.finished(this, start, s\"[QUERY] '$dbName' completed: matched ${out.size}\"))\n    reportFailure(f, start, failure => s\"[QUERY] '$dbName' internal error, failure: '${failure.getMessage}'\")\n\n  }\n\n  override protected[core] def count(table: String,\n                                     startKey: List[Any],\n                                     endKey: List[Any],\n                                     skip: Int,\n                                     stale: StaleParameter)(implicit transid: TransactionId): Future[Long] = {\n    val f =\n      query(table, startKey, endKey, skip, limit = 0, includeDocs = false, descending = true, reduce = false, stale)\n    f.map(_.size)\n  }\n\n  override protected[core] def readAttachment[T](doc: DocInfo, attached: Attached, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T] = {\n    val name = attached.attachmentName\n    val start = transid.started(\n      this,\n      LoggingMarkers.DATABASE_ATT_GET,\n      s\"[ATT_GET] '$dbName' finding attachment '$name' of document '$doc'\")\n\n    val attachmentUri = Uri(name)\n    if (isInlined(attachmentUri)) {\n      memorySource(attachmentUri).runWith(sink)\n    } else {\n      val storedName = attachmentUri.path.toString()\n      val f = attachmentStore.readAttachment(doc.id, storedName, sink)\n      f.foreach(_ =>\n        transid.finished(this, start, s\"[ATT_GET] '$dbName' completed: found attachment '$name' of document '$doc'\"))\n      f\n    }\n  }\n\n  override protected[core] def deleteAttachments[T](doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] = {\n    attachmentStore.deleteAttachments(doc.id)\n  }\n\n  override protected[database] def putAndAttach[A <: DocumentAbstraction](\n    d: A,\n    update: (A, Attached) => A,\n    contentType: ContentType,\n    docStream: Source[ByteString, _],\n    oldAttachment: Option[Attached])(implicit transid: TransactionId): Future[(DocInfo, Attached)] = {\n    attachToExternalStore(d, update, contentType, docStream, oldAttachment, attachmentStore)\n  }\n\n  override def shutdown(): Unit = {\n    attachmentStore.shutdown()\n  }\n\n  override protected[database] def get(id: DocId)(implicit transid: TransactionId): Future[Option[JsObject]] = {\n    val start = transid.started(this, LoggingMarkers.DATABASE_GET, s\"[GET] '$dbName' finding document: '$id'\")\n\n    val t = Try {\n      artifacts.get(id.id) match {\n        case Some(a) =>\n          transid.finished(this, start, s\"[GET] '$dbName' completed: found document '$id'\")\n          Some(a.doc)\n        case _ =>\n          transid.finished(this, start, s\"[GET] '$dbName', document: '$id'; not found.\")\n          None\n      }\n    }\n\n    val f = Future.fromTry(t)\n\n    reportFailure(f, start, failure => s\"[GET] '$dbName' internal error, doc: '$id', failure: '${failure.getMessage}'\")\n  }\n\n  private def computeRevision(js: JsObject): (Option[String], String) = {\n    js.fields.get(_rev) match {\n      case Some(JsString(r)) => (Some(r), digest(js))\n      case _                 => (None, digest(js))\n    }\n  }\n\n  private def digest(js: JsObject) = {\n    val jsWithoutRev = transform(js, Seq.empty, Seq(_rev))\n    val md = emptyDigest()\n    encodeDigest(md.digest(jsWithoutRev.compactPrint.getBytes(UTF_8)))\n  }\n\n  //Use curried case class to allow equals support only for id and rev\n  //This allows us to implement atomic replace and remove which check\n  //for id,rev equality only\n  private case class Artifact(id: String, rev: String)(val doc: JsObject, val computed: JsObject) {\n    def docInfo = DocInfo(DocId(id), DocRevision(rev.toString))\n  }\n\n  private object Artifact {\n    def apply(id: String, rev: String, doc: JsObject): Artifact = {\n      val docWithRev = transform(doc, Seq((_rev, Some(JsString(rev)))))\n      Artifact(id, rev)(docWithRev, documentHandler.computedFields(doc))\n    }\n\n    def apply(info: DocInfo): Artifact = {\n      Artifact(info.id.id, info.rev.rev)(JsObject.empty, JsObject.empty)\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/memory/MemoryAttachmentStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.memory\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.ContentType\nimport org.apache.pekko.stream.scaladsl.{Keep, Sink, Source}\nimport org.apache.pekko.util.{ByteString, ByteStringBuilder}\nimport org.apache.openwhisk.common.LoggingMarkers.{\n  DATABASE_ATTS_DELETE,\n  DATABASE_ATT_DELETE,\n  DATABASE_ATT_GET,\n  DATABASE_ATT_SAVE\n}\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.database.StoreUtils._\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity.DocId\n\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.reflect.ClassTag\n\nobject MemoryAttachmentStoreProvider extends AttachmentStoreProvider {\n  override def makeStore[D <: DocumentSerializer: ClassTag]()(implicit actorSystem: ActorSystem,\n                                                              logging: Logging): AttachmentStore =\n    new MemoryAttachmentStore(implicitly[ClassTag[D]].runtimeClass.getSimpleName.toLowerCase)\n}\n\n/**\n * Basic in-memory AttachmentStore implementation. Useful for testing.\n */\nclass MemoryAttachmentStore(dbName: String)(implicit system: ActorSystem, logging: Logging) extends AttachmentStore {\n\n  override protected[core] implicit val executionContext: ExecutionContext = system.dispatcher\n\n  private case class Attachment(bytes: ByteString)\n\n  private val attachments = new TrieMap[String, Attachment]\n  private var closed = false\n\n  override val scheme = \"mems\"\n\n  override protected[core] def attach(\n    docId: DocId,\n    name: String,\n    contentType: ContentType,\n    docStream: Source[ByteString, _])(implicit transid: TransactionId): Future[AttachResult] = {\n    require(name != null, \"name undefined\")\n    val start =\n      transid.started(this, DATABASE_ATT_SAVE, s\"[ATT_PUT] uploading attachment '$name' of document 'id: $docId'\")\n\n    val uploadSink = Sink.fold[ByteStringBuilder, ByteString](new ByteStringBuilder)((builder, b) => builder ++= b)\n\n    val f = docStream.runWith(combinedSink(uploadSink))\n\n    val g = f.map { r =>\n      attachments += (attachmentKey(docId, name) -> Attachment(r.uploadResult.result().compact))\n      transid\n        .finished(this, start, s\"[ATT_PUT] '$dbName' completed uploading attachment '$name' of document '$docId'\")\n      AttachResult(r.digest, r.length)\n    }\n\n    reportFailure(\n      g,\n      start,\n      failure =>\n        s\"[ATT_PUT] '$dbName' internal error, name: '$name', doc: 'id: $docId', failure: '${failure.getMessage}'\")\n  }\n\n  /**\n   * Retrieves a saved attachment, streaming it into the provided Sink.\n   */\n  override protected[core] def readAttachment[T](docId: DocId, name: String, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T] = {\n\n    val start =\n      transid.started(\n        this,\n        DATABASE_ATT_GET,\n        s\"[ATT_GET] '$dbName' finding attachment '$name' of document 'id: $docId'\")\n\n    val f = attachments.get(attachmentKey(docId, name)) match {\n      case Some(Attachment(bytes)) =>\n        val r = Source.single(bytes).toMat(sink)(Keep.right).run\n        r.map(t => {\n          transid.finished(this, start, s\"[ATT_GET] '$dbName' completed: found attachment '$name' of document '$docId'\")\n          t\n        })\n      case None =>\n        transid.finished(\n          this,\n          start,\n          s\"[ATT_GET] '$dbName', retrieving attachment '$name' of document '$docId'; not found.\")\n        Future.failed(NoDocumentException(\"Not found on 'readAttachment'.\"))\n    }\n    reportFailure(\n      f,\n      start,\n      failure => s\"[ATT_GET] '$dbName' internal error, name: '$name', doc: '$docId', failure: '${failure.getMessage}'\")\n  }\n\n  override protected[core] def deleteAttachments(docId: DocId)(implicit transid: TransactionId): Future[Boolean] = {\n    val start = transid.started(this, DATABASE_ATTS_DELETE, s\"[ATTS_DELETE] uploading attachment of document '$docId'\")\n\n    val prefix = docId + \"/\"\n    attachments --= attachments.keySet.filter(_.startsWith(prefix))\n    transid.finished(this, start, s\"[ATTS_DELETE] completed: delete attachment of document '$docId'\")\n    Future.successful(true)\n  }\n\n  override protected[core] def deleteAttachment(docId: DocId, name: String)(\n    implicit transid: TransactionId): Future[Boolean] = {\n    val start = transid.started(this, DATABASE_ATT_DELETE, s\"[ATT_DELETE] uploading attachment of document '$docId'\")\n    attachments.remove(attachmentKey(docId, name))\n    transid.finished(this, start, s\"[ATT_DELETE] completed: delete attachment of document '$docId'\")\n    Future.successful(true)\n  }\n\n  def attachmentCount: Int = attachments.size\n\n  def isClosed = closed\n\n  override def shutdown(): Unit = {\n    closed = true\n  }\n\n  private def attachmentKey(docId: DocId, name: String) = s\"${docId.id}/$name\"\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/memory/MemoryViewMapper.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.memory\n\nimport spray.json.{JsArray, JsBoolean, JsNumber, JsObject, JsString, JsTrue}\nimport org.apache.openwhisk.core.database.{ActivationHandler, UnsupportedQueryKeys, UnsupportedView, WhisksHandler}\nimport org.apache.openwhisk.core.entity.{UserLimits, WhiskQueries}\nimport org.apache.openwhisk.utils.JsHelpers\n\n/**\n * Maps the CouchDB view logic to expressed in javascript to Scala logic so as to enable\n * performing queries by {{{MemoryArtifactStore}}}. Also serves as an example of what all query usecases\n * are to be supported by any {{{ArtifactStore}}} implementation\n */\ntrait MemoryViewMapper {\n  protected val TOP: String = WhiskQueries.TOP\n\n  def filter(ddoc: String, view: String, startKey: List[Any], endKey: List[Any], d: JsObject, c: JsObject): Boolean\n\n  def sort(ddoc: String, view: String, descending: Boolean, s: Seq[JsObject]): Seq[JsObject]\n\n  protected def checkKeys(startKey: List[Any], endKey: List[Any]): Unit = {\n    require(startKey.nonEmpty)\n    require(endKey.nonEmpty)\n    require(startKey.head == endKey.head, s\"First key should be same => ($startKey) - ($endKey)\")\n  }\n\n  protected def equal(js: JsObject, name: String, value: String): Boolean =\n    JsHelpers.getFieldPath(js, name) match {\n      case Some(JsString(v)) => v == value\n      case _                 => false\n    }\n\n  protected def isTrue(js: JsObject, name: String): Boolean =\n    JsHelpers.getFieldPath(js, name) match {\n      case Some(JsBoolean(v)) => v\n      case _                  => false\n    }\n\n  protected def gte(js: JsObject, name: String, value: Number): Boolean =\n    JsHelpers.getFieldPath(js, name) match {\n      case Some(JsNumber(n)) => n.longValue >= value.longValue\n      case _                 => false\n    }\n\n  protected def lte(js: JsObject, name: String, value: Number): Boolean =\n    JsHelpers.getFieldPath(js, name) match {\n      case Some(JsNumber(n)) => n.longValue <= value.longValue\n      case _                 => false\n    }\n\n  protected def numericSort(s: Seq[JsObject], descending: Boolean, name: String): Seq[JsObject] = {\n    val f =\n      (js: JsObject) =>\n        JsHelpers.getFieldPath(js, name) match {\n          case Some(JsNumber(n)) => n.longValue\n          case _                 => 0L\n      }\n    val order = implicitly[Ordering[Long]]\n    val ordering = if (descending) order.reverse else order\n    s.sortBy(f)(ordering)\n  }\n}\n\nprivate object ActivationViewMapper extends MemoryViewMapper {\n  private val NS = \"namespace\"\n  private val NS_WITH_PATH = ActivationHandler.NS_PATH\n  private val START = \"start\"\n\n  override def filter(ddoc: String,\n                      view: String,\n                      startKey: List[Any],\n                      endKey: List[Any],\n                      d: JsObject,\n                      c: JsObject): Boolean = {\n    checkKeys(startKey, endKey)\n    val nsValue = startKey.head.asInstanceOf[String]\n    view match {\n      //whisks-filters ddoc uses namespace + invoking action path as first key\n      case \"activations\" if ddoc.startsWith(\"whisks-filters\") =>\n        filterActivation(d, equal(c, NS_WITH_PATH, nsValue), startKey, endKey)\n      //whisks ddoc uses namespace as first key\n      case \"activations\" if ddoc.startsWith(\"whisks\") => filterActivation(d, equal(d, NS, nsValue), startKey, endKey)\n      case _                                          => throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  override def sort(ddoc: String, view: String, descending: Boolean, s: Seq[JsObject]): Seq[JsObject] =\n    view match {\n      case \"activations\" if ddoc.startsWith(\"whisks\") => numericSort(s, descending, START)\n      case _                                          => throw UnsupportedView(s\"$ddoc/$view\")\n    }\n\n  private def filterActivation(d: JsObject, matchNS: Boolean, startKey: List[Any], endKey: List[Any]): Boolean = {\n    val filterResult = (startKey, endKey) match {\n      case (_ :: Nil, _ :: `TOP` :: Nil) =>\n        matchNS\n      case (_ :: (since: Number) :: Nil, _ :: `TOP` :: `TOP` :: Nil) =>\n        matchNS && gte(d, START, since)\n      case (_ :: (since: Number) :: Nil, _ :: (upto: Number) :: `TOP` :: Nil) =>\n        matchNS && gte(d, START, since) && lte(d, START, upto)\n      case _ => throw UnsupportedQueryKeys(s\"$startKey, $endKey\")\n    }\n    filterResult\n  }\n}\n\nprivate object WhisksViewMapper extends MemoryViewMapper {\n  private val NS = \"namespace\"\n  private val ROOT_NS = WhisksHandler.ROOT_NS\n  private val TYPE = \"entityType\"\n  private val UPDATED = \"updated\"\n  private val PUBLISH = \"publish\"\n  private val BINDING = \"binding\"\n\n  override def filter(ddoc: String,\n                      view: String,\n                      startKey: List[Any],\n                      endKey: List[Any],\n                      d: JsObject,\n                      c: JsObject): Boolean = {\n    checkKeys(startKey, endKey)\n    val entityType = WhisksHandler.getEntityTypeForDesignDoc(ddoc, view)\n\n    val matchTypeAndView = equal(d, TYPE, entityType) && matchViewConditions(ddoc, view, d)\n    val matchNS = equal(d, NS, startKey.head.asInstanceOf[String])\n    val matchRootNS = equal(c, ROOT_NS, startKey.head.asInstanceOf[String])\n\n    //Here ddocs for actions, rules and triggers use\n    //namespace and namespace/packageName as first key\n\n    val filterResult = (startKey, endKey) match {\n      case (ns :: Nil, _ :: `TOP` :: Nil) =>\n        (matchTypeAndView && matchNS) || (matchTypeAndView && matchRootNS)\n\n      case (ns :: (since: Number) :: Nil, _ :: `TOP` :: `TOP` :: Nil) =>\n        (matchTypeAndView && matchNS && gte(d, UPDATED, since)) ||\n          (matchTypeAndView && matchRootNS && gte(d, UPDATED, since))\n      case (ns :: (since: Number) :: Nil, _ :: (upto: Number) :: `TOP` :: Nil) =>\n        (matchTypeAndView && matchNS && gte(d, UPDATED, since) && lte(d, UPDATED, upto)) ||\n          (matchTypeAndView && matchRootNS && gte(d, UPDATED, since) && lte(d, UPDATED, upto))\n\n      case _ => throw UnsupportedQueryKeys(s\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n    filterResult\n  }\n\n  private def matchViewConditions(ddoc: String, view: String, d: JsObject): Boolean = {\n    view match {\n      case \"packages-public\" if ddoc.startsWith(\"whisks\") =>\n        isTrue(d, PUBLISH) && hasEmptyBinding(d)\n      case _ => true\n    }\n  }\n\n  private def hasEmptyBinding(js: JsObject) = {\n    js.fields.get(BINDING) match {\n      case Some(x: JsObject) if x.fields.nonEmpty => false\n      case _                                      => true\n    }\n  }\n\n  override def sort(ddoc: String, view: String, descending: Boolean, s: Seq[JsObject]): Seq[JsObject] = {\n    view match {\n      case \"actions\" | \"rules\" | \"triggers\" | \"packages\" | \"packages-public\" if ddoc.startsWith(\"whisks\") =>\n        numericSort(s, descending, UPDATED)\n      case _ => throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n}\n\nprivate object SubjectViewMapper extends MemoryViewMapper {\n  private val BLOCKED = \"blocked\"\n  private val SUBJECT = \"subject\"\n  private val UUID = \"uuid\"\n  private val KEY = \"key\"\n  private val NS_NAME = \"name\"\n\n  override def filter(ddoc: String,\n                      view: String,\n                      startKey: List[Any],\n                      endKey: List[Any],\n                      d: JsObject,\n                      c: JsObject): Boolean = {\n    require(startKey == endKey, s\"startKey: $startKey and endKey: $endKey must be same for $ddoc/$view\")\n    (ddoc, view) match {\n      case (s, \"identities\") if s.startsWith(\"subjects\") =>\n        filterForMatchingSubjectOrNamespace(ddoc, view, startKey, endKey, d)\n      case (\"namespaceThrottlings\", \"blockedNamespaces\") =>\n        filterForBlacklistedNamespace(d)\n      case _ =>\n        throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  private def filterForBlacklistedNamespace(d: JsObject): Boolean = {\n    val id = d.fields(\"_id\")\n    id match {\n      case JsString(idv) if idv.endsWith(\"/limits\") =>\n        val limits = UserLimits.serdes.read(d)\n        limits.concurrentInvocations.contains(0) || limits.invocationsPerMinute.contains(0)\n      case _ =>\n        d.getFields(BLOCKED) match {\n          case Seq(JsTrue) => true\n          case _           => false\n        }\n    }\n  }\n\n  private def filterForMatchingSubjectOrNamespace(ddoc: String,\n                                                  view: String,\n                                                  startKey: List[Any],\n                                                  endKey: List[Any],\n                                                  d: JsObject) = {\n    val notBlocked = !isTrue(d, BLOCKED)\n    startKey match {\n      case (ns: String) :: Nil => notBlocked && (equal(d, SUBJECT, ns) || matchingNamespace(d, equal(_, NS_NAME, ns)))\n      case (uuid: String) :: (key: String) :: Nil =>\n        notBlocked &&\n          (\n            (equal(d, UUID, uuid) && equal(d, KEY, key))\n              || matchingNamespace(d, js => equal(js, UUID, uuid) && equal(js, KEY, key))\n          )\n      case _ => throw UnsupportedQueryKeys(s\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n  }\n\n  override def sort(ddoc: String, view: String, descending: Boolean, s: Seq[JsObject]): Seq[JsObject] = {\n    s //No sorting to be done\n  }\n\n  private def matchingNamespace(js: JsObject, matcher: JsObject => Boolean): Boolean = {\n    js.fields.get(\"namespaces\") match {\n      case Some(JsArray(e)) => e.exists(v => matcher(v.asJsObject))\n      case _                => false\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/memory/NoopActivationStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.memory\n\nimport java.time.Instant\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.{Logging, PrintStreamLogging, TransactionId, WhiskInstants}\nimport org.apache.openwhisk.core.database.{\n  ActivationStore,\n  ActivationStoreProvider,\n  CacheChangeNotification,\n  UserContext\n}\nimport org.apache.openwhisk.core.entity.{ActivationId, DocInfo, EntityName, EntityPath, Subject, WhiskActivation}\nimport spray.json.{JsNumber, JsObject}\n\nimport scala.concurrent.Future\n\nobject NoopActivationStore extends ActivationStore with WhiskInstants {\n  override val logging = new PrintStreamLogging()\n  private val emptyInfo = DocInfo(\"foo\")\n  private val emptyCount = JsObject(\"activations\" -> JsNumber(0))\n  private val dummyActivation = WhiskActivation(\n    EntityPath(\"testnamespace\"),\n    EntityName(\"activation\"),\n    Subject(),\n    ActivationId.generate(),\n    start = Instant.now.inMills,\n    end = Instant.now.inMills)\n\n  override def store(activation: WhiskActivation, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo] = Future.successful(emptyInfo)\n\n  override def get(activationId: ActivationId, context: UserContext)(\n    implicit transid: TransactionId): Future[WhiskActivation] = {\n    val activation = dummyActivation.copy(activationId = activationId)\n    Future.successful(activation)\n  }\n\n  override def delete(activationId: ActivationId, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[Boolean] = Future.successful(true)\n\n  override def countActivationsInNamespace(namespace: EntityPath,\n                                           name: Option[EntityPath],\n                                           skip: Int,\n                                           since: Option[Instant],\n                                           upto: Option[Instant],\n                                           context: UserContext)(implicit transid: TransactionId): Future[JsObject] =\n    Future.successful(emptyCount)\n\n  override def listActivationsMatchingName(\n    namespace: EntityPath,\n    name: EntityPath,\n    skip: Int,\n    limit: Int,\n    includeDocs: Boolean,\n    since: Option[Instant],\n    upto: Option[Instant],\n    context: UserContext)(implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]] =\n    Future.successful(Right(List.empty))\n\n  override def listActivationsInNamespace(\n    namespace: EntityPath,\n    skip: Int,\n    limit: Int,\n    includeDocs: Boolean,\n    since: Option[Instant],\n    upto: Option[Instant],\n    context: UserContext)(implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]] =\n    Future.successful(Right(List.empty))\n}\n\nobject NoopActivationStoreProvider extends ActivationStoreProvider {\n  override def instance(actorSystem: ActorSystem, logging: Logging) =\n    NoopActivationStore\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/mongodb/MongoDBArtifactStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging.ErrorLevel\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.stream.scaladsl._\nimport org.apache.pekko.util.ByteString\nimport com.mongodb.client.gridfs.model.GridFSUploadOptions\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, TransactionId}\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.database.StoreUtils._\nimport org.apache.openwhisk.core.entity.Attachments.Attached\nimport org.apache.openwhisk.core.entity.{DocId, DocInfo, DocRevision, DocumentReader, UUID}\nimport org.apache.openwhisk.http.Messages\nimport org.bson.json.{JsonMode, JsonWriterSettings}\nimport org.mongodb.scala.bson.BsonString\nimport org.mongodb.scala.bson.collection.immutable.Document\nimport org.mongodb.scala.gridfs.{GridFSBucket, GridFSFile, MongoGridFSException}\nimport org.mongodb.scala.model._\nimport org.mongodb.scala.{MongoClient, MongoCollection, MongoException}\nimport spray.json._\n\nimport scala.concurrent.Future\nimport scala.util.Try\n\nobject MongoDBArtifactStore {\n  val _computed = \"_computed\"\n}\n\n/**\n * Basic client to put and delete artifacts in a data store.\n *\n * @param client the mongodb client to access database\n * @param dbName the name of the database to operate on\n * @param collName the name of the collection to operate on\n * @param documentHandler helper class help to simulate the designDoc of CouchDB\n * @param viewMapper helper class help to simulate the designDoc of CouchDB\n */\nclass MongoDBArtifactStore[DocumentAbstraction <: DocumentSerializer](client: MongoClient,\n                                                                      dbName: String,\n                                                                      collName: String,\n                                                                      documentHandler: DocumentHandler,\n                                                                      viewMapper: MongoDBViewMapper,\n                                                                      val inliningConfig: InliningConfig,\n                                                                      val attachmentStore: Option[AttachmentStore])(\n  implicit system: ActorSystem,\n  val logging: Logging,\n  jsonFormat: RootJsonFormat[DocumentAbstraction],\n  docReader: DocumentReader)\n    extends ArtifactStore[DocumentAbstraction]\n    with DocumentProvider\n    with DefaultJsonProtocol\n    with AttachmentSupport[DocumentAbstraction] {\n\n  import MongoDBArtifactStore._\n\n  protected[core] implicit val executionContext = system.dispatcher\n\n  private val mongodbScheme = \"mongodb\"\n  val attachmentScheme: String = attachmentStore.map(_.scheme).getOrElse(mongodbScheme)\n\n  private val database = client.getDatabase(dbName)\n  private val collection = getCollectionAndCreateIndexes()\n  private val gridFSBucket = GridFSBucket(database, collName)\n\n  private val jsonWriteSettings = JsonWriterSettings.builder().outputMode(JsonMode.RELAXED).build\n\n  // MongoDB doesn't support using `$` as the first char of field name, so below two fields needs to be encoded first\n  private val fieldsNeedEncode = Seq(\"annotations\", \"parameters\")\n\n  override protected[database] def put(d: DocumentAbstraction)(implicit transid: TransactionId): Future[DocInfo] = {\n    val asJson = d.toDocumentRecord\n\n    val id: String = asJson.fields.getOrElse(\"_id\", JsString.empty).convertTo[String].trim\n    require(!id.isEmpty, \"document id must be defined\")\n\n    val (old_rev, rev) = revisionCalculate(asJson)\n    val docinfoStr = s\"id: $id, rev: $rev\"\n    val start =\n      transid.started(this, LoggingMarkers.DATABASE_SAVE, s\"[PUT] '$collName' saving document: '$docinfoStr'\")\n\n    val encodedData = encodeFields(fieldsNeedEncode, asJson)\n\n    val data = JsObject(\n      encodedData.fields + (_computed -> documentHandler.computedFields(asJson)) + (\"_rev\" -> rev.toJson))\n\n    val filters =\n      if (rev.startsWith(\"1-\")) {\n        // for new document, we should get no matched document and insert new one\n        // if there is a matched document, that one with no _rev field will be replaced\n        // if there is a document with the same id but has an _rev field, will return en E11000(conflict) error\n        Filters.and(Filters.eq(\"_id\", id), Filters.not(Filters.exists(\"_rev\")))\n      } else {\n        // for old document, we should find a matched document and replace it\n        // if no matched document find and try to insert new document, mongodb will return an E11000 error\n        Filters.and(Filters.eq(\"_id\", id), Filters.eq(\"_rev\", old_rev))\n      }\n\n    val f =\n      collection\n        .findOneAndReplace(\n          filters,\n          Document(data.compactPrint),\n          FindOneAndReplaceOptions().upsert(true).returnDocument(ReturnDocument.AFTER))\n        .toFuture()\n        .map { doc =>\n          transid.finished(this, start, s\"[PUT] '$collName' completed document: '$docinfoStr', document: '$doc'\")\n          DocInfo(DocId(id), DocRevision(rev))\n        }\n        .recover {\n          //  E11000 means a duplicate key error\n          case t: MongoException if t.getCode == 11000 =>\n            transid.finished(this, start, s\"[PUT] '$dbName', document: '$docinfoStr'; conflict.\")\n            throw DocumentConflictException(\"conflict on 'put'\")\n          case t: MongoException =>\n            transid.failed(\n              this,\n              start,\n              s\"[PUT] '$dbName' failed to put document: '$docinfoStr'; return error code: '${t.getCode}'\",\n              ErrorLevel)\n            throw new Exception(\"Unexpected mongodb server error: \" + t.getMessage)\n        }\n\n    reportFailure(\n      f,\n      failure =>\n        transid\n          .failed(this, start, s\"[PUT] '$collName' internal error, failure: '${failure.getMessage}'\", ErrorLevel))\n  }\n\n  override protected[database] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] = {\n    require(doc != null && doc.rev.asString != null, \"doc revision required for delete\")\n\n    val start =\n      transid.started(this, LoggingMarkers.DATABASE_DELETE, s\"[DEL] '$collName' deleting document: '$doc'\")\n\n    val f = collection\n      .deleteOne(Filters.and(Filters.eq(\"_id\", doc.id.id), Filters.eq(\"_rev\", doc.rev.rev)))\n      .toFuture()\n      .flatMap { result =>\n        if (result.getDeletedCount == 1) { // the result can only be 1 or 0\n          transid.finished(this, start, s\"[DEL] '$collName' completed document: '$doc'\")\n          Future(true)\n        } else {\n          collection.find(Filters.eq(\"_id\", doc.id.id)).toFuture.map { result =>\n            if (result.size == 1) {\n              // find the document according to _id, conflict\n              transid.finished(this, start, s\"[DEL] '$collName', document: '$doc'; conflict.\")\n              throw DocumentConflictException(\"conflict on 'delete'\")\n            } else {\n              // doesn't find the document according to _id, not found\n              transid.finished(this, start, s\"[DEL] '$collName', document: '$doc'; not found.\")\n              throw NoDocumentException(s\"$doc not found on 'delete'\")\n            }\n          }\n        }\n      }\n      .recover {\n        case t: MongoException =>\n          transid.failed(\n            this,\n            start,\n            s\"[DEL] '$collName' failed to delete document: '$doc'; error code: '${t.getCode}'\",\n            ErrorLevel)\n          throw new Exception(\"Unexpected mongodb server error: \" + t.getMessage)\n      }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(\n          this,\n          start,\n          s\"[DEL] '$collName' internal error, doc: '$doc', failure: '${failure.getMessage}'\",\n          ErrorLevel))\n  }\n\n  override protected[database] def get[A <: DocumentAbstraction](doc: DocInfo,\n                                                                 attachmentHandler: Option[(A, Attached) => A] = None)(\n    implicit transid: TransactionId,\n    ma: Manifest[A]): Future[A] = {\n\n    val start = transid.started(this, LoggingMarkers.DATABASE_GET, s\"[GET] '$dbName' finding document: '$doc'\")\n\n    require(doc != null, \"doc undefined\")\n\n    val f = collection\n      .find(Filters.eq(\"_id\", doc.id.id)) // method deserialize will check whether the _rev matched\n      .toFuture()\n      .map(result =>\n        if (result.isEmpty) {\n          transid.finished(this, start, s\"[GET] '$collName', document: '$doc'; not found.\")\n          throw NoDocumentException(\"not found on 'get'\")\n        } else {\n          transid.finished(this, start, s\"[GET] '$collName' completed: found document '$doc'\")\n          val response = result.head.toJson(jsonWriteSettings).parseJson.asJsObject\n          val decodeData = decodeFields(fieldsNeedEncode, response)\n\n          val deserializedDoc = deserialize[A, DocumentAbstraction](doc, decodeData)\n          attachmentHandler\n            .map(processAttachments(deserializedDoc, decodeData, doc.id.id, _))\n            .getOrElse(deserializedDoc)\n      })\n      .recoverWith {\n        case t: MongoException =>\n          transid.finished(this, start, s\"[GET] '$collName' failed to get document: '$doc'; error code: '${t.getCode}'\")\n          throw new Exception(\"Unexpected mongodb server error: \" + t.getMessage)\n        case _: DeserializationException => throw DocumentUnreadable(Messages.corruptedEntity)\n      }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(\n          this,\n          start,\n          s\"[GET] '$collName' internal error, doc: '$doc', failure: '${failure.getMessage}'\",\n          ErrorLevel))\n  }\n\n  override protected[database] def get(id: DocId)(implicit transid: TransactionId): Future[Option[JsObject]] = {\n    val start = transid.started(this, LoggingMarkers.DATABASE_GET, s\"[GET] '$collName' finding document: '$id'\")\n    val f = collection\n      .find(Filters.equal(\"_id\", id.id))\n      .head()\n      .map {\n        case d: Document =>\n          transid.finished(this, start, s\"[GET] '$dbName' completed: found document '$id'\")\n          Some(decodeFields(fieldsNeedEncode, d.toJson(jsonWriteSettings).parseJson.asJsObject))\n        case null =>\n          transid.finished(this, start, s\"[GET] '$dbName', document: '$id'; not found.\")\n          None\n      }\n      .recover {\n        case t: MongoException =>\n          transid.failed(\n            this,\n            start,\n            s\"[GET] '$collName' failed to get document: '$id'; error code: '${t.getCode}'\",\n            ErrorLevel)\n          throw new Exception(\"Unexpected mongodb server error: \" + t.getMessage)\n      }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(\n          this,\n          start,\n          s\"[GET] '$collName' internal error, doc: '$id', failure: '${failure.getMessage}'\",\n          ErrorLevel))\n  }\n\n  override protected[core] def query(table: String,\n                                     startKey: List[Any],\n                                     endKey: List[Any],\n                                     skip: Int,\n                                     limit: Int,\n                                     includeDocs: Boolean,\n                                     descending: Boolean,\n                                     reduce: Boolean,\n                                     stale: StaleParameter)(implicit transid: TransactionId): Future[List[JsObject]] = {\n    require(!(reduce && includeDocs), \"reduce and includeDocs cannot both be true\")\n    require(!reduce, \"Reduce scenario not supported\") //TODO Investigate reduce\n    require(skip >= 0, \"skip should be non negative\")\n    require(limit >= 0, \"limit should be non negative\")\n\n    val Array(ddoc, viewName) = table.split(\"/\")\n\n    val find = collection\n      .find(viewMapper.filter(ddoc, viewName, startKey, endKey))\n\n    viewMapper.sort(ddoc, viewName, descending).foreach(find.sort)\n\n    find.skip(skip).limit(limit)\n\n    val realIncludeDocs = includeDocs | documentHandler.shouldAlwaysIncludeDocs(ddoc, viewName)\n    val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s\"[QUERY] '$collName' searching '$table\")\n\n    val f = find\n      .toFuture()\n      .map { docs =>\n        transid.finished(this, start, s\"[QUERY] '$dbName' completed: matched ${docs.size}\")\n        docs.map { doc =>\n          val js = decodeFields(fieldsNeedEncode, doc.toJson(jsonWriteSettings).parseJson.convertTo[JsObject])\n          documentHandler.transformViewResult(\n            ddoc,\n            viewName,\n            startKey,\n            endKey,\n            realIncludeDocs,\n            JsObject(js.fields - _computed),\n            MongoDBArtifactStore.this)\n        }\n      }\n      .flatMap(Future.sequence(_))\n      .map(_.flatten.toList)\n      .recover {\n        case t: MongoException =>\n          transid.failed(this, start, s\"[QUERY] '$collName' failed; error code: '${t.getCode}'\", ErrorLevel)\n          throw new Exception(\"Unexpected mongodb server error: \" + t.getMessage)\n      }\n\n    reportFailure(\n      f,\n      failure =>\n        transid\n          .failed(this, start, s\"[QUERY] '$collName' internal error, failure: '${failure.getMessage}'\", ErrorLevel))\n  }\n\n  protected[core] def count(table: String, startKey: List[Any], endKey: List[Any], skip: Int, stale: StaleParameter)(\n    implicit transid: TransactionId): Future[Long] = {\n    require(skip >= 0, \"skip should be non negative\")\n\n    val Array(ddoc, viewName) = table.split(\"/\")\n    val start = transid.started(this, LoggingMarkers.DATABASE_QUERY, s\"[COUNT] '$dbName' searching '$table\")\n\n    val query = viewMapper.filter(ddoc, viewName, startKey, endKey)\n\n    val option = CountOptions().skip(skip)\n    val f =\n      collection\n        .countDocuments(query, option)\n        .toFuture()\n        .map { result =>\n          transid.finished(this, start, s\"[COUNT] '$collName' completed: count $result\")\n          result\n        }\n        .recover {\n          case t: MongoException =>\n            transid.failed(this, start, s\"[COUNT] '$collName' failed; error code: '${t.getCode}'\", ErrorLevel)\n            throw new Exception(\"Unexpected mongodb server error: \" + t.getMessage)\n        }\n\n    reportFailure(\n      f,\n      failure =>\n        transid\n          .failed(this, start, s\"[COUNT] '$dbName' internal error, failure: '${failure.getMessage}'\", ErrorLevel))\n  }\n\n  override protected[database] def putAndAttach[A <: DocumentAbstraction](\n    doc: A,\n    update: (A, Attached) => A,\n    contentType: ContentType,\n    docStream: Source[ByteString, _],\n    oldAttachment: Option[Attached])(implicit transid: TransactionId): Future[(DocInfo, Attached)] = {\n\n    attachmentStore match {\n      case Some(as) =>\n        attachToExternalStore(doc, update, contentType, docStream, oldAttachment, as)\n      case None =>\n        attachToMongo(doc, update, contentType, docStream, oldAttachment)\n    }\n\n  }\n\n  private def attachToMongo[A <: DocumentAbstraction](\n    doc: A,\n    update: (A, Attached) => A,\n    contentType: ContentType,\n    docStream: Source[ByteString, _],\n    oldAttachment: Option[Attached])(implicit transid: TransactionId): Future[(DocInfo, Attached)] = {\n\n    for {\n      bytesOrSource <- inlineOrAttach(docStream)\n      uri = uriOf(bytesOrSource, UUID().asString)\n      attached <- {\n        bytesOrSource match {\n          case Left(bytes) =>\n            Future.successful(Attached(uri.toString, contentType, Some(bytes.size), Some(digest(bytes))))\n          case Right(source) =>\n            attach(doc, uri.path.toString, contentType, source).map { r =>\n              Attached(uri.toString, contentType, Some(r.length), Some(r.digest))\n            }\n        }\n      }\n      docInfo <- put(update(doc, attached))\n\n      //Remove old attachment if it was part of attachmentStore\n      _ <- oldAttachment\n        .map { old =>\n          val oldUri = Uri(old.attachmentName)\n          if (oldUri.scheme == mongodbScheme) {\n            val name = oldUri.path.toString\n            gridFSBucket.delete(BsonString(s\"${docInfo.id.id}/$name\")).toFuture.map { _ =>\n              true\n            }\n          } else {\n            Future.successful(true)\n          }\n        }\n        .getOrElse(Future.successful(true))\n    } yield (docInfo, attached)\n  }\n\n  private def attach(d: DocumentAbstraction, name: String, contentType: ContentType, docStream: Source[ByteString, _])(\n    implicit transid: TransactionId): Future[AttachResult] = {\n\n    logging.info(this, s\"Uploading attach $name\")\n    val asJson = d.toDocumentRecord\n    val id: String = asJson.fields(\"_id\").convertTo[String].trim\n    require(!id.isEmpty, \"document id must be defined\")\n\n    val start = transid.started(\n      this,\n      LoggingMarkers.DATABASE_ATT_SAVE,\n      s\"[ATT_PUT] '$collName' uploading attachment '$name' of document 'id: $id'\")\n\n    val document: org.bson.Document = new org.bson.Document(\"contentType\", contentType.toString)\n    //add the document id to the metadata\n    document.append(\"belongsTo\", id)\n\n    val option = new GridFSUploadOptions().metadata(document)\n\n    val uploadStream = gridFSBucket.openUploadStream(BsonString(s\"$id/$name\"), name, option)\n    val sink = MongoDBAsyncStreamSink(uploadStream)\n\n    val f = docStream\n      .runWith(combinedSink(sink))\n      .map { r =>\n        transid\n          .finished(this, start, s\"[ATT_PUT] '$collName' completed uploading attachment '$name' of document '$id'\")\n        AttachResult(r.digest, r.length)\n      }\n      .recover {\n        case t: MongoException =>\n          transid.failed(\n            this,\n            start,\n            s\"[ATT_PUT] '$collName' failed to upload attachment '$name' of document '$id'; error code '${t.getCode}'\",\n            ErrorLevel)\n          throw new Exception(\"Unexpected mongodb server error: \" + t.getMessage)\n      }\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(\n          this,\n          start,\n          s\"[ATT_PUT] '$collName' internal error, name: '$name', doc: '$id', failure: '${failure.getMessage}'\",\n          ErrorLevel))\n  }\n\n  override protected[core] def readAttachment[T](doc: DocInfo, attached: Attached, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T] = {\n\n    val name = attached.attachmentName\n    val attachmentUri = Uri(name)\n\n    attachmentUri.scheme match {\n      case AttachmentSupport.MemScheme =>\n        memorySource(attachmentUri).runWith(sink)\n      case s if s == mongodbScheme || attachmentUri.isRelative =>\n        //relative case is for compatibility with earlier naming approach where attachment name would be like 'jarfile'\n        //Compared to current approach of '<scheme>:<name>'\n        readAttachmentFromMongo(doc, attachmentUri, sink)\n      case s if attachmentStore.isDefined && attachmentStore.get.scheme == s =>\n        attachmentStore.get.readAttachment(doc.id, attachmentUri.path.toString, sink)\n      case _ =>\n        throw new IllegalArgumentException(s\"Unknown attachment scheme in attachment uri $attachmentUri\")\n    }\n  }\n\n  private def readAttachmentFromMongo[T](doc: DocInfo, attachmentUri: Uri, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T] = {\n\n    val attachmentName = attachmentUri.path.toString\n    val start = transid.started(\n      this,\n      LoggingMarkers.DATABASE_ATT_GET,\n      s\"[ATT_GET] '$dbName' finding attachment '$attachmentName' of document '$doc'\")\n\n    require(doc != null, \"doc undefined\")\n    require(doc.rev.rev != null, \"doc revision must be specified\")\n\n    val downloadStream = gridFSBucket.openDownloadStream(BsonString(s\"${doc.id.id}/$attachmentName\"))\n\n    def readStream(file: GridFSFile) = {\n      val source = MongoDBAsyncStreamSource(downloadStream)\n      source\n        .runWith(sink)\n        .map { result =>\n          transid\n            .finished(\n              this,\n              start,\n              s\"[ATT_GET] '$collName' completed: found attachment '$attachmentName' of document '$doc'\")\n          result\n        }\n    }\n\n    def getGridFSFile = {\n      downloadStream\n        .gridFSFile()\n        .head()\n        .transform(\n          identity, {\n            case ex: MongoGridFSException if ex.getMessage.contains(\"File not found\") =>\n              transid.finished(\n                this,\n                start,\n                s\"[ATT_GET] '$collName', retrieving attachment '$attachmentName' of document '$doc'; not found.\")\n              NoDocumentException(\"Not found on 'readAttachment'.\")\n            case ex: MongoGridFSException =>\n              transid.failed(\n                this,\n                start,\n                s\"[ATT_GET] '$collName' failed to get attachment '$attachmentName' of document '$doc'; error code: '${ex.getCode}'\",\n                ErrorLevel)\n              throw new Exception(\"Unexpected mongodb server error: \" + ex.getMessage)\n            case t => t\n          })\n    }\n\n    val f = for {\n      file <- getGridFSFile\n      result <- readStream(file)\n    } yield result\n\n    reportFailure(\n      f,\n      failure =>\n        transid.failed(\n          this,\n          start,\n          s\"[ATT_GET] '$dbName' internal error, name: '$attachmentName', doc: '$doc', failure: '${failure.getMessage}'\",\n          ErrorLevel))\n\n  }\n\n  override protected[core] def deleteAttachments[T](doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] =\n    attachmentStore\n      .map(as => as.deleteAttachments(doc.id))\n      .getOrElse(Future.successful(true)) // For MongoDB it is expected that the entire document is deleted.\n\n  override def shutdown(): Unit = {\n    // MongoClient maintains the connection pool internally, we don't need to manage it\n    attachmentStore.foreach(_.shutdown())\n  }\n\n  private def reportFailure[T, U](f: Future[T], onFailure: Throwable => U): Future[T] = {\n    f.failed.foreach {\n      case _: ArtifactStoreException => // These failures are intentional and shouldn't trigger the catcher.\n      case x                         => onFailure(x)\n    }\n    f\n  }\n\n  // calculate the revision manually, to be compatible with couchdb's _rev field\n  private def revisionCalculate(doc: JsObject): (String, String) = {\n    val md = StoreUtils.emptyDigest()\n    val new_rev =\n      md.digest(doc.toString.getBytes()).map(0xFF & _).map { \"%02x\".format(_) }.foldLeft(\"\") { _ + _ }.take(32)\n    doc.fields\n      .get(\"_rev\")\n      .map { value =>\n        val start = value.convertTo[String].trim.split(\"-\").apply(0).toInt + 1\n        (value.convertTo[String].trim, s\"$start-$new_rev\")\n      }\n      .getOrElse {\n        (\"\", s\"1-$new_rev\")\n      }\n  }\n\n  private def processAttachments[A <: DocumentAbstraction](doc: A,\n                                                           js: JsObject,\n                                                           docId: String,\n                                                           attachmentHandler: (A, Attached) => A): A = {\n    js.fields(\"exec\").asJsObject().fields.get(\"code\").map {\n      case code: JsObject =>\n        code.getFields(\"attachmentName\", \"attachmentType\", \"digest\", \"length\") match {\n          case Seq(JsString(name), JsString(contentTypeValue), JsString(digest), JsNumber(length)) =>\n            val contentType = ContentType.parse(contentTypeValue) match {\n              case Right(ct) => ct\n              case Left(_)   => ContentTypes.NoContentType //Should not happen\n            }\n            attachmentHandler(doc, Attached(getAttachmentName(name), contentType, Some(length.longValue), Some(digest)))\n          case x =>\n            throw DeserializationException(\"Attachment json does not have required fields\" + x)\n        }\n      case _ => doc\n    } getOrElse {\n      doc // This should not happen as an action always contain field: exec.code\n    }\n  }\n\n  /**\n   * Determines if the attachment scheme confirms to new UUID based scheme or not\n   * and generates the name based on that\n   */\n  private def getAttachmentName(name: String): String = {\n    Try(java.util.UUID.fromString(name))\n      .map(_ => Uri.from(scheme = attachmentScheme, path = name).toString)\n      .getOrElse(name)\n  }\n\n  private def getCollectionAndCreateIndexes(): MongoCollection[Document] = {\n    val coll = database.getCollection(collName)\n    // create indexes in specific collection if they do not exist\n    coll.listIndexes().toFuture().map { idxes =>\n      val keys = idxes.map {\n        _.get(\"key\").map { fields =>\n          Document(fields.asDocument())\n        } getOrElse {\n          Document.empty // this should not happen\n        }\n      }\n\n      viewMapper.indexes.foreach { idx =>\n        if (!keys.contains(idx))\n          coll.createIndex(idx).toFuture\n      }\n    }\n    coll\n  }\n\n  // encode JsValue which has complex and arbitrary structure to JsString\n  private def encodeFields(fields: Seq[String], jsValue: JsObject): JsObject = {\n    var data = jsValue.fields\n    fields.foreach { field =>\n      data.get(field).foreach { value =>\n        data = data.updated(field, JsString(value.compactPrint))\n      }\n    }\n    JsObject(data)\n  }\n\n  // decode fields from JsString\n  private def decodeFields(fields: Seq[String], jsValue: JsObject): JsObject = {\n    var data = jsValue.fields\n    fields.foreach { field =>\n      data.get(field).foreach { value =>\n        Try {\n          data = data.updated(field, value.asInstanceOf[JsString].value.parseJson)\n        }\n      }\n    }\n    JsObject(data)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/mongodb/MongoDBArtifactStoreProvider.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.{DocumentReader, WhiskActivation, WhiskAuth, WhiskEntity}\nimport org.mongodb.scala.MongoClient\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json.RootJsonFormat\n\nimport scala.reflect.ClassTag\n\ncase class MongoDBConfig(uri: String, database: String) {\n  assume(Set(database, uri).forall(_.trim.nonEmpty), \"At least one expected property is missing\")\n\n  def collectionFor[D](implicit tag: ClassTag[D]) = tag.runtimeClass.getSimpleName.toLowerCase\n}\n\nobject MongoDBClient {\n  private var _clientWithConfig: Option[(MongoClient, MongoDBConfig)] = None\n\n  def client(config: MongoDBConfig): MongoClient = {\n    _clientWithConfig match {\n      case Some((client, oldConfig)) if oldConfig == config =>\n        client\n      case _ =>\n        val client = MongoClient(config.uri)\n        _clientWithConfig = Some(client, config)\n        client\n    }\n  }\n}\n\nobject MongoDBArtifactStoreProvider extends ArtifactStoreProvider {\n\n  def makeStore[D <: DocumentSerializer: ClassTag](useBatching: Boolean)(implicit jsonFormat: RootJsonFormat[D],\n                                                                         docReader: DocumentReader,\n                                                                         actorSystem: ActorSystem,\n                                                                         logging: Logging): ArtifactStore[D] = {\n    val dbConfig = loadConfigOrThrow[MongoDBConfig](ConfigKeys.mongodb)\n    makeArtifactStore(dbConfig, getAttachmentStore())\n  }\n\n  def makeArtifactStore[D <: DocumentSerializer: ClassTag](dbConfig: MongoDBConfig,\n                                                           attachmentStore: Option[AttachmentStore])(\n    implicit jsonFormat: RootJsonFormat[D],\n    docReader: DocumentReader,\n    actorSystem: ActorSystem,\n    logging: Logging): ArtifactStore[D] = {\n\n    val inliningConfig = loadConfigOrThrow[InliningConfig](ConfigKeys.db)\n\n    val (handler, mapper) = handlerAndMapper(implicitly[ClassTag[D]])\n\n    new MongoDBArtifactStore[D](\n      MongoDBClient.client(dbConfig),\n      dbConfig.database,\n      dbConfig.collectionFor[D],\n      handler,\n      mapper,\n      inliningConfig,\n      attachmentStore)\n  }\n\n  private def handlerAndMapper[D](entityType: ClassTag[D])(implicit actorSystem: ActorSystem,\n                                                           logging: Logging): (DocumentHandler, MongoDBViewMapper) = {\n    entityType.runtimeClass match {\n      case x if x == classOf[WhiskEntity] =>\n        (WhisksHandler, WhisksViewMapper)\n      case x if x == classOf[WhiskActivation] =>\n        (ActivationHandler, ActivationViewMapper)\n      case x if x == classOf[WhiskAuth] =>\n        (SubjectHandler, SubjectViewMapper)\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/mongodb/MongoDBAsyncStreamSink.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport java.nio.ByteBuffer\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.stream.{Attributes, IOResult, Inlet, SinkShape}\nimport org.apache.pekko.stream.scaladsl.Sink\nimport org.apache.pekko.stream.stage.{AsyncCallback, GraphStageLogic, GraphStageWithMaterializedValue, InHandler}\nimport org.apache.pekko.util.ByteString\nimport org.mongodb.scala.Completed\nimport org.mongodb.scala.gridfs.{AsyncOutputStream}\n\nimport scala.concurrent.{ExecutionContext, Future, Promise}\nimport scala.util.{Failure, Success, Try}\n\nclass MongoDBAsyncStreamSink(stream: AsyncOutputStream)(implicit ec: ExecutionContext)\n    extends GraphStageWithMaterializedValue[SinkShape[ByteString], Future[IOResult]] {\n  val in: Inlet[ByteString] = Inlet(\"AsyncStream.in\")\n\n  override val shape: SinkShape[ByteString] = SinkShape(in)\n\n  override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[IOResult]) = {\n    val ioResultPromise = Promise[IOResult]()\n    val logic = new GraphStageLogic(shape) with InHandler {\n      handler =>\n      var buffers: Iterator[ByteBuffer] = Iterator()\n      var writeCallback: AsyncCallback[Try[Int]] = _\n      var closeCallback: AsyncCallback[Try[Completed]] = _\n      var position: Int = _\n      var writeDone = Promise[Completed]\n\n      setHandler(in, this)\n\n      override def preStart(): Unit = {\n        //close operation is async and thus requires the stage to remain open\n        //even after all data is read\n        setKeepGoing(true)\n        writeCallback = getAsyncCallback[Try[Int]](handleWriteResult)\n        closeCallback = getAsyncCallback[Try[Completed]](handleClose)\n        pull(in)\n      }\n\n      override def onPush(): Unit = {\n        buffers = grab(in).asByteBuffers.iterator\n        writeDone = Promise[Completed]\n        writeNextBufferOrPull()\n      }\n\n      override def onUpstreamFinish(): Unit = {\n        //Work done perform close\n        //Using async \"blessed\" callback does not work at this stage so\n        // need to invoke as normal callback\n        //TODO Revisit this\n\n        //write of ByteBuffers from ByteString is an async operation. For last push\n        //the write operation may involve multiple async callbacks and by that time\n        //onUpstreamFinish may get invoked. So to ensure that close operation is performed\n        //\"after\" the last push writes are done we rely on writeDone promise\n        //and schedule the close on its completion\n        writeDone.future.onComplete(_ => stream.close().head().onComplete(handleClose))\n      }\n\n      override def onUpstreamFailure(ex: Throwable): Unit = {\n        fail(ex)\n      }\n\n      private def handleWriteResult(bytesWrittenOrFailure: Try[Int]): Unit = bytesWrittenOrFailure match {\n        case Success(bytesWritten) =>\n          position += bytesWritten\n          writeNextBufferOrPull()\n        case Failure(failure) => fail(failure)\n      }\n\n      private def handleClose(completed: Try[Completed]): Unit = completed match {\n        case Success(Completed()) =>\n          completeStage()\n          ioResultPromise.trySuccess(IOResult(position, Success(Done)))\n        case Failure(failure) =>\n          fail(failure)\n      }\n\n      private def writeNextBufferOrPull(): Unit = {\n        if (buffers.hasNext) {\n          stream.write(buffers.next()).head().onComplete(writeCallback.invoke)\n        } else {\n          writeDone.trySuccess(Completed())\n          pull(in)\n        }\n      }\n\n      private def fail(failure: Throwable) = {\n        failStage(failure)\n        ioResultPromise.trySuccess(IOResult(position, Failure(failure)))\n      }\n\n    }\n    (logic, ioResultPromise.future)\n  }\n}\n\nobject MongoDBAsyncStreamSink {\n  def apply(stream: AsyncOutputStream)(implicit ec: ExecutionContext): Sink[ByteString, Future[IOResult]] = {\n    Sink.fromGraph(new MongoDBAsyncStreamSink(stream))\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/mongodb/MongoDBAsyncStreamSource.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport java.nio.ByteBuffer\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.stream.SourceShape\nimport org.apache.pekko.stream.Attributes\nimport org.apache.pekko.stream.Outlet\nimport org.apache.pekko.stream.IOResult\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.stream.stage.GraphStageLogic\nimport org.apache.pekko.stream.stage.OutHandler\nimport org.apache.pekko.stream.stage.GraphStageWithMaterializedValue\nimport org.apache.pekko.stream.stage.AsyncCallback\nimport org.apache.pekko.util.ByteString\nimport org.mongodb.scala.Completed\nimport org.mongodb.scala.gridfs.AsyncInputStream\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.concurrent.Promise\nimport scala.util.Success\nimport scala.util.Try\nimport scala.util.Failure\n\nclass MongoDBAsyncStreamSource(stream: AsyncInputStream, chunkSize: Int)(implicit ec: ExecutionContext)\n    extends GraphStageWithMaterializedValue[SourceShape[ByteString], Future[IOResult]] {\n  require(chunkSize > 0, \"chunkSize must be greater than 0\")\n  val out: Outlet[ByteString] = Outlet(\"AsyncStream.out\")\n\n  override val shape: SourceShape[ByteString] = SourceShape(out)\n\n  override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[IOResult]) = {\n    val ioResultPromise = Promise[IOResult]()\n    val logic = new GraphStageLogic(shape) with OutHandler {\n      handler =>\n      val buffer = ByteBuffer.allocate(chunkSize)\n      var readCallback: AsyncCallback[Try[Int]] = _\n      var closeCallback: AsyncCallback[Try[Completed]] = _\n      var position: Int = _\n\n      setHandler(out, this)\n\n      override def preStart(): Unit = {\n        readCallback = getAsyncCallback[Try[Int]](handleBufferRead)\n        closeCallback = getAsyncCallback[Try[Completed]](handleClose)\n      }\n\n      override def onPull(): Unit = {\n        stream.read(buffer).head().onComplete(readCallback.invoke)\n      }\n\n      private def handleBufferRead(bytesReadOrFailure: Try[Int]): Unit = bytesReadOrFailure match {\n        case Success(bytesRead) if bytesRead >= 0 =>\n          buffer.flip\n          push(out, ByteString.fromByteBuffer(buffer))\n          buffer.clear\n          position += bytesRead\n        case Success(_) =>\n          stream.close().head().onComplete(closeCallback.invoke) //Work done perform close\n        case Failure(failure) =>\n          fail(failure)\n      }\n\n      private def handleClose(completed: Try[Completed]): Unit = completed match {\n        case Success(Completed()) =>\n          completeStage()\n          ioResultPromise.trySuccess(IOResult(position, Success(Done)))\n        case Failure(failure) =>\n          fail(failure)\n      }\n\n      private def fail(failure: Throwable) = {\n        failStage(failure)\n        ioResultPromise.trySuccess(IOResult(position, Failure(failure)))\n      }\n    }\n    (logic, ioResultPromise.future)\n  }\n}\n\nobject MongoDBAsyncStreamSource {\n  def apply(stream: AsyncInputStream, chunkSize: Int = 512 * 1024)(\n    implicit ec: ExecutionContext): Source[ByteString, Future[IOResult]] = {\n    Source.fromGraph(new MongoDBAsyncStreamSource(stream, chunkSize))\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/mongodb/MongoDBViewMapper.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity.WhiskQueries\nimport org.mongodb.scala.Document\nimport org.mongodb.scala.bson.conversions.Bson\nimport org.mongodb.scala.model.Filters._\nimport org.mongodb.scala.model.Sorts\n\ntrait MongoDBViewMapper {\n  protected val _computed: String = \"_computed\"\n  protected val TOP: String = WhiskQueries.TOP\n\n  val indexes: List[Document]\n\n  def filter(ddoc: String, view: String, startKey: List[Any], endKey: List[Any]): Bson\n\n  def sort(ddoc: String, view: String, descending: Boolean): Option[Bson]\n\n  protected def checkKeys(startKey: List[Any], endKey: List[Any]): Unit = {\n    require(startKey.nonEmpty)\n    require(endKey.nonEmpty)\n    require(startKey.head == endKey.head, s\"First key should be same => ($startKey) - ($endKey)\")\n  }\n}\n\nprivate object ActivationViewMapper extends MongoDBViewMapper {\n  private val NS = \"namespace\"\n  private val NS_WITH_PATH = s\"${_computed}.${ActivationHandler.NS_PATH}\"\n  private val START = \"start\"\n  override val indexes: List[Document] =\n    List(\n      Document(s\"$START\" -> -1),\n      Document(s\"$START\" -> -1, s\"$NS\" -> -1),\n      Document(s\"$NS_WITH_PATH\" -> -1, s\"$START\" -> -1))\n\n  override def filter(ddoc: String, view: String, startKey: List[Any], endKey: List[Any]): Bson = {\n    checkKeys(startKey, endKey)\n    view match {\n      //whisks-filters ddoc uses namespace + invoking action path as first key\n      case \"activations\" if ddoc.startsWith(\"whisks-filters\") => createActivationFilter(NS_WITH_PATH, startKey, endKey)\n      //whisks ddoc uses namespace as first key\n      case \"activations\" if ddoc.startsWith(\"whisks\") => createActivationFilter(NS, startKey, endKey)\n      case _                                          => throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  override def sort(ddoc: String, view: String, descending: Boolean): Option[Bson] = {\n    view match {\n      case \"activations\" if ddoc.startsWith(\"whisks-filters\") =>\n        val sort = if (descending) Sorts.descending(NS_WITH_PATH, START) else Sorts.ascending(NS_WITH_PATH, START)\n        Some(sort)\n      case \"activations\" if ddoc.startsWith(\"whisks\") =>\n        val sort = if (descending) Sorts.descending(NS, START) else Sorts.ascending(NS, START)\n        Some(sort)\n      case _ => throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  private def createActivationFilter(nsPropName: String, startKey: List[Any], endKey: List[Any]) = {\n    require(startKey.head.isInstanceOf[String])\n    val matchNS = equal(nsPropName, startKey.head)\n\n    val filter = (startKey, endKey) match {\n      case (_ :: Nil, _ :: `TOP` :: Nil) =>\n        matchNS\n      case (_ :: since :: Nil, _ :: `TOP` :: `TOP` :: Nil) =>\n        and(matchNS, gte(START, since))\n      case (_ :: since :: Nil, _ :: upto :: `TOP` :: Nil) =>\n        and(matchNS, gte(START, since), lte(START, upto))\n      case _ => throw UnsupportedQueryKeys(s\"$startKey, $endKey\")\n    }\n    filter\n  }\n}\n\nprivate object WhisksViewMapper extends MongoDBViewMapper {\n  private val NS = \"namespace\"\n  private val ROOT_NS = s\"${_computed}.${WhisksHandler.ROOT_NS}\"\n  private val TYPE = \"entityType\"\n  private val UPDATED = \"updated\"\n  private val PUBLISH = \"publish\"\n  private val BINDING = \"binding\"\n  override val indexes: List[Document] =\n    List(Document(s\"$NS\" -> -1, s\"$UPDATED\" -> -1), Document(s\"$ROOT_NS\" -> -1, s\"$UPDATED\" -> -1))\n\n  override def filter(ddoc: String, view: String, startKey: List[Any], endKey: List[Any]): Bson = {\n    checkKeys(startKey, endKey)\n    view match {\n      case \"all\" => listAllInNamespace(ddoc, view, startKey, endKey)\n      case _     => listCollectionInNamespace(ddoc, view, startKey, endKey)\n    }\n  }\n\n  private def listCollectionInNamespace(ddoc: String, view: String, startKey: List[Any], endKey: List[Any]): Bson = {\n\n    val entityType = getEntityType(ddoc, view)\n\n    val matchType = equal(TYPE, entityType)\n    val matchNS = equal(NS, startKey.head)\n    val matchRootNS = equal(ROOT_NS, startKey.head)\n\n    val filter = (startKey, endKey) match {\n      case (ns :: Nil, _ :: `TOP` :: Nil) =>\n        or(and(matchType, matchNS), and(matchType, matchRootNS))\n      case (ns :: since :: Nil, _ :: `TOP` :: `TOP` :: Nil) =>\n        // @formatter:off\n                or(\n                    and(matchType, matchNS, gte(UPDATED, since)),\n                    and(matchType, matchRootNS, gte(UPDATED, since))\n                )\n            // @formatter:on\n      case (ns :: since :: Nil, _ :: upto :: `TOP` :: Nil) =>\n        or(\n          and(matchType, matchNS, gte(UPDATED, since), lte(UPDATED, upto)),\n          and(matchType, matchRootNS, gte(UPDATED, since), lte(UPDATED, upto)))\n      case _ => throw UnsupportedQueryKeys(s\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n    if (view == \"packages-public\")\n      and(equal(BINDING, Map.empty), equal(PUBLISH, true), filter)\n    else\n      filter\n  }\n\n  private def listAllInNamespace(ddoc: String, view: String, startKey: List[Any], endKey: List[Any]): Bson = {\n    val matchRootNS = equal(ROOT_NS, startKey.head)\n    val filter = (startKey, endKey) match {\n      case (ns :: Nil, _ :: `TOP` :: Nil) =>\n        and(exists(TYPE), matchRootNS)\n      case _ => throw UnsupportedQueryKeys(s\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n    filter\n  }\n\n  override def sort(ddoc: String, view: String, descending: Boolean): Option[Bson] = {\n    view match {\n      case \"actions\" | \"rules\" | \"triggers\" | \"packages\" | \"packages-public\" | \"all\"\n          if ddoc.startsWith(\"whisks\") || ddoc.startsWith(\"all-whisks\") =>\n        val sort = if (descending) Sorts.descending(UPDATED) else Sorts.ascending(UPDATED)\n        Some(sort)\n      case _ => throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  private def getEntityType(ddoc: String, view: String): String = view match {\n    case \"actions\"                      => \"action\"\n    case \"rules\"                        => \"rule\"\n    case \"triggers\"                     => \"trigger\"\n    case \"packages\" | \"packages-public\" => \"package\"\n    case _                              => throw UnsupportedView(s\"$ddoc/$view\")\n  }\n}\nprivate object SubjectViewMapper extends MongoDBViewMapper {\n  private val BLOCKED = \"blocked\"\n  private val SUBJECT = \"subject\"\n  private val UUID = \"uuid\"\n  private val KEY = \"key\"\n  private val NS_NAME = \"namespaces.name\"\n  private val NS_UUID = \"namespaces.uuid\"\n  private val NS_KEY = \"namespaces.key\"\n  private val CONCURRENT_INVOCATIONS = \"concurrentInvocations\"\n  private val INVOCATIONS_PERMINUTE = \"invocationsPerMinute\"\n  override val indexes: List[Document] =\n    List(Document(s\"$NS_NAME\" -> -1), Document(s\"$NS_UUID\" -> -1, s\"$NS_KEY\" -> -1))\n\n  override def filter(ddoc: String, view: String, startKey: List[Any], endKey: List[Any]): Bson = {\n    require(startKey == endKey, s\"startKey: $startKey and endKey: $endKey must be same for $ddoc/$view\")\n    (ddoc, view) match {\n      case (s, \"identities\") if s.startsWith(\"subjects\") =>\n        filterForMatchingSubjectOrNamespace(ddoc, view, startKey, endKey)\n      case (\"namespaceThrottlings\", \"blockedNamespaces\") =>\n        or(equal(BLOCKED, true), equal(CONCURRENT_INVOCATIONS, 0), equal(INVOCATIONS_PERMINUTE, 0))\n      case _ =>\n        throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  override def sort(ddoc: String, view: String, descending: Boolean): Option[Bson] = {\n    (ddoc, view) match {\n      case (s, \"identities\") if s.startsWith(\"subjects\") => None\n      case (\"namespaceThrottlings\", \"blockedNamespaces\") => None\n      case _ =>\n        throw UnsupportedView(s\"$ddoc/$view\")\n    }\n  }\n\n  private def filterForMatchingSubjectOrNamespace(ddoc: String,\n                                                  view: String,\n                                                  startKey: List[Any],\n                                                  endKey: List[Any]): Bson = {\n    val notBlocked = notEqual(BLOCKED, true)\n    startKey match {\n      case (ns: String) :: Nil                    => and(notBlocked, or(equal(SUBJECT, ns), equal(NS_NAME, ns)))\n      case (uuid: String) :: (key: String) :: Nil =>\n        // @formatter:off\n        and(\n          notBlocked,\n          or(\n            and(equal(UUID, uuid), equal(KEY, key)),\n            and(equal(NS_UUID, uuid), equal(NS_KEY, key))\n          ))\n      // @formatter:on\n      case _ => throw UnsupportedQueryKeys(s\"$ddoc/$view -> ($startKey, $endKey)\")\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/s3/CloudFrontSigner.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\nimport java.io.ByteArrayInputStream\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.security.PrivateKey\nimport java.time.Instant\nimport java.util.Date\n\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport com.amazonaws.auth.PEM\nimport com.amazonaws.services.cloudfront.CloudFrontUrlSigner\nimport com.amazonaws.services.cloudfront.util.SignerUtils\nimport com.amazonaws.services.cloudfront.util.SignerUtils.Protocol\n\nimport scala.concurrent.duration._\n\ncase class CloudFrontConfig(domainName: String,\n                            keyPairId: String,\n                            privateKey: String,\n                            timeout: FiniteDuration = 10.minutes)\n\ncase class CloudFrontSigner(config: CloudFrontConfig) extends UrlSigner {\n  private val privateKey = createPrivateKey(config.privateKey)\n\n  override def getSignedURL(s3ObjectKey: String): Uri = {\n    val resourcePath = SignerUtils.generateResourcePath(Protocol.https, config.domainName, s3ObjectKey)\n    val date = Date.from(Instant.now().plusSeconds(config.timeout.toSeconds))\n    val url = CloudFrontUrlSigner.getSignedURLWithCannedPolicy(resourcePath, config.keyPairId, privateKey, date)\n    Uri(url)\n  }\n\n  override def toString: String = s\"CloudFront Signer - ${config.domainName}\"\n\n  private def createPrivateKey(keyContent: String): PrivateKey = {\n    val is = new ByteArrayInputStream(keyContent.getBytes(UTF_8))\n    PEM.readPrivateKey(is)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/database/s3/S3AttachmentStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.model.headers.CacheDirectives._\nimport org.apache.pekko.http.scaladsl.model.headers._\nimport org.apache.pekko.http.scaladsl.model.{ContentType, HttpRequest, HttpResponse, Uri}\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport org.apache.pekko.stream.connectors.s3.headers.CannedAcl\nimport org.apache.pekko.stream.connectors.s3.scaladsl.S3\nimport org.apache.pekko.stream.connectors.s3.{S3Attributes, S3Exception, S3Headers, S3Settings}\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.util.ByteString\nimport com.typesafe.config.Config\nimport org.apache.openwhisk.common.LoggingMarkers.{\n  DATABASE_ATTS_DELETE,\n  DATABASE_ATT_DELETE,\n  DATABASE_ATT_GET,\n  DATABASE_ATT_SAVE\n}\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.StoreUtils._\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity.DocId\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.reflect.ClassTag\n\nobject S3AttachmentStoreProvider extends AttachmentStoreProvider {\n  val pekkoConnectorsConfigKey = s\"${ConfigKeys.s3}.pekko-connectors\"\n  case class S3Config(bucket: String, prefix: Option[String], cloudFrontConfig: Option[CloudFrontConfig] = None) {\n    def prefixFor[D](implicit tag: ClassTag[D]): String = {\n      val className = tag.runtimeClass.getSimpleName.toLowerCase\n      prefix.map(p => s\"$p/$className\").getOrElse(className)\n    }\n\n    def signer: Option[UrlSigner] = cloudFrontConfig.map(CloudFrontSigner)\n  }\n\n  override def makeStore[D <: DocumentSerializer: ClassTag]()(implicit actorSystem: ActorSystem,\n                                                              logging: Logging): AttachmentStore = {\n    val config = loadConfigOrThrow[S3Config](ConfigKeys.s3)\n    new S3AttachmentStore(s3Settings(actorSystem.settings.config), config.bucket, config.prefixFor[D], config.signer)\n  }\n\n  def makeStore[D <: DocumentSerializer: ClassTag](config: Config)(implicit actorSystem: ActorSystem,\n                                                                   logging: Logging): AttachmentStore = {\n    val s3config = loadConfigOrThrow[S3Config](config, ConfigKeys.s3)\n    new S3AttachmentStore(s3Settings(config), s3config.bucket, s3config.prefixFor[D], s3config.signer)\n  }\n\n  private def s3Settings(config: Config) = S3Settings(config.getConfig(pekkoConnectorsConfigKey))\n\n}\n\ntrait UrlSigner {\n  def getSignedURL(s3ObjectKey: String): Uri\n}\n\nclass S3AttachmentStore(s3Settings: S3Settings, bucket: String, prefix: String, urlSigner: Option[UrlSigner])(\n  implicit system: ActorSystem,\n  logging: Logging)\n    extends AttachmentStore {\n\n  private val s3attributes = S3Attributes.settings(s3Settings)\n  private val commonS3Headers = {\n    val cache = `Cache-Control`(`max-age`(365.days.toSeconds))\n    S3Headers()\n      .withCannedAcl(CannedAcl.Private) //All contents are private\n      .withCustomHeaders(Map(cache.name -> cache.value)) //As objects are immutable cache them for long time\n  }\n  override val scheme = \"s3\"\n\n  override protected[core] implicit val executionContext: ExecutionContext = system.dispatcher\n\n  logging.info(this, s\"Initializing S3AttachmentStore with bucket=[$bucket], prefix=[$prefix], signer=[$urlSigner]\")\n\n  override protected[core] def attach(\n    docId: DocId,\n    name: String,\n    contentType: ContentType,\n    docStream: Source[ByteString, _])(implicit transid: TransactionId): Future[AttachResult] = {\n    require(name != null, \"name undefined\")\n    val start =\n      transid.started(this, DATABASE_ATT_SAVE, s\"[ATT_PUT] uploading attachment '$name' of document 'id: $docId'\")\n\n    //A possible optimization for small attachments < 5MB can be to use putObject instead of multipartUpload\n    //and thus use 1 remote call instead of 3\n    val f = docStream\n      .runWith(\n        combinedSink(\n          S3.multipartUploadWithHeaders(bucket, objectKey(docId, name), contentType, s3Headers = commonS3Headers)\n            .withAttributes(s3attributes)))\n      .map(r => AttachResult(r.digest, r.length))\n\n    f.foreach(_ =>\n      transid\n        .finished(this, start, s\"[ATT_PUT] '$prefix' completed uploading attachment '$name' of document 'id: $docId'\"))\n\n    reportFailure(\n      f,\n      start,\n      failure => s\"[ATT_PUT] '$prefix' internal error, name: '$name', doc: '$docId', failure: '${failure.getMessage}'\")\n  }\n\n  override protected[core] def readAttachment[T](docId: DocId, name: String, sink: Sink[ByteString, Future[T]])(\n    implicit transid: TransactionId): Future[T] = {\n    require(name != null, \"name undefined\")\n    val start =\n      transid.started(\n        this,\n        DATABASE_ATT_GET,\n        s\"[ATT_GET] '$prefix' finding attachment '$name' of document 'id: $docId'\")\n    val source = getAttachmentSource(objectKey(docId, name))\n\n    val f = source\n      .flatMap { x =>\n        x.withAttributes(s3attributes).runWith(sink)\n      }\n      .recoverWith {\n        case e: Throwable if isMissingKeyException(e) =>\n          Future.failed(NoDocumentException(\"Not found on 'readAttachment'.\"))\n      }\n\n    val g = f.transform(\n      { s =>\n        transid\n          .finished(this, start, s\"[ATT_GET] '$prefix' completed: found attachment '$name' of document 'id: $docId'\")\n        s\n      }, {\n        case e: NoDocumentException =>\n          transid\n            .finished(\n              this,\n              start,\n              s\"[ATT_GET] '$prefix', retrieving attachment '$name' of document 'id: $docId'; not found.\",\n              logLevel = Logging.ErrorLevel)\n          e\n        case e => e\n      })\n\n    reportFailure(\n      g,\n      start,\n      failure =>\n        s\"[ATT_GET] '$prefix' internal error, name: '$name', doc: 'id: $docId', failure: '${failure.getMessage}'\")\n  }\n\n  private def getAttachmentSource(objectKey: String): Future[Source[ByteString, Any]] = urlSigner match {\n    case Some(signer) =>\n      getUrlContent(signer.getSignedURL(objectKey)).map(_.get)\n\n    // S3.getObject returns Source[ByteString, Future[ObjectMetadata]].\n    // The materialized future will fail if the object doesn't exist, which is handled by the caller.\n    case None =>\n      Future.successful(S3.getObject(bucket, objectKey).withAttributes(s3attributes))\n  }\n\n  private def getUrlContent(uri: Uri): Future[Option[Source[ByteString, Any]]] = {\n    val future = Http().singleRequest(HttpRequest(uri = uri))\n    future.flatMap {\n      case HttpResponse(status, _, entity, _) if status.isSuccess() && !status.isRedirection() =>\n        Future.successful(Some(entity.dataBytes))\n      case HttpResponse(status, _, entity, _) =>\n        Unmarshal(entity).to[String].flatMap { err =>\n          //With CloudFront also the error message confirms to same S3 exception format\n          Future.failed(S3Exception(err, status))\n        }\n    }\n  }\n\n  override protected[core] def deleteAttachments(docId: DocId)(implicit transid: TransactionId): Future[Boolean] = {\n    val start =\n      transid.started(\n        this,\n        DATABASE_ATTS_DELETE,\n        s\"[ATT_DELETE] deleting attachments of document 'id: $docId' with prefix ${objectKeyPrefix(docId)}\")\n\n    val f = S3\n      .deleteObjectsByPrefix(bucket, Some(objectKeyPrefix(docId)))\n      .withAttributes(s3attributes)\n      .runWith(Sink.seq)\n      .map(_ => true)\n\n    f.foreach(_ =>\n      transid.finished(this, start, s\"[ATTS_DELETE] completed: deleting attachments of document 'id: $docId'\"))\n\n    reportFailure(\n      f,\n      start,\n      failure => s\"[ATTS_DELETE] '$prefix' internal error, doc: '$docId', failure: '${failure.getMessage}'\")\n  }\n\n  override protected[core] def deleteAttachment(docId: DocId, name: String)(\n    implicit transid: TransactionId): Future[Boolean] = {\n    val start =\n      transid.started(this, DATABASE_ATT_DELETE, s\"[ATT_DELETE] deleting attachment '$name' of document 'id: $docId'\")\n\n    val f = S3\n      .deleteObject(bucket, objectKey(docId, name))\n      .withAttributes(s3attributes)\n      .runWith(Sink.head)\n      .map(_ => true)\n\n    f.foreach(_ =>\n      transid.finished(this, start, s\"[ATT_DELETE] completed: deleting attachment '$name' of document 'id: $docId'\"))\n\n    reportFailure(\n      f,\n      start,\n      failure => s\"[ATT_DELETE] '$prefix' internal error, doc: '$docId', failure: '${failure.getMessage}'\")\n  }\n\n  override def shutdown(): Unit = {}\n\n  private def objectKey(id: DocId, name: String): String = s\"$prefix/${id.id}/$name\"\n\n  private def objectKeyPrefix(id: DocId): String =\n    s\"$prefix/${id.id}/\" //must end with a slash so that \".../<package>/<action>other\" does not match for \"<package>/<action>\"\n\n  private def isMissingKeyException(e: Throwable): Boolean = {\n    //In some case S3Exception is a sub cause. So need to recurse\n    e match {\n      case s: S3Exception if s.code == \"NoSuchKey\" => true\n      // In case of CloudFront a missing key would be reflected as access denied\n      case s: S3Exception if s.code == \"AccessDenied\" && urlSigner.isDefined => true\n      case t if t != null && isMissingKeyException(t.getCause)               => true\n      case _                                                                 => false\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entitlement/Privilege.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport scala.util.Try\n\nimport spray.json.DeserializationException\nimport spray.json.JsString\nimport spray.json.JsValue\nimport spray.json.RootJsonFormat\n\nsealed trait Privilege\n\n/** An enumeration of privileges available to subjects. */\nprotected[core] object Privilege extends Enumeration {\n\n  case object READ extends Privilege\n  case object PUT extends Privilege\n  case object DELETE extends Privilege\n  case object ACTIVATE extends Privilege\n  case object REJECT extends Privilege\n\n  val CRUD: Set[Privilege] = Set(READ, PUT, DELETE)\n  val ALL: Set[Privilege] = CRUD + ACTIVATE\n\n  def fromName(name: String) = name match {\n    case \"READ\"     => READ\n    case \"PUT\"      => PUT\n    case \"DELETE\"   => DELETE\n    case \"ACTIVATE\" => ACTIVATE\n    case \"REJECT\"   => REJECT\n  }\n\n  implicit val serdes = new RootJsonFormat[Privilege] {\n    def write(p: Privilege) = JsString(p.toString)\n\n    def read(json: JsValue) =\n      Try {\n        val JsString(str) = json\n        Privilege.fromName(str.trim.toUpperCase)\n      } getOrElse {\n        throw new DeserializationException(\"Privilege must be a valid string\")\n      }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/ActivationEntityLimit.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.size._\n\ncase class ActivationEntityPayload(max: ByteSize, truncation: ByteSize)\ncase class ActivationEntityLimitConf(serdesOverhead: ByteSize, payload: ActivationEntityPayload)\n\n/**\n * ActivationEntityLimit defines the limits on the input/output payloads for actions\n * and triggers. This refers to the invoke-time parameters for actions or the trigger-time\n * parameters for triggers.\n */\nprotected[core] object ActivationEntityLimit {\n  private val config = loadConfigOrThrow[ActivationEntityLimitConf](ConfigKeys.activation)\n  private val namespacePayloadLimitConfig = try {\n    loadConfigOrThrow[ActivationEntityPayload](ConfigKeys.namespaceActivationPayloadLimit)\n  } catch {\n    case _: Throwable =>\n      // Supports backwards compatibility for openwhisk that do not use the namespace default limit\n      ActivationEntityPayload(config.payload.max, config.payload.truncation)\n  }\n\n  // system limit\n  protected[core] val MAX_ACTIVATION_ENTITY_LIMIT: ByteSize = config.payload.max\n  protected[core] val MAX_ACTIVATION_ENTITY_TRUNCATION_LIMIT: ByteSize = config.payload.truncation\n  protected[core] val MAX_ACTIVATION_LIMIT: ByteSize = config.payload.max + config.serdesOverhead\n\n  // namespace default limit\n  protected[core] val MAX_ACTIVATION_ENTITY_LIMIT_DEFAULT: ByteSize = namespacePayloadLimitConfig.max\n  protected[core] val MAX_ACTIVATION_ENTITY_TRUNCATION_LIMIT_DEFAULT: ByteSize = namespacePayloadLimitConfig.truncation\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/ActivationId.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport spray.json.DefaultJsonProtocol.StringJsonFormat\nimport spray.json._\nimport org.apache.openwhisk.http.Messages\n\nimport org.apache.openwhisk.core.entity.size._\n\nimport scala.util.{Failure, Success, Try}\n\n/**\n * An activation id, is a unique id assigned to activations (invoke action or fire trigger).\n * It must be globally unique.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param asString the activation id\n */\nprotected[openwhisk] case class ActivationId private (val asString: String) extends AnyVal {\n  override def toString: String = asString\n  def toJsObject: JsObject = JsObject(\"activationId\" -> asString.toJson)\n}\n\nprotected[core] object ActivationId {\n\n  protected[core] trait ActivationIdGenerator {\n    def make(): ActivationId = ActivationId.generate()\n  }\n\n  /** Checks if the current character is hexadecimal */\n  private def isHexadecimal(c: Char) = c.isDigit || c == 'a' || c == 'b' || c == 'c' || c == 'd' || c == 'e' || c == 'f'\n\n  /**\n   * Parses an activation id from a string.\n   *\n   * @param id the activation id as string\n   * @return ActivationId instance\n   */\n  def parse(id: String): Try[ActivationId] = {\n    val length = id.length\n    if (length != 32) {\n      Failure(\n        new IllegalArgumentException(Messages.activationIdLengthError(SizeError(\"Activation id\", length.B, 32.B))))\n    } else if (!id.forall(isHexadecimal)) {\n      Failure(new IllegalArgumentException(Messages.activationIdIllegal))\n    } else {\n      Success(new ActivationId(id))\n    }\n  }\n\n  /**\n   * Generates a random activation id using java.util.UUID factory.\n   *\n   * Uses fast path to generate the ActivationId without additional requirement checks.\n   *\n   * @return new ActivationId\n   */\n  protected[core] def generate(): ActivationId = new ActivationId(UUIDs.randomUUID().toString.filterNot(_ == '-'))\n\n  protected[core] implicit val serdes: RootJsonFormat[ActivationId] = new RootJsonFormat[ActivationId] {\n    def write(d: ActivationId) = JsString(d.toString)\n\n    def read(value: JsValue): ActivationId = {\n      val parsed = value match {\n        case JsString(s) => ActivationId.parse(s)\n        case JsNumber(n) => ActivationId.parse(n.toString)\n        case _           => Failure(DeserializationException(Messages.activationIdIllegal))\n      }\n\n      parsed match {\n        case Success(aid)                         => aid\n        case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)\n        case Failure(_)                           => deserializationError(Messages.activationIdIllegal)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/ActivationLogs.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\nprotected[core] case class ActivationLogs(logs: Vector[String] = Vector.empty) extends AnyVal {\n  def toJsonObject = JsObject(\"logs\" -> toJson)\n  def toJson = logs.toJson\n\n  override def toString = logs.mkString(\"[\", \", \", \"]\")\n}\n\nprotected[core] object ActivationLogs {\n  protected[core] implicit val serdes = new RootJsonFormat[ActivationLogs] {\n    def write(l: ActivationLogs) = l.toJson\n    def read(value: JsValue) = ActivationLogs(value.convertTo[Vector[String]])\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/ActivationResult.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport scala.util.Try\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.{OK, ServiceUnavailable}\nimport spray.json._\nimport spray.json.DefaultJsonProtocol\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.http.Messages._\n\nprotected[core] case class ActivationResponse private (statusCode: Int,\n                                                       result: Option[JsValue],\n                                                       size: Option[Int] = None) {\n\n  def toJsonObject = ActivationResponse.serdes.write(this).asJsObject\n\n  // Used when presenting to end-users, to hide the statusCode (which is an implementation detail),\n  // and to provide a convenience boolean \"success\" field.\n  def toExtendedJson: JsObject = {\n    val baseFields = this.toJsonObject.fields\n    JsObject(\n      (baseFields - \"statusCode\") ++ Seq(\n        \"success\" -> JsBoolean(this.isSuccess),\n        \"status\" -> JsString(ActivationResponse.messageForCode(statusCode))))\n\n  }\n\n  def isSuccess = statusCode == ActivationResponse.Success\n  def isApplicationError = statusCode == ActivationResponse.ApplicationError\n  def isContainerError = statusCode == ActivationResponse.DeveloperError\n  def isWhiskError = statusCode == ActivationResponse.WhiskError\n  def withoutResult = ActivationResponse(statusCode, None)\n\n  override def toString = toJsonObject.compactPrint\n}\n\nprotected[core] object ActivationResponse extends DefaultJsonProtocol {\n  /* The field name that is universally recognized as the marker of an error, from the application or otherwise. */\n  val ERROR_FIELD: String = \"error\"\n\n  // These constants need to be synchronized with messageForCode() method below\n  val Success = 0 // action ran successfully and produced a result\n  val ApplicationError = 1 // action ran but there was an error and it was handled\n  val DeveloperError = 2 // action ran but failed to handle an error, or action did not run and failed to initialize\n  val WhiskError = 3 // internal system error\n\n  val statusSuccess = \"success\"\n  val statusApplicationError = \"application_error\"\n  val statusDeveloperError = \"action_developer_error\"\n  val statusWhiskError = \"whisk_internal_error\"\n\n  protected[core] def statusForCode(code: Int) = {\n    require(code >= 0 && code <= 3)\n    code match {\n      case Success          => statusSuccess\n      case ApplicationError => statusApplicationError\n      case DeveloperError   => statusDeveloperError\n      case WhiskError       => statusWhiskError\n    }\n  }\n\n  protected[core] def messageForCode(code: Int) = {\n    require(code >= 0 && code <= 3)\n    code match {\n      case Success          => \"success\"\n      case ApplicationError => \"application error\"\n      case DeveloperError   => \"action developer error\"\n      case WhiskError       => \"whisk internal error\"\n    }\n  }\n\n  private def error(code: Int, errorValue: JsValue, size: Option[Int]) = {\n    require(code == ApplicationError || code == DeveloperError || code == WhiskError)\n    ActivationResponse(code, Some(JsObject(ERROR_FIELD -> errorValue)), size)\n  }\n\n  protected[core] def success(result: Option[JsValue] = None, size: Option[Int] = None) =\n    ActivationResponse(Success, result, size)\n\n  protected[core] def applicationError(errorValue: JsValue, size: Option[Int] = None) =\n    error(ApplicationError, errorValue, size)\n  protected[core] def applicationError(errorMsg: String) =\n    error(ApplicationError, JsString(errorMsg), None)\n  protected[core] def developerError(errorValue: JsValue, size: Option[Int]) =\n    error(DeveloperError, errorValue, size)\n  protected[core] def developerError(errorMsg: String, size: Option[Int] = None) =\n    error(DeveloperError, JsString(errorMsg), size)\n  protected[core] def whiskError(errorValue: JsValue) = error(WhiskError, errorValue, None)\n  protected[core] def whiskError(errorMsg: String) =\n    error(WhiskError, JsString(errorMsg), None)\n\n  /**\n   * Returns an ActivationResponse that is used as a placeholder for payload\n   * Used as a feed for starting a sequence.\n   * NOTE: the code is application error (since this response could be used as a response for the sequence\n   * if the payload contains an error)\n   */\n  protected[core] def payloadPlaceholder(payload: Option[JsValue]) = ActivationResponse(ApplicationError, payload)\n\n  /**\n   * Class of errors for invoker-container communication.\n   */\n  protected[core] sealed trait ContainerConnectionError\n  protected[core] sealed trait ContainerHttpError extends ContainerConnectionError\n  protected[core] case class ConnectionError(t: Throwable) extends ContainerHttpError\n  protected[core] case class NoResponseReceived() extends ContainerHttpError\n  protected[core] case class Timeout(t: Throwable) extends ContainerHttpError\n\n  protected[core] case class MemoryExhausted() extends ContainerConnectionError\n\n  /**\n   * @param statusCode the container HTTP response code (e.g., 200 OK)\n   * @param entity the entity response as string\n   * @param truncated either None to indicate complete entity or Some(actual length, max allowed)\n   */\n  protected[core] case class ContainerResponse(statusCode: Int,\n                                               entity: String,\n                                               truncated: Option[(ByteSize, ByteSize)]) {\n\n    /** true iff status code is OK (HTTP 200 status code), anything else is considered an error. **/\n    val okStatus = statusCode == OK.intValue\n    val ok = okStatus && truncated.isEmpty\n\n    /** true iff status code is ServiceUnavailable (HTTP 503 status code) */\n    val shuttingDown = statusCode == ServiceUnavailable.intValue\n\n    override def toString = {\n      val base = if (okStatus) \"ok\" else \"not ok\"\n      val rest = truncated.map(e => s\", truncated ${e.toString}\").getOrElse(\"\")\n      base + rest\n    }\n  }\n\n  protected[core] object ContainerResponse {\n    def apply(okStatus: Boolean, entity: String, truncated: Option[(ByteSize, ByteSize)] = None): ContainerResponse = {\n      ContainerResponse(if (okStatus) OK.intValue else 500, entity, truncated)\n    }\n  }\n\n  /**\n   * Interprets response from container after initialization. This method is only called when the initialization failed.\n   *\n   * @param response an either a container error or container response (HTTP Status Code, HTTP response bytes as String)\n   * @return appropriate ActivationResponse representing initialization error\n   */\n  protected[core] def processInitResponseContent(response: Either[ContainerConnectionError, ContainerResponse],\n                                                 logger: Logging): ActivationResponse = {\n    require(response.isLeft || !response.exists(_.ok), s\"should not interpret init response when status code is OK\")\n    response match {\n      case Right(ContainerResponse(code, str, truncated)) =>\n        val sizeOpt = Option(str).map(_.length)\n        truncated match {\n          case None =>\n            Try { str.parseJson.asJsObject } match {\n              case scala.util.Success(result @ JsObject(fields)) =>\n                // If the response is a JSON object container an error field, accept it as the response error.\n                val errorOpt = fields.get(ERROR_FIELD)\n                val errorContent = errorOpt getOrElse invalidInitResponse(str).toJson\n                developerError(errorContent, sizeOpt)\n              case _ =>\n                developerError(invalidInitResponse(str), sizeOpt)\n            }\n\n          case Some((length, maxlength)) =>\n            developerError(truncatedResponse(str, length, maxlength), Some(length.toBytes.toInt))\n        }\n\n      case Left(_: MemoryExhausted) =>\n        developerError(memoryExhausted)\n\n      case Left(e) =>\n        // This indicates a terminal failure in the container (it exited prematurely).\n        developerError(abnormalInitialization)\n    }\n  }\n\n  /**\n   * Interprets response from container after running the action. This method is only called when the initialization succeeded.\n   *\n   * @param response an Option (HTTP Status Code, HTTP response bytes as String)\n   * @return appropriate ActivationResponse representing run result\n   */\n  protected[core] def processRunResponseContent(response: Either[ContainerConnectionError, ContainerResponse],\n                                                logger: Logging): ActivationResponse = {\n    response match {\n      case Right(res @ ContainerResponse(_, str, truncated)) =>\n        truncated match {\n          case None =>\n            val sizeOpt = Option(str).map(_.length)\n            Try { str.parseJson } match {\n              case scala.util.Success(result @ JsObject(fields)) =>\n                // If the response is a JSON object container an error field, accept it as the response error.\n                val errorOpt = fields.get(ERROR_FIELD)\n\n                if (res.okStatus) {\n                  errorOpt map { error =>\n                    applicationError(error, sizeOpt)\n                  } getOrElse {\n                    // The happy path.\n                    success(Some(result), sizeOpt)\n                  }\n                } else {\n                  // Any non-200 code is treated as a container failure. We still need to check whether\n                  // there was a useful error message in there.\n                  val errorContent = errorOpt getOrElse invalidRunResponse(str).toJson\n                  developerError(errorContent, sizeOpt)\n                }\n\n              case scala.util.Success(result @ JsArray(_)) =>\n                if (res.okStatus) {\n                  success(Some(result), sizeOpt)\n                } else {\n                  // Any non-200 code is treated as a container failure. We still need to check whether\n                  // there was a useful error message in there.\n                  val errorContent = invalidRunResponse(str).toJson\n                  //developerErrorWithLog(errorContent, sizeOpt, None)\n                  developerError(errorContent, sizeOpt)\n                }\n\n              case scala.util.Success(notAnObj) =>\n                // This should affect only blackbox containers, since our own containers should already test for that.\n                developerError(invalidRunResponse(str), sizeOpt)\n\n              case scala.util.Failure(t) =>\n                // This should affect only blackbox containers, since our own containers should already test for that.\n                logger.warn(this, s\"response did not json parse: '$str' led to $t\")\n                developerError(invalidRunResponse(str), sizeOpt)\n            }\n\n          case Some((length, maxlength)) =>\n            applicationError(JsString(truncatedResponse(str, length, maxlength)), Some(length.toBytes.toInt))\n        }\n\n      case Left(_: MemoryExhausted) =>\n        developerError(memoryExhausted)\n\n      case Left(e) =>\n        // This indicates a terminal failure in the container (it exited prematurely).\n        developerError(abnormalRun)\n    }\n  }\n\n  protected[core] implicit val serdes = jsonFormat3(ActivationResponse.apply)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/Annotations.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nobject Annotations {\n  val FinalParamsAnnotationName = \"final\"\n  val WebActionAnnotationName = \"web-export\"\n  val WebCustomOptionsAnnotationName = \"web-custom-options\"\n  val RawHttpAnnotationName = \"raw-http\"\n  val RequireWhiskAuthAnnotation = \"require-whisk-auth\"\n  val ProvideApiKeyAnnotationName = \"provide-api-key\"\n  val InvokerResourcesAnnotationName = \"invoker-resources\"\n  val InvokerResourcesStrictPolicyAnnotationName = \"invoker-resources-strict-policy\"\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/ArgNormalizer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport spray.json._\nimport scala.util.Try\n\nprotected[entity] trait ArgNormalizer[T] {\n\n  protected[core] val serdes: RootJsonFormat[T]\n\n  protected[entity] def factory(s: String): T = serdes.read(Try(s.parseJson).getOrElse(JsString(s)))\n\n  /**\n   * Creates a new T from string. The method checks that a string\n   * argument is not null, not empty, and normalizes it by trimming\n   * white space before creating new T.\n   *\n   * @param s is the string argument to supply to factory of T\n   * @return T instance\n   * @throws IllegalArgumentException if string is null or empty\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(s: String): T = {\n    require(s != null && s.trim.nonEmpty, \"argument undefined\")\n    factory(s.trim)\n  }\n}\n\nprotected[entity] object ArgNormalizer {\n  protected[entity] def trim(s: String) = Option(s) map { _.trim } getOrElse s\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/Attachments.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport org.apache.pekko.http.scaladsl.model.ContentType\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.core.entity.size._\n\nimport scala.util.Try\n\nobject Attachments {\n\n  /**\n   * A marker for a field that is either inlined in an entity, or a reference\n   * to an attachment. In the case where the value is inlined, it (de)serializes\n   * to the same value as if it weren't wrapped.\n   *\n   * Note that such fields may be defined at any level of nesting in an entity,\n   * but the attachments will always be top-level. The logic for actually retrieving\n   * an attachment therefore must be separate for all use cases.\n   */\n  sealed trait Attachment[+T]\n\n  case class Inline[T](value: T) extends Attachment[T]\n\n  case class Attached(attachmentName: String,\n                      attachmentType: ContentType,\n                      length: Option[Long] = None,\n                      digest: Option[String] = None)\n      extends Attachment[Nothing]\n\n  // Attachments are considered free because the name/content type are system determined\n  // and a size check for the content is done during create/update\n  implicit class SizeAttachment[T](a: Attachment[T])(implicit ev: T => SizeConversion) extends SizeConversion {\n    def sizeIn(unit: SizeUnits.Unit): ByteSize = a match {\n      case Inline(v) => (v: SizeConversion).sizeIn(unit)\n      case _         => 0.bytes\n    }\n  }\n\n  implicit class OptionSizeAttachment[T](a: Option[Attachment[T]])(implicit ev: T => SizeConversion)\n      extends SizeConversion {\n    def sizeIn(unit: SizeUnits.Unit): ByteSize = a match {\n      case Some(Inline(v)) => (v: SizeConversion).sizeIn(unit)\n      case _               => 0.bytes\n    }\n  }\n\n  object Attached {\n    implicit val serdes = {\n      implicit val contentTypeSerdes = new RootJsonFormat[ContentType] {\n        override def write(c: ContentType) = JsString(c.value)\n\n        override def read(js: JsValue): ContentType = {\n          Try(js.convertTo[String]).toOption.flatMap(ContentType.parse(_).toOption).getOrElse {\n            throw new DeserializationException(\"Could not deserialize content-type\")\n          }\n        }\n      }\n\n      jsonFormat4(Attached.apply)\n    }\n  }\n\n  implicit def serdes[T: JsonFormat] = new JsonFormat[Attachment[T]] {\n    val sub = implicitly[JsonFormat[T]]\n\n    def write(a: Attachment[T]): JsValue = a match {\n      case Inline(v)   => sub.write(v)\n      case a: Attached => Attached.serdes.write(a)\n    }\n\n    def read(js: JsValue): Attachment[T] =\n      Try {\n        Inline(sub.read(js))\n      } recover {\n        case _: DeserializationException => Attached.serdes.read(js)\n      } getOrElse {\n        throw new DeserializationException(\"Could not deserialize as attachment record: \" + js)\n      }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/BasicAuthenticationAuthKey.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport org.apache.pekko.http.scaladsl.model.headers.{BasicHttpCredentials, HttpCredentials}\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n/**\n * Authentication key for Basic Authentication, consisting of a UUID and Secret.\n *\n * @param uuid the uuid assured to be non-null because both types are values\n * @param key the key assured to be non-null because both types are values\n */\nprotected[core] case class BasicAuthenticationAuthKey(uuid: UUID, key: Secret)\n    extends GenericAuthKey(JsObject(\"api_key\" -> s\"$uuid:$key\".toJson)) {\n  def revoke = new BasicAuthenticationAuthKey(uuid, Secret())\n  def compact: String = s\"$uuid:$key\"\n  override def toString: String = uuid.toString\n  override def getCredentials: Option[HttpCredentials] = Some(BasicHttpCredentials(uuid.asString, key.asString))\n}\n\nprotected[core] object BasicAuthenticationAuthKey {\n\n  /**\n   * Creates AuthKey from a string where the uuid and key are separated by a colon.\n   * If the string contains more than one colon, all values are ignored except for\n   * the first two hence \"k:v*\" produces (\"k\",\"v\").\n   *\n   * @param str the string containing uuid and key separated by colon\n   * @return AuthKey if argument is properly formatted\n   * @throws IllegalArgumentException if argument is not well formed\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(str: String): BasicAuthenticationAuthKey = {\n    val (uuid, secret) = str.split(':').toList match {\n      case k :: v :: _ => (k, v)\n      case k :: Nil    => (k, \"\")\n      case Nil         => (\"\", \"\")\n    }\n\n    new BasicAuthenticationAuthKey(UUID(uuid.trim), Secret(secret.trim))\n  }\n\n  /**\n   * Creates an auth key for a randomly generated UUID with a randomly generated secret.\n   */\n  protected[core] def apply(): BasicAuthenticationAuthKey = new BasicAuthenticationAuthKey(UUID(), Secret())\n\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/CacheKey.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport spray.json.DefaultJsonProtocol\n\nclass UnsupportedCacheKeyTypeException(msg: String) extends Exception(msg)\n\n/**\n * A key that is used to store an entity on the cache.\n *\n * @param mainId The main part for the key to be used. For example this is the id of a document.\n * @param secondaryId A second part of the key. For example the revision of an entity. This part\n * of the key will not be written to the logs.\n */\ncase class CacheKey(mainId: String, secondaryId: Option[String]) {\n  override def toString() = {\n    s\"CacheKey($mainId)\"\n  }\n}\n\nobject CacheKey extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat2(CacheKey.apply)\n\n  def apply(key: Any): CacheKey = {\n    key match {\n      case e: EntityName                 => CacheKey(e.asString, None)\n      case a: BasicAuthenticationAuthKey => CacheKey(a.uuid.asString, Some(a.key.asString))\n      case d: DocInfo => {\n        val revision = if (d.rev.empty) None else Some(d.rev.asString)\n        CacheKey(d.id.asString, revision)\n      }\n      case w: WhiskEntity => CacheKey(w.docid.asDocInfo)\n      case s: String      => CacheKey(s, None)\n      case others => {\n        throw new UnsupportedCacheKeyTypeException(s\"Unable to apply the entity ${others.getClass} on CacheKey.\")\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/CreationId.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\nimport spray.json.DefaultJsonProtocol.StringJsonFormat\nimport spray.json.{JsObject, _}\n\nimport scala.util.{Failure, Success, Try}\n\nprotected[openwhisk] case class CreationId private (val asString: String) extends AnyVal {\n  override def toString: String = asString\n  def toJsObject: JsObject = JsObject(\"creationId\" -> asString.toJson)\n}\n\nprotected[core] object CreationId {\n\n  protected[core] trait CreationIdGenerator {\n    def make(): CreationId = CreationId.generate()\n  }\n\n  /** Checks if the current character is hexadecimal */\n  private def isHexadecimal(c: Char) = c.isDigit || c == 'a' || c == 'b' || c == 'c' || c == 'd' || c == 'e' || c == 'f'\n\n  /**\n   * Parses an creation id from a string.\n   *\n   * @param id the creation id as string\n   * @return CreationId instance\n   */\n  def parse(id: String): Try[CreationId] = {\n    val length = id.length\n    if (length != 32) {\n      Failure(new IllegalArgumentException(Messages.creationIdLengthError(SizeError(\"Creation id\", length.B, 32.B))))\n    } else if (!id.forall(isHexadecimal)) {\n      Failure(new IllegalArgumentException(Messages.creationIdIllegal))\n    } else {\n      Success(new CreationId(id))\n    }\n  }\n\n  /**\n   * Generates a random creation id using java.util.UUID factory.\n   *\n   * Uses fast path to generate the CreationId without additional requirement checks.\n   *\n   * @return new CreationId\n   */\n  protected[core] def generate(): CreationId = new CreationId(UUIDs.randomUUID().toString.filterNot(_ == '-'))\n\n  protected[core] implicit val serdes: RootJsonFormat[CreationId] = new RootJsonFormat[CreationId] {\n    def write(d: CreationId) = JsString(d.toString)\n\n    def read(value: JsValue): CreationId = {\n      val parsed = value match {\n        case JsString(s) => CreationId.parse(s)\n        case JsNumber(n) => CreationId.parse(n.toString)\n        case _           => Failure(DeserializationException(Messages.creationIdIllegal))\n      }\n\n      parsed match {\n        case Success(cid)                         => cid\n        case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)\n        case Failure(_)                           => deserializationError(Messages.creationIdIllegal)\n      }\n    }\n  }\n\n  val systemPrefix = \"cid_\"\n  val void = CreationId(systemPrefix + \"void\")\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/DocInfo.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport org.apache.commons.lang3.StringUtils\n\nimport scala.util.Try\nimport spray.json.DefaultJsonProtocol\nimport spray.json.JsNull\nimport spray.json.JsString\nimport spray.json.JsValue\nimport spray.json.RootJsonFormat\nimport spray.json.deserializationError\nimport spray.json._\nimport org.apache.openwhisk.core.entity.ArgNormalizer.trim\n\n/**\n * A DocId is the document id === primary key in the datastore.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param id the document id, required not null\n */\nprotected[core] class DocId(val id: String) extends AnyVal {\n  def asString = id // to make explicit that this is a string conversion\n  protected[core] def asDocInfo = DocInfo(this)\n  protected[core] def asDocInfo(rev: DocRevision) = DocInfo(this, rev)\n  protected[entity] def toJson = JsString(id)\n  override def toString = id\n}\n\n/**\n * A DocRevision is the document revision, an opaque value that may be\n * determined by the datastore.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param rev the document revision, optional\n */\nprotected[core] class DocRevision private (val rev: String) extends AnyVal with Ordered[DocRevision] {\n  def asString = rev // to make explicit that this is a string conversion\n  def empty = rev == null\n  override def toString = rev\n  def serialize = DocRevision.serdes.write(this).compactPrint\n\n  override def compare(that: DocRevision): Int = {\n    if (this.empty && that.empty) {\n      0\n    } else if (this.empty) {\n      -1\n    } else if (that.empty) {\n      1\n    } else {\n      StringUtils.substringBefore(rev, \"-\").toInt - StringUtils.substringBefore(that.rev, \"-\").toInt\n    }\n  }\n\n  def ==(that: DocRevision): Boolean = {\n    this.compare(that) == 0\n  }\n}\n\n/**\n * Document Info wrapping the document id and revision. The constructor\n * is protected to make sure id and rev are well formed and defined. Use\n * one of the factories in the companion object where necessary. Since\n * the id and rev are values, the type system ensures they are not null.\n *\n * @param id the document id\n * @param rev the document revision, optional; this is an opaque value determined by the datastore\n */\nprotected[core] case class DocInfo protected[entity] (id: DocId, rev: DocRevision = DocRevision.empty) {\n  override def toString = {\n    if (rev.empty) {\n      s\"id: $id\"\n    } else {\n      s\"id: $id, rev: $rev\"\n    }\n  }\n\n  override def hashCode = {\n    if (rev.empty) {\n      id.hashCode\n    } else {\n      s\"$id.$rev\".hashCode\n    }\n  }\n}\n\n/**\n * A BulkEntityResult is wrapping the fields that are returned for a single document on a bulk-put of several documents.\n * http://docs.couchdb.org/en/2.1.0/api/database/bulk-api.html#post--db-_bulk_docs\n *\n * @param id the document id\n * @param rev the document revision, optional; this is an opaque value determined by the datastore\n * @param error the error, that occurred on trying to put this document into CouchDB\n * @param reason the error message that correspands to the error\n */\ncase class BulkEntityResult(id: String, rev: Option[DocRevision], error: Option[String], reason: Option[String]) {\n  def toDocInfo = DocInfo(DocId(id), rev.getOrElse(DocRevision.empty))\n}\n\nprotected[core] object DocId extends ArgNormalizer[DocId] {\n\n  /**\n   * Unapply method for convenience of case matching.\n   */\n  def unapply(s: String): Option[DocId] = Try(DocId(s)).toOption\n\n  implicit val serdes = new RootJsonFormat[DocId] {\n    def write(d: DocId) = d.toJson\n\n    def read(value: JsValue) =\n      Try {\n        val JsString(s) = value\n        new DocId(s)\n      } getOrElse deserializationError(\"doc id malformed\")\n  }\n}\n\nprotected[core] object DocRevision {\n\n  /**\n   * Creates a DocRevision. Normalizes the revision if necessary.\n   *\n   * @param s is the document revision as a string, may be null\n   * @return DocRevision\n   */\n  protected[core] def apply(s: String): DocRevision = new DocRevision(trim(s))\n\n  protected[core] val empty: DocRevision = new DocRevision(null)\n\n  protected[core] def parse(msg: String) = Try(serdes.read(msg.parseJson))\n\n  implicit val serdes = new RootJsonFormat[DocRevision] {\n    def write(d: DocRevision) = if (d.rev != null) JsString(d.rev) else JsNull\n\n    def read(value: JsValue) = value match {\n      case JsString(s) => DocRevision(s)\n      case JsNull      => DocRevision.empty\n      case _           => deserializationError(\"doc revision malformed\")\n    }\n  }\n}\n\nprotected[core] object DocInfo extends DefaultJsonProtocol {\n\n  /**\n   * Creates a DocInfo with id set to the argument and no revision.\n   *\n   * @param id is the document identifier, must be defined\n   * @throws IllegalArgumentException if id is null or empty\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(id: String): DocInfo = DocInfo(DocId(id))\n\n  /**\n   * Creates a DocInfo with id and revision per the provided arguments.\n   *\n   * @param id is the document identifier, must be defined\n   * @param rev the document revision, optional\n   * @return DocInfo for id and revision\n   * @throws IllegalArgumentException if id is null or empty\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def !(id: String, rev: String): DocInfo = DocInfo(DocId(id), DocRevision(rev))\n\n  implicit val serdes = jsonFormat2(DocInfo.apply)\n}\n\nobject BulkEntityResult extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat4(BulkEntityResult.apply)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/DocumentReader.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport spray.json._\n\nprotected[core] abstract class DocumentReader {\n  def read[A](ma: Manifest[A], value: JsValue): WhiskDocument\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/EntityPath.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.util.regex.Matcher\n\nimport scala.util.Try\n\nimport spray.json.JsString\nimport spray.json.JsValue\nimport spray.json.RootJsonFormat\nimport spray.json.deserializationError\nimport org.apache.openwhisk.http.Messages\n\n/**\n * EntityPath is a path string of allowed characters. The path consists of parts each of which\n * must be a valid EntityName, separated by the EntityPath separator character. The private\n * constructor accepts a validated sequence of path parts and can reconstruct the path\n * from it. The path cannot be empty.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param path the sequence of parts that make up a namespace path\n */\nprotected[core] class EntityPath private (private val path: Seq[String]) extends AnyVal {\n  def namespace: String = path.foldLeft(\"\")((a, b) => if (a != \"\") a.trim + EntityPath.PATHSEP + b.trim else b.trim)\n\n  /**\n   * @return number of parts in the path.\n   */\n  def segments = path.length\n\n  /**\n   * Adds segment to path.\n   */\n  def addPath(e: EntityName) = EntityPath(path :+ e.name)\n\n  /**\n   * Concatenates given path to existing path.\n   */\n  def addPath(p: EntityPath) = EntityPath(path ++ p.path)\n\n  /**\n   * Computes the relative path by dropping the leftmost segment. The return is an option\n   * since dropping a singleton results in an invalid path.\n   */\n  def relativePath: Option[EntityPath] = Try(EntityPath(path.drop(1))).toOption\n\n  /**\n   * @return the root of the path (the first segment).\n   */\n  def root: EntityName = EntityName(path.head)\n\n  /**\n   * @return the last segment of the path.\n   */\n  def last: EntityName = EntityName(path.last)\n\n  /**\n   * @return true iff the path contains exactly one segment (i.e., the namespace)\n   */\n  def defaultPackage: Boolean = path.size == 1\n\n  /**\n   * Replaces root of this path with given namespace iff the root is\n   * the default namespace.\n   */\n  def resolveNamespace(newNamespace: Namespace): EntityPath = {\n    resolveNamespace(newNamespace.name)\n  }\n\n  /**\n   * Replaces root of this path with given namespace iff the root is\n   * the default namespace.\n   */\n  def resolveNamespace(newNamespace: EntityName): EntityPath = {\n    // check if namespace is default\n    if (root.toPath == EntityPath.DEFAULT) {\n      val newPath = path.updated(0, newNamespace.name)\n      EntityPath(newPath)\n    } else this\n  }\n\n  /**\n   * Converts the path to a fully qualified name. The path must contains at least 2 parts.\n   *\n   * @throws IllegalArgumentException if the path does not conform to schema (at least namespace and entity name must be present0\n   */\n  @throws[IllegalArgumentException]\n  def toFullyQualifiedEntityName = {\n    require(path.size > 1, Messages.malformedFullyQualifiedEntityName)\n    val name = last\n    val newPath = EntityPath(path.dropRight(1))\n    FullyQualifiedEntityName(newPath, name)\n  }\n\n  def toDocId = DocId(namespace)\n  def toJson = JsString(namespace)\n  def asString = namespace // to make explicit that this is a string conversion\n  override def toString = namespace\n}\n\nprotected[core] object EntityPath {\n\n  /** Path separator */\n  protected[core] val PATHSEP = \"/\"\n\n  /**\n   * Default namespace name. This name is not a valid entity name and is a special string\n   * that allows omission of the namespace during API calls. It is only used in the URI\n   * namespace extraction.\n   */\n  protected[core] val DEFAULT = EntityPath(\"_\")\n\n  /**\n   * Constructs a Namespace from a string. String must be a valid path, consisting of\n   * a valid EntityName separated by the Namespace separator character.\n   *\n   * @param path a valid namespace path\n   * @return Namespace for the path\n   * @throws IllegalArgumentException if the path does not conform to schema\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(path: String): EntityPath = {\n    require(path != null, \"path undefined\")\n    val parts = path.split(PATHSEP).filter { _.nonEmpty }.toSeq\n    EntityPath(parts)\n  }\n\n  /**\n   * Namespace is a path string of allowed characters. The path consists of parts each of which\n   * must be a valid EntityName, separated by the Namespace separator character. The constructor\n   * accepts a sequence of path parts and can reconstruct the path from it.\n   *\n   * @param path the sequence of parts that make up a namespace path\n   * @throws IllegalArgumentException if any of the parts are not valid path part names\n   */\n  @throws[IllegalArgumentException]\n  private def apply(parts: Seq[String]): EntityPath = {\n    require(parts != null && parts.nonEmpty, \"path undefined\")\n    require(parts.forall { s =>\n      s != null && EntityName.entityNameMatcher(s).matches\n    }, s\"path contains invalid parts ${parts.toString}\")\n    new EntityPath(parts)\n  }\n\n  /** Returns true iff the path is a valid namespace path. */\n  protected[core] def validate(path: String): Boolean = {\n    Try { EntityPath(path) } map { _ =>\n      true\n    } getOrElse false\n  }\n\n  implicit val serdes = new RootJsonFormat[EntityPath] {\n    def write(n: EntityPath) = n.toJson\n\n    def read(value: JsValue) =\n      Try {\n        val JsString(name) = value\n        EntityPath(name)\n      } getOrElse deserializationError(\"namespace malformed\")\n  }\n}\n\n/**\n * EntityName is a string of allowed characters.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n */\nprotected[core] class EntityName private (val name: String) extends AnyVal {\n  def asString = name // to make explicit that this is a string conversion\n  def toJson = JsString(name)\n  def toPath: EntityPath = EntityPath(name)\n  def addPath(e: EntityName): EntityPath = toPath.addPath(e)\n  def addPath(e: Option[EntityName]): EntityPath = e map { toPath.addPath(_) } getOrElse toPath\n  override def toString = name\n}\n\nprotected[core] object EntityName {\n  protected[core] val ENTITY_NAME_MAX_LENGTH = 256\n\n  /**\n   * Allowed path part or entity name format (excludes path separator): first character\n   * is a letter|digit|underscore, followed by one or more allowed characters in [\\w@ .&-].\n   * The name may not have trailing white space.\n   */\n  protected[core] val REGEX = raw\"\\A([\\w]|[\\w][\\w@ .&-]{0,${ENTITY_NAME_MAX_LENGTH - 2}}[\\w@.&-])\\z\"\n  private val entityNamePattern = REGEX.r.pattern // compile once\n  protected[core] def entityNameMatcher(s: String): Matcher = entityNamePattern.matcher(s)\n\n  /**\n   * Unapply method for convenience of case matching.\n   */\n  protected[core] def unapply(name: String): Option[EntityName] = Try(EntityName(name)).toOption\n\n  /**\n   * EntityName is a string of allowed characters.\n   *\n   * @param name the entity name\n   * @throws IllegalArgumentException if the name does not conform to schema\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(name: String): EntityName = {\n    require(name != null && entityNameMatcher(name).matches, s\"name [$name] is not allowed\")\n    new EntityName(name)\n  }\n\n  implicit val serdes = new RootJsonFormat[EntityName] {\n    def write(n: EntityName) = n.toJson\n\n    def read(value: JsValue) =\n      Try {\n        val JsString(name) = value\n        EntityName(name)\n      } getOrElse deserializationError(\"entity name malformed\")\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/Exec.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.nio.charset.StandardCharsets\n\nimport org.apache.openwhisk.core.ConfigKeys\n\nimport scala.util.matching.Regex\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.entity.Attachments._\nimport org.apache.openwhisk.core.entity.ExecManifest._\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.size.SizeString\nimport pureconfig._\nimport org.apache.openwhisk.http._\n\n/**\n * Exec encodes the executable details of an action. For black\n * box container, an image name is required. For Javascript and Python\n * actions, the code to execute is required.\n * For Swift actions, the source code to execute the action is required.\n * For Java actions, a base64-encoded string representing a jar file is\n * required, as well as the name of the entrypoint class.\n * An example exec looks like this:\n * { kind  : one of supported language runtimes,\n *   code  : code to execute if kind is supported,\n *   image : container name when kind is \"blackbox\",\n *   binary: for some runtimes that allow binary attachments,\n *   main  : name of the entry point function, when using a non-default value (for Java, the name of the main class)\" }\n */\nsealed abstract class Exec extends ByteSizeable {\n  override def toString: String = Exec.serdes.write(this).compactPrint\n\n  /** A type descriptor. */\n  val kind: String\n\n  /** When true exec may not be executed or updated. */\n  val deprecated: Boolean\n}\n\nsealed abstract class ExecMetaDataBase extends Exec {\n  override def toString: String = ExecMetaDataBase.serdes.write(this).compactPrint\n}\n\n/**\n * A common super class for all action exec types that contain their executable\n * code explicitly (i.e., any action other than a sequence).\n */\nsealed abstract class CodeExec[+T](implicit ev: T => SizeConversion) extends Exec {\n\n  /** An entrypoint (typically name of 'main' function). 'None' means a default value will be used. */\n  val entryPoint: Option[String]\n\n  /** The executable code. */\n  val code: T\n\n  /** Serialize code to a JSON value. */\n  def codeAsJson: JsValue\n\n  /** The runtime image (either built-in or a public image). */\n  val image: ImageName\n\n  /** Indicates if the action execution generates log markers to stdout/stderr once action activation completes. */\n  val sentinelledLogs: Boolean\n\n  /** Indicates if a container image is required from the registry to execute the action. */\n  val pull: Boolean\n\n  /**\n   * Indicates whether the code is stored in a text-readable or binary format.\n   * The binary bit may be read from the database but currently it is always computed\n   * when the \"code\" is moved to an attachment this may get changed to avoid recomputing\n   * the binary property.\n   */\n  val binary: Boolean\n\n  override def size = code.sizeInBytes + entryPoint.map(_.sizeInBytes).getOrElse(0.B)\n}\n\nsealed abstract class ExecMetaData extends ExecMetaDataBase {\n\n  /** An entrypoint (typically name of 'main' function). 'None' means a default value will be used. */\n  val entryPoint: Option[String]\n\n  /** The runtime image (either built-in or a public image). */\n  val image: ImageName\n\n  /** Indicates if a container image is required from the registry to execute the action. */\n  val pull: Boolean\n\n  /**\n   * Indicates whether the code is stored in a text-readable or binary format.\n   * The binary bit may be read from the database but currently it is always computed\n   * when the \"code\" is moved to an attachment this may get changed to avoid recomputing\n   * the binary property.\n   */\n  val binary: Boolean\n\n  override def size = 0.B\n}\n\ntrait AttachedCode {\n  def inline(bytes: Array[Byte]): Exec\n  def attach(attached: Attached): Exec\n}\n\nprotected[core] case class CodeExecAsString(manifest: RuntimeManifest,\n                                            override val code: String,\n                                            override val entryPoint: Option[String])\n    extends CodeExec[String] {\n  override val kind = manifest.kind\n  override val image = manifest.image\n  override val sentinelledLogs = manifest.sentinelledLogs.getOrElse(true)\n  override val deprecated = manifest.deprecated.getOrElse(false)\n  override val pull = false\n  override lazy val binary = Exec.isBinaryCode(code)\n  override def codeAsJson = JsString(code)\n}\n\nprotected[core] case class CodeExecMetaDataAsString(manifest: RuntimeManifest,\n                                                    override val binary: Boolean = false,\n                                                    override val entryPoint: Option[String])\n    extends ExecMetaData {\n  override val kind = manifest.kind\n  override val image = manifest.image\n  override val deprecated = manifest.deprecated.getOrElse(false)\n  override val pull = false\n}\n\nprotected[core] case class CodeExecAsAttachment(manifest: RuntimeManifest,\n                                                override val code: Attachment[String],\n                                                override val entryPoint: Option[String],\n                                                override val binary: Boolean = false)\n    extends CodeExec[Attachment[String]]\n    with AttachedCode {\n  override val kind = manifest.kind\n  override val image = manifest.image\n  override val sentinelledLogs = manifest.sentinelledLogs.getOrElse(true)\n  override val deprecated = manifest.deprecated.getOrElse(false)\n  override val pull = false\n  override def codeAsJson = code.toJson\n\n  override def inline(bytes: Array[Byte]): CodeExecAsAttachment = {\n    val encoded = new String(bytes, StandardCharsets.UTF_8)\n    copy(code = Inline(encoded))\n  }\n\n  override def attach(attached: Attached): CodeExecAsAttachment = {\n    copy(code = attached)\n  }\n}\n\nprotected[core] case class CodeExecMetaDataAsAttachment(manifest: RuntimeManifest,\n                                                        override val binary: Boolean = false,\n                                                        override val entryPoint: Option[String])\n    extends ExecMetaData {\n  override val kind = manifest.kind\n  override val image = manifest.image\n  override val deprecated = manifest.deprecated.getOrElse(false)\n  override val pull = false\n}\n\n/**\n * @param image the image name\n * @param code an optional script or zip archive (as base64 encoded) string\n */\nprotected[core] case class BlackBoxExec(override val image: ImageName,\n                                        override val code: Option[Attachment[String]],\n                                        override val entryPoint: Option[String],\n                                        val native: Boolean,\n                                        override val binary: Boolean)\n    extends CodeExec[Option[Attachment[String]]]\n    with AttachedCode {\n  override val kind = Exec.BLACKBOX\n  override val deprecated = false\n  override def codeAsJson = code.toJson\n  override val sentinelledLogs = native\n  override val pull = !native\n  override def size = super.size + image.resolveImageName().sizeInBytes\n\n  override def inline(bytes: Array[Byte]): BlackBoxExec = {\n    val encoded = new String(bytes, StandardCharsets.UTF_8)\n    copy(code = Some(Inline(encoded)))\n  }\n\n  override def attach(attached: Attached): BlackBoxExec = {\n    copy(code = Some(attached))\n  }\n}\n\nprotected[core] case class BlackBoxExecMetaData(override val image: ImageName,\n                                                override val entryPoint: Option[String],\n                                                val native: Boolean,\n                                                override val binary: Boolean = false)\n    extends ExecMetaData {\n  override val kind = ExecMetaDataBase.BLACKBOX\n  override val deprecated = false\n  override val pull = !native\n}\n\nprotected[core] case class SequenceExec(components: Vector[FullyQualifiedEntityName]) extends Exec {\n  override val kind = Exec.SEQUENCE\n  override val deprecated = false\n  override def size = components.map(_.size).reduceOption(_ + _).getOrElse(0.B)\n}\n\nprotected[core] case class SequenceExecMetaData(components: Vector[FullyQualifiedEntityName]) extends ExecMetaDataBase {\n  override val kind = ExecMetaDataBase.SEQUENCE\n  override val deprecated = false\n  override def size = components.map(_.size).reduceOption(_ + _).getOrElse(0.B)\n}\n\nobject Exec extends ArgNormalizer[Exec] with DefaultJsonProtocol {\n\n  val maxSize: ByteSize = 48.MB\n  val sizeLimit = loadConfigOrThrow[ByteSize](ConfigKeys.execSizeLimit)\n\n  require(\n    sizeLimit <= maxSize,\n    s\"Executable code size limit $sizeLimit specified by '${ConfigKeys.execSizeLimit}' should not be more than max size of $maxSize\")\n\n  // The possible values of the JSON 'kind' field for certain runtimes:\n  // - Sequence because it is an intrinsic\n  // - Black Box because it is a type marker\n  protected[core] val SEQUENCE = \"sequence\"\n  protected[core] val BLACKBOX = \"blackbox\"\n\n  // This is for error cases where the action `kind` may not be known.\n  protected[core] val UNKNOWN = \"unknown\"\n\n  private def execManifests = ExecManifest.runtimesManifest\n\n  override protected[core] implicit lazy val serdes = new RootJsonFormat[Exec] {\n    private def attFmt[T: JsonFormat] = Attachments.serdes[T]\n    private lazy val runtimes: Set[String] = execManifests.knownContainerRuntimes ++ Set(SEQUENCE, BLACKBOX)\n\n    override def write(e: Exec) = e match {\n      case c: CodeExecAsString =>\n        val base = Map(\"kind\" -> JsString(c.kind), \"code\" -> JsString(c.code), \"binary\" -> JsBoolean(c.binary))\n        val main = c.entryPoint.map(\"main\" -> JsString(_))\n        JsObject(base ++ main)\n\n      case a: CodeExecAsAttachment =>\n        val base =\n          Map(\"kind\" -> JsString(a.kind), \"code\" -> attFmt[String].write(a.code), \"binary\" -> JsBoolean(a.binary))\n        val main = a.entryPoint.map(\"main\" -> JsString(_))\n        JsObject(base ++ main)\n\n      case s @ SequenceExec(comp) =>\n        JsObject(\"kind\" -> JsString(s.kind), \"components\" -> comp.map(_.qualifiedNameWithLeadingSlash).toJson)\n\n      case b: BlackBoxExec =>\n        val base =\n          Map(\n            \"kind\" -> JsString(b.kind),\n            \"image\" -> JsString(b.image.resolveImageName()),\n            \"binary\" -> JsBoolean(b.binary))\n        val code = b.code.map(\"code\" -> attFmt[String].write(_))\n        val main = b.entryPoint.map(\"main\" -> JsString(_))\n        JsObject(base ++ code ++ main)\n      case _ => JsObject.empty\n    }\n\n    override def read(v: JsValue) = {\n      require(v != null)\n\n      val obj = v.asJsObject\n\n      val kind = obj.fields.get(\"kind\") match {\n        case Some(JsString(k)) => k.trim.toLowerCase\n        case _                 => throw new DeserializationException(\"'kind' must be a string defined in 'exec'\")\n      }\n\n      lazy val optMainField: Option[String] = obj.fields.get(\"main\") match {\n        case Some(JsString(m)) => Some(m)\n        case Some(_) =>\n          throw new DeserializationException(s\"if defined, 'main' be a string in 'exec' for '$kind' actions\")\n        case None => None\n      }\n\n      kind match {\n        case Exec.SEQUENCE =>\n          val comp: Vector[FullyQualifiedEntityName] = obj.fields.get(\"components\") match {\n            case Some(JsArray(components)) => components map (FullyQualifiedEntityName.serdes.read(_))\n            case Some(_)                   => throw new DeserializationException(s\"'components' must be an array\")\n            case None                      => throw new DeserializationException(s\"'components' must be defined for sequence kind\")\n          }\n          SequenceExec(comp)\n\n        case Exec.BLACKBOX =>\n          val image: ImageName = obj.fields.get(\"image\") match {\n            case Some(JsString(i)) => ImageName.fromString(i).get // throws deserialization exception on failure\n            case _ =>\n              throw new DeserializationException(\n                s\"'image' must be a string defined in 'exec' for '${Exec.BLACKBOX}' actions\")\n          }\n          val (codeOpt: Option[Attachment[String]], binary) = obj.fields.get(\"code\") match {\n            case None                                => (None, false)\n            case Some(JsString(i)) if i.trim.isEmpty => (None, false)\n            case Some(code)                          => (Some(attFmt[String].read(code)), isBinary(code, obj))\n          }\n          val native = execManifests.skipDockerPull(image)\n          BlackBoxExec(image, codeOpt, optMainField, native, binary)\n\n        case _ =>\n          // map \"default\" virtual runtime versions to the currently blessed actual runtime version\n          val manifest = execManifests.resolveDefaultRuntime(kind) match {\n            case Some(k) => k\n            case None    => throw new DeserializationException(Messages.invalidRuntimeError(kind, runtimes))\n          }\n\n          manifest.attached\n            .map { _ =>\n              // java actions once stored the attachment in \"jar\" instead of \"code\"\n              val code = obj.fields.get(\"code\").orElse(obj.fields.get(\"jar\")).getOrElse {\n                throw new DeserializationException(\n                  s\"'code' must be a string or attachment object defined in 'exec' for '$kind' actions\")\n              }\n\n              val main = optMainField.orElse {\n                if (manifest.requireMain.exists(identity)) {\n                  throw new DeserializationException(s\"'main' must be a string defined in 'exec' for '$kind' actions\")\n                } else None\n              }\n\n              CodeExecAsAttachment(manifest, attFmt[String].read(code), main, isBinary(code, obj))\n            }\n            .getOrElse {\n              val code: String = obj.fields.get(\"code\") match {\n                case Some(JsString(c)) => c\n                case _ =>\n                  throw new DeserializationException(s\"'code' must be a string defined in 'exec' for '$kind' actions\")\n              }\n              CodeExecAsString(manifest, code, optMainField)\n            }\n      }\n    }\n  }\n\n  val isBase64Pattern = new Regex(\"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$\").pattern\n\n  def isBinaryCode(code: String): Boolean = {\n    if (code != null) {\n      val t = code.trim\n      (t.length > 0) && (t.length % 4 == 0) && isBase64Pattern.matcher(t).matches()\n    } else false\n  }\n\n  private def isBinary(code: JsValue, obj: JsObject): Boolean = {\n    code match {\n      case JsString(c) => isBinaryCode(c)\n      case _           => obj.fields.get(\"binary\").map(_.convertTo[Boolean]).getOrElse(false)\n    }\n  }\n}\n\nprotected[core] object ExecMetaDataBase extends ArgNormalizer[ExecMetaDataBase] with DefaultJsonProtocol {\n\n  // The possible values of the JSON 'kind' field for certain runtimes:\n  // - Sequence because it is an intrinsic\n  // - Black Box because it is a type marker\n  protected[core] val SEQUENCE = \"sequence\"\n  protected[core] val BLACKBOX = \"blackbox\"\n\n  private def execManifests = ExecManifest.runtimesManifest\n\n  override protected[core] implicit lazy val serdes = new RootJsonFormat[ExecMetaDataBase] {\n    private def attFmt[T: JsonFormat] = Attachments.serdes[T]\n    private lazy val runtimes: Set[String] = execManifests.knownContainerRuntimes ++ Set(SEQUENCE, BLACKBOX)\n\n    override def write(e: ExecMetaDataBase) = e match {\n      case c: CodeExecMetaDataAsString =>\n        val base = Map(\"kind\" -> JsString(c.kind), \"binary\" -> JsBoolean(c.binary))\n        val main = c.entryPoint.map(\"main\" -> JsString(_))\n        JsObject(base ++ main)\n\n      case a: CodeExecMetaDataAsAttachment =>\n        val base =\n          Map(\"kind\" -> JsString(a.kind), \"binary\" -> JsBoolean(a.binary))\n        val main = a.entryPoint.map(\"main\" -> JsString(_))\n        JsObject(base ++ main)\n\n      case s @ SequenceExecMetaData(comp) =>\n        JsObject(\"kind\" -> JsString(s.kind), \"components\" -> comp.map(_.qualifiedNameWithLeadingSlash).toJson)\n\n      case b: BlackBoxExecMetaData =>\n        val base =\n          Map(\n            \"kind\" -> JsString(b.kind),\n            \"image\" -> JsString(b.image.resolveImageName()),\n            \"binary\" -> JsBoolean(b.binary))\n        val main = b.entryPoint.map(\"main\" -> JsString(_))\n        JsObject(base ++ main)\n    }\n\n    override def read(v: JsValue) = {\n      require(v != null)\n\n      val obj = v.asJsObject\n\n      val kind = obj.fields.get(\"kind\") match {\n        case Some(JsString(k)) => k.trim.toLowerCase\n        case _                 => throw new DeserializationException(\"'kind' must be a string defined in 'exec'\")\n      }\n\n      lazy val optMainField: Option[String] = obj.fields.get(\"main\") match {\n        case Some(JsString(m)) => Some(m)\n        case Some(_) =>\n          throw new DeserializationException(s\"if defined, 'main' be a string in 'exec' for '$kind' actions\")\n        case None => None\n      }\n\n      lazy val binary: Boolean = obj.fields.get(\"binary\") match {\n        case Some(JsBoolean(b)) => b\n        case _                  => throw new DeserializationException(\"'binary' must be a boolean defined in 'exec'\")\n      }\n\n      kind match {\n        case ExecMetaDataBase.SEQUENCE =>\n          val comp: Vector[FullyQualifiedEntityName] = obj.fields.get(\"components\") match {\n            case Some(JsArray(components)) => components map (FullyQualifiedEntityName.serdes.read(_))\n            case Some(_)                   => throw new DeserializationException(s\"'components' must be an array\")\n            case None                      => throw new DeserializationException(s\"'components' must be defined for sequence kind\")\n          }\n          SequenceExecMetaData(comp)\n\n        case ExecMetaDataBase.BLACKBOX =>\n          val image: ImageName = obj.fields.get(\"image\") match {\n            case Some(JsString(i)) => ImageName.fromString(i).get // throws deserialization exception on failure\n            case _ =>\n              throw new DeserializationException(\n                s\"'image' must be a string defined in 'exec' for '${Exec.BLACKBOX}' actions\")\n          }\n          val native = execManifests.skipDockerPull(image)\n          BlackBoxExecMetaData(image, optMainField, native, binary)\n\n        case _ =>\n          // map \"default\" virtual runtime versions to the currently blessed actual runtime version\n          val manifest = execManifests.resolveDefaultRuntime(kind) match {\n            case Some(k) => k\n            case None    => throw new DeserializationException(s\"kind '$kind' not in $runtimes\")\n          }\n\n          manifest.attached\n            .map { a =>\n              val main = optMainField.orElse {\n                if (manifest.requireMain.exists(identity)) {\n                  throw new DeserializationException(s\"'main' must be a string defined in 'exec' for '$kind' actions\")\n                } else None\n              }\n\n              CodeExecMetaDataAsAttachment(manifest, binary, main)\n            }\n            .getOrElse {\n              CodeExecMetaDataAsString(manifest, binary, optMainField)\n            }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/ExecManifest.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.util.{Failure, Success, Try}\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.core.entity.Attachments._\nimport org.apache.openwhisk.core.entity.Attachments.Attached._\nimport fastparse._\nimport NoWhitespace._\n\nimport scala.concurrent.duration.{Duration, FiniteDuration}\n\n/**\n * Reads manifest of supported runtimes from configuration file and stores\n * a representation of each runtime which is used for serialization and\n * deserialization. This singleton must be initialized.\n */\nprotected[core] object ExecManifest {\n\n  /**\n   * Required properties to initialize this singleton via WhiskConfig.\n   */\n  protected[core] def requiredProperties = Map(WhiskConfig.runtimesManifest -> null)\n\n  /**\n   * Reads runtimes manifest from WhiskConfig and initializes the\n   * singleton Runtime instance.\n   *\n   * @param config a valid configuration\n   * @param manifestOverride an optional inline manifest (used for testing)\n   * @return the manifest if initialized successfully, or an failure\n   */\n  protected[core] def initialize(config: WhiskConfig, manifestOverride: Option[String] = None): Try[Runtimes] = {\n    val rmc = loadConfigOrThrow[RuntimeManifestConfig](ConfigKeys.runtimes)\n    val mf = Try(manifestOverride.getOrElse(config.runtimesManifest).parseJson.asJsObject).flatMap(runtimes(_, rmc))\n    mf.foreach(m => manifest = Some(m))\n    mf\n  }\n\n  /**\n   * Gets existing runtime manifests.\n   *\n   * @return singleton Runtimes instance previous initialized from WhiskConfig\n   * @throws IllegalStateException if singleton was not previously initialized\n   */\n  @throws[IllegalStateException]\n  protected[core] def runtimesManifest: Runtimes = {\n    manifest.getOrElse {\n      throw new IllegalStateException(\"Runtimes manifest is not initialized.\")\n    }\n  }\n\n  private var manifest: Option[Runtimes] = None\n\n  /**\n   * @param config a configuration object as JSON\n   * @return Runtimes instance\n   */\n  protected[entity] def runtimes(config: JsObject, runtimeManifestConfig: RuntimeManifestConfig): Try[Runtimes] = Try {\n    val runtimes = config.fields\n      .get(\"runtimes\")\n      .map(_.convertTo[Map[String, Set[RuntimeManifest]]].map {\n        case (name, versions) =>\n          RuntimeFamily(name, versions.map { mf =>\n            val img = ImageName(mf.image.name, mf.image.registry, mf.image.prefix, mf.image.tag)\n            mf.copy(image = img)\n          })\n      }.toSet)\n\n    val blackbox = config.fields\n      .get(\"blackboxes\")\n      .map(_.convertTo[Set[ImageName]].map { image =>\n        ImageName(image.name, image.registry, image.prefix, image.tag)\n      })\n\n    val bypassPullForLocalImages = runtimeManifestConfig.bypassPullForLocalImages\n      .flatMap {\n        case true  => runtimeManifestConfig.localImagePrefix\n        case false => None\n      }\n\n    Runtimes(runtimes.getOrElse(Set.empty), blackbox.getOrElse(Set.empty), bypassPullForLocalImages)\n  }\n\n  /**\n   * Misc options related to runtime manifests.\n   *\n   * @param bypassPullForLocalImages if true, allow images with a prefix that matches localImagePrefix\n   *                                 to skip docker pull on invoker even if the image is not part of the blackbox set;\n   *                                 this is useful for testing with local images that aren't published to the runtimes registry\n   * @param localImagePrefix         image prefix for bypassPullForLocalImages\n   */\n  protected[core] case class RuntimeManifestConfig(bypassPullForLocalImages: Option[Boolean] = None,\n                                                   localImagePrefix: Option[String] = None)\n\n  /**\n   * A runtime manifest describes the \"exec\" runtime support.\n   *\n   * @param kind            the name of the kind e.g., nodejs:6\n   * @param deprecated      true iff the runtime is deprecated (allows get/delete but not create/update/invoke)\n   * @param default         true iff the runtime is the default kind for its family (nodejs:default -> nodejs:6)\n   * @param attached        true iff the source is an attachments (not inlined source)\n   * @param requireMain     true iff main entry point is not optional\n   * @param sentinelledLogs true iff the runtime generates stdout/stderr log sentinels after an activation\n   * @param image           optional image name, otherwise inferred via fixed mapping (remove colons and append 'action')\n   * @param stemCells       optional list of stemCells to be initialized by invoker per kind\n   */\n  protected[core] case class RuntimeManifest(kind: String,\n                                             image: ImageName,\n                                             deprecated: Option[Boolean] = None,\n                                             default: Option[Boolean] = None,\n                                             attached: Option[Attached] = None,\n                                             requireMain: Option[Boolean] = None,\n                                             sentinelledLogs: Option[Boolean] = None,\n                                             stemCells: Option[List[StemCell]] = None)\n\n  /**\n   * A stemcell configuration read from the manifest for a container image to be initialized by the container pool.\n   *\n   * @param initialCount  the initial number of stemcell containers to create\n   * @param memory the max memory this stemcell will allocate\n   * @param reactive the reactive prewarming prewarmed config, which is disabled by default\n   */\n  protected[entity] case class StemCell(initialCount: Int,\n                                        memory: ByteSize,\n                                        reactive: Option[ReactivePrewarmingConfig] = None) {\n    require(initialCount > 0, \"initialCount must be positive\")\n  }\n\n  /**\n   * A stemcell's ReactivePrewarmingConfig configuration\n   *\n   * @param minCount the max number of stemcell containers to exist\n   * @param maxCount  the max number of stemcell containers to create\n   * @param ttl time to live of the prewarmed container\n   * @param threshold the executed activation number of cold start in previous one minute\n   * @param increment increase per increment prewarmed number under per threshold activations\n   */\n  protected[core] case class ReactivePrewarmingConfig(minCount: Int,\n                                                      maxCount: Int,\n                                                      ttl: FiniteDuration,\n                                                      threshold: Int,\n                                                      increment: Int) {\n    require(\n      minCount >= 0 && minCount <= maxCount,\n      \"minCount must be be greater than 0 and less than or equal to maxCount\")\n    require(maxCount > 0, \"maxCount must be positive\")\n    require(ttl.toMillis > 0, \"ttl must be positive\")\n    require(threshold > 0, \"threshold must be positive\")\n    require(increment > 0 && increment <= maxCount, \"increment must be positive and less than or equal to maxCount\")\n  }\n\n  /**\n   * An image name for an action refers to the container image canonically as\n   * \"prefix/name[:tag]\" e.g., \"openwhisk/python3action:latest\".\n   */\n  protected[core] case class ImageName(name: String,\n                                       registry: Option[String] = None,\n                                       prefix: Option[String] = None,\n                                       tag: Option[String] = None) {\n\n    /**\n     * The actual name of the image for an action kind resolved by registry setting.\n     */\n    def resolveImageName(systemRegistry: Option[String] = None): String = {\n      val r = Option(registry.getOrElse(systemRegistry.getOrElse((\"\"))))\n        .filter(_.nonEmpty)\n        .map { reg =>\n          if (reg.endsWith(\"/\")) reg else reg + \"/\"\n        }\n        .getOrElse(\"\")\n      val p = prefix.filter(_.nonEmpty).map(_ + \"/\").getOrElse(\"\")\n      val t = tag.filter(_.nonEmpty).map(\":\" + _).getOrElse(\"\")\n      r + p + name + t\n    }\n\n    /**\n     * Overrides equals to allow match on undefined tag or when tag is latest\n     * in this or that.\n     */\n    override def equals(that: Any) = that match {\n      case ImageName(n, r, p, t) =>\n        name == n && registry == r && p == prefix && (t == tag || {\n          val thisTag = tag.getOrElse(ImageName.defaultImageTag)\n          val thatTag = t.getOrElse(ImageName.defaultImageTag)\n          thisTag == thatTag\n        })\n\n      case _ => false\n    }\n  }\n\n  protected[core] object ImageName {\n    private val defaultImageTag = \"latest\"\n\n    // docker image name grammar, taken from: https://github.com/docker/distribution/blob/master/reference/reference.go\n    //\n    // Grammar\n    //\n    // reference                       := name [ \":\" tag ] [ \"@\" digest ]\n    // name                            := [domain '/'] path-component ['/' path-component]*\n    // domain                          := domain-component ['.' domain-component]* [':' port-number]\n    // domain-component                := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/\n    // port-number                     := /[0-9]+/\n    // path-component                  := alpha-numeric [separator alpha-numeric]*\n    // alpha-numeric                   := /[a-z0-9]+/\n    // separator                       := /[_.]|__|[-]*/\n    //\n    // tag                             := /[\\w][\\w.-]{0,127}/\n    //\n    // digest                          := digest-algorithm \":\" digest-hex\n    // digest-algorithm                := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*\n    // digest-algorithm-separator      := /[+.-_]/\n    // digest-algorithm-component      := /[A-Za-z][A-Za-z0-9]*/\n    // digest-hex                      := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value\n    private def lowercaseLetters[_: P] = P(CharIn(\"a-z\"))\n    private def uppercaseLetters[_: P] = P(CharIn(\"a-Z\"))\n    private def letters[_: P] = P(lowercaseLetters | uppercaseLetters)\n    private def digits[_: P] = P(CharIn(\"0-9\"))\n\n    private def alphaNumeric[_: P] = P(lowercaseLetters | digits)\n    private def alphaNumericWithUpper[_: P] = P(letters | digits)\n    private def word[_: P] = P(alphaNumericWithUpper | \"_\")\n\n    private def digestHex[_: P] = P(digits | CharIn(\"a-fA-F\")).rep(32)\n    private def digestAlgorithmComponent[_: P] = P(letters ~ alphaNumericWithUpper.rep)\n    private def digestAlgorithmSeperator[_: P] = P(\"+\" | \".\" | \"-\" | \"_\")\n    private def digestAlgorithm[_: P] = P(digestAlgorithmComponent.rep(min = 1, sep = digestAlgorithmSeperator))\n    private def digest[_: P] = P(digestAlgorithm ~ \":\" ~ digestHex)\n\n    private def tag[_: P] = P(word ~ (word | \".\" | \"-\").rep(max = 127))\n\n    private def separator[_: P] = P(\"_\" | \".\" | \"__\" | \"-\".rep)\n    private def pathComponent[_: P] = P(alphaNumeric.rep(min = 1, sep = separator))\n    private def portNumber[_: P] = P(digits.rep(1))\n    // FIXME: this is not correct yet. It accepts \"-\" as the beginning and end of a domain\n    private def domainComponent[_: P] = P(alphaNumericWithUpper | \"-\").rep\n    private def domain[_: P] =\n      P((domainComponent\n        .rep(min = 2, sep = \".\") ~ (\":\" ~ portNumber).?) | (domainComponent.rep(min = 1, sep = \".\") ~ \":\" ~ portNumber))\n    private def name[_: P] = P((domain.! ~ \"/\").? ~ pathComponent.!.rep(min = 1, sep = \"/\"))\n\n    private def reference[_: P] = P(Start ~ name ~ (\":\" ~ tag.!).? ~ (\"@\" ~ digest.!).? ~ End)\n\n    /**\n     * Constructs an ImageName from a string. This method checks that the image name conforms\n     * to the Docker naming. As a result, failure to deserialize a string will throw an exception\n     * which fails the Try. Callers could use this to short-circuit operations (CRUD or activation).\n     * Internal container names use the proper constructor directly.\n     */\n    def fromString(s: String): Try[ImageName] = {\n      parse(s, reference(_)) match {\n        case Parsed.Success((registry, imagePathParts, imageTag, _), _) =>\n          // imagePathParts has at least one element per the parser above\n          val prefix = (imagePathParts.dropRight(1)).mkString(\"/\")\n          val imageName = imagePathParts.last\n\n          Success(ImageName(imageName, registry, if (prefix.nonEmpty) Some(prefix) else None, imageTag))\n        case Parsed.Failure(_, _, _) =>\n          Failure(DeserializationException(\"could not parse image name\"))\n      }\n    }\n  }\n\n  /**\n   * A runtime family manifest is a collection of runtimes grouped by a family (e.g., swift with versions swift:2 and swift:3).\n   *\n   * @param name runtime family\n   * @version set of runtime manifests\n   */\n  protected[entity] case class RuntimeFamily(name: String, versions: Set[RuntimeManifest])\n\n  /**\n   * A collection of runtime families.\n   *\n   * @param runtimes                 set of supported runtime families\n   * @param blackboxImages           set of blackbox container images\n   * @param bypassPullForLocalImages container image prefix that is exempted from docker pull operations\n   */\n  protected[core] case class Runtimes(runtimes: Set[RuntimeFamily],\n                                      blackboxImages: Set[ImageName],\n                                      bypassPullForLocalImages: Option[String]) {\n\n    val knownContainerRuntimes: Set[String] = runtimes.flatMap(_.versions.map(_.kind))\n\n    val manifests: Map[String, RuntimeManifest] = {\n      runtimes.flatMap {\n        _.versions.map { m =>\n          m.kind -> m\n        }\n      }.toMap\n    }\n\n    def skipDockerPull(image: ImageName): Boolean = {\n      blackboxImages.contains(image) ||\n      image.prefix.flatMap(p => bypassPullForLocalImages.map(_ == p)).getOrElse(false)\n    }\n\n    def toJson: JsObject = {\n      runtimes\n        .map { family =>\n          family.name -> family.versions.map {\n            case rt =>\n              JsObject(\n                \"kind\" -> rt.kind.toJson,\n                \"image\" -> rt.image.resolveImageName().toJson,\n                \"deprecated\" -> rt.deprecated.getOrElse(false).toJson,\n                \"default\" -> rt.default.getOrElse(false).toJson,\n                \"attached\" -> rt.attached.isDefined.toJson,\n                \"requireMain\" -> rt.requireMain.getOrElse(false).toJson)\n          }\n        }\n        .toMap\n        .toJson\n        .asJsObject\n    }\n\n    def resolveDefaultRuntime(kind: String): Option[RuntimeManifest] = {\n      kind match {\n        case defaultSplitter(family) => defaultRuntimes.get(family).flatMap(manifests.get(_))\n        case _                       => manifests.get(kind)\n      }\n    }\n\n    /**\n     * Collects all runtimes for which there is a stemcell configuration defined\n     *\n     * @return list of runtime manifests with stemcell configurations\n     */\n    def stemcells: Map[RuntimeManifest, List[StemCell]] = {\n      manifests\n        .flatMap {\n          case (_, m) => m.stemCells.map(m -> _)\n        }\n        .filter(_._2.nonEmpty)\n    }\n\n    private val defaultRuntimes: Map[String, String] = {\n      runtimes.map { family =>\n        family.versions.filter(_.default.exists(identity)).toList match {\n          case Nil if family.versions.size == 1 => family.name -> family.versions.head.kind\n          case Nil                              => throw new IllegalArgumentException(s\"${family.name} has multiple versions, but no default.\")\n          case d :: Nil                         => family.name -> d.kind\n          case ds =>\n            throw new IllegalArgumentException(s\"Found more than one default for ${family.name}: ${ds.mkString(\",\")}.\")\n        }\n      }.toMap\n    }\n\n    private val defaultSplitter = \"([a-z0-9]+):default\".r\n  }\n\n  protected[entity] implicit val imageNameSerdes: RootJsonFormat[ImageName] = jsonFormat4(ImageName.apply)\n\n  protected[entity] implicit val ttlSerdes: RootJsonFormat[FiniteDuration] = new RootJsonFormat[FiniteDuration] {\n    override def write(finiteDuration: FiniteDuration): JsValue = JsString(finiteDuration.toString)\n\n    override def read(value: JsValue): FiniteDuration = value match {\n      case JsString(s) =>\n        val duration = Duration(s)\n        FiniteDuration(duration.length, duration.unit)\n      case _ =>\n        deserializationError(\"time unit not supported. Only milliseconds, seconds, minutes, hours, days are supported\")\n    }\n  }\n\n  protected[entity] implicit val reactivePrewarmingConfigSerdes: RootJsonFormat[ReactivePrewarmingConfig] = jsonFormat5(\n    ReactivePrewarmingConfig.apply)\n\n  protected[entity] implicit val stemCellSerdes = new RootJsonFormat[StemCell] {\n    import org.apache.openwhisk.core.entity.size.serdes\n    val defaultSerdes = jsonFormat3(StemCell.apply)\n    override def read(value: JsValue): StemCell = {\n      val fields = value.asJsObject.fields\n      val initialCount: Option[Int] =\n        fields\n          .get(\"initialCount\")\n          .orElse(fields.get(\"count\"))\n          .map(_.convertTo[Int])\n      val memory: Option[ByteSize] = fields.get(\"memory\").map(_.convertTo[ByteSize])\n      val config = fields.get(\"reactive\").map(_.convertTo[ReactivePrewarmingConfig])\n\n      (initialCount, memory) match {\n        case (Some(c), Some(m)) => StemCell(c, m, config)\n        case (Some(c), None) =>\n          throw new IllegalArgumentException(s\"memory is required, just provide initialCount: ${c}\")\n        case (None, Some(m)) =>\n          throw new IllegalArgumentException(s\"initialCount is required, just provide memory: ${m.toString}\")\n        case _ => throw new IllegalArgumentException(\"both initialCount and memory are required\")\n      }\n    }\n\n    override def write(s: StemCell) = defaultSerdes.write(s)\n  }\n\n  protected[entity] implicit val runtimeManifestSerdes: RootJsonFormat[RuntimeManifest] = jsonFormat8(RuntimeManifest)\n\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/FullyQualifiedEntityName.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\n\nimport spray.json._\nimport org.apache.openwhisk.core.entity.size.SizeString\n\n/**\n * A FullyQualifiedEntityName (qualified name) is a triple consisting of\n * - EntityPath: the namespace and package where the entity is located\n * - EntityName: the name of the entity\n * - Version: the semantic version of the resource\n * - Binding : the entity path of the package binding, it can be used by entities that support binding\n */\nprotected[core] case class FullyQualifiedEntityName(path: EntityPath,\n                                                    name: EntityName,\n                                                    version: Option[SemVer] = None,\n                                                    binding: Option[EntityPath] = None)\n    extends ByteSizeable {\n  private val qualifiedName: String = path + EntityPath.PATHSEP + name\n\n  /** Resolves default namespace in path to given name if the root path is the default namespace. */\n  def resolve(namespace: EntityName) = FullyQualifiedEntityName(path.resolveNamespace(namespace), name, version)\n\n  /** @return full path including name, i.e., \"path/name\" */\n  def fullPath: EntityPath = path.addPath(name)\n\n  /**\n   * Creates new fully qualified entity name that shifts the name into the path and adds a new name:\n   * (p, n).add(x) -> (p/n, x).\n   *\n   * @return new fully qualified name\n   */\n  def add(n: EntityName) = FullyQualifiedEntityName(path.addPath(name), n)\n\n  def toDocId = new DocId(qualifiedName)\n  def namespace: EntityName = path.root\n  def qualifiedNameWithLeadingSlash: String = EntityPath.PATHSEP + qualifiedName\n  def asString = path.addPath(name) + version.map(\"@\" + _.toString).getOrElse(\"\")\n  def toStringWithoutVersion = path.addPath(name).asString\n  def serialize = FullyQualifiedEntityName.serdes.write(this).compactPrint\n\n  override def size = qualifiedName.sizeInBytes\n  override def toString = asString\n  override def hashCode = qualifiedName.hashCode\n}\n\nprotected[core] object FullyQualifiedEntityName extends DefaultJsonProtocol {\n  // must use jsonFormat with explicit field names and order because class extends a trait\n  private val caseClassSerdes = jsonFormat(FullyQualifiedEntityName.apply _, \"path\", \"name\", \"version\", \"binding\")\n\n  protected[core] val serdes = new RootJsonFormat[FullyQualifiedEntityName] {\n    def write(n: FullyQualifiedEntityName) = caseClassSerdes.write(n)\n\n    def read(value: JsValue) =\n      Try {\n        value match {\n          case JsObject(fields) => caseClassSerdes.read(value)\n          // tolerate dual serialization modes; Exec serializes a sequence of fully qualified names\n          // by their document id which excludes the version (hence it is just a string)\n          case JsString(name) => EntityPath(name).toFullyQualifiedEntityName\n          case _              => deserializationError(\"fully qualified name malformed\")\n        }\n      } match {\n        case Success(s)                           => s\n        case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)\n        case Failure(t)                           => deserializationError(\"fully qualified name malformed\")\n      }\n  }\n\n  // alternate serializer that drops version\n  protected[entity] val serdesAsDocId = new RootJsonFormat[FullyQualifiedEntityName] {\n    def write(n: FullyQualifiedEntityName) = n.toDocId.toJson\n    def read(value: JsValue) =\n      Try {\n        value match {\n          case JsString(name) => EntityPath(name).toFullyQualifiedEntityName\n          case _              => deserializationError(\"fully qualified name malformed\")\n        }\n      } match {\n        case Success(s)                           => s\n        case Failure(t: IllegalArgumentException) => deserializationError(t.getMessage)\n        case Failure(t)                           => deserializationError(\"fully qualified name malformed\")\n      }\n  }\n\n  protected[core] def parse(msg: String) = Try(serdes.read(msg.parseJson))\n\n  /**\n   * Converts the name to a fully qualified name.\n   * There are 3 cases:\n   * - name is not a valid EntityPath => error\n   * - name is a valid single segment with a leading slash => error\n   * - name is a valid single segment without a leading slash => map it to user namespace, default package\n   * - name is a valid multi segment with a leading slash => treat it as fully qualified name (max segments allowed: 3)\n   * - name is a valid multi segment without a leading slash => treat it as package name and resolve it to the user namespace (max segments allowed: 3)\n   *\n   * The last case is ambiguous as '/namespace/action' and 'package/action' will be the same EntityPath value.\n   * The action should use a fully qualified result to avoid the ambiguity.\n   *\n   * @param name name of the action to fully qualify\n   * @param namespace the user namespace for the simple resolution\n   * @return Some(FullyQualifiedName) if the name is valid otherwise None\n   */\n  protected[core] def resolveName(name: JsValue, namespace: EntityName): Option[FullyQualifiedEntityName] = {\n    name match {\n      case v @ JsString(s) =>\n        Try(v.convertTo[EntityPath]).toOption\n          .flatMap { path =>\n            val n = path.segments\n            val leadingSlash = s.startsWith(EntityPath.PATHSEP)\n            if (n < 1 || n > 3 || (leadingSlash && n == 1) || (!leadingSlash && n > 3)) None\n            else if (leadingSlash || n == 3) Some(path)\n            else Some(namespace.toPath.addPath(path))\n          }\n          .map(_.resolveNamespace(namespace).toFullyQualifiedEntityName)\n      case _ => None\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/GenericAuthKey.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport org.apache.pekko.http.scaladsl.model.headers.HttpCredentials\nimport spray.json._\n\n/**\n * Base class for Authentication\n *\n * This is used to transport data generated by the authentication directive to the invoker.\n * The invoker can pass this data to the action container.\n *\n * Be aware that this class itself cannot be used to provide meaningful authentication.\n */\nprotected[core] class GenericAuthKey(val toEnvironment: JsObject) {\n  def getCredentials: Option[HttpCredentials] = None\n  def canEqual(a: Any) = a.isInstanceOf[GenericAuthKey]\n  override def equals(that: Any): Boolean =\n    that match {\n      case that: GenericAuthKey => (this.toEnvironment == that.asInstanceOf[GenericAuthKey].toEnvironment)\n      case _                    => false\n    }\n}\n\nprotected[core] object GenericAuthKey {\n\n  protected[core] implicit val serdes: RootJsonFormat[GenericAuthKey] = new RootJsonFormat[GenericAuthKey] {\n    def write(k: GenericAuthKey) = k.toEnvironment\n    def read(value: JsValue) = new GenericAuthKey(value.asJsObject)\n  }\n\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/Identity.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.database.{\n  MultipleReadersSingleWriterCache,\n  NoDocumentException,\n  StaleParameter,\n  WriteTime\n}\nimport org.apache.openwhisk.core.entitlement.Privilege\nimport org.apache.openwhisk.core.entity.types.AuthStore\nimport spray.json._\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration.FiniteDuration\nimport scala.util.Try\n\ncase class UserLimits(invocationsPerMinute: Option[Int] = None,\n                      concurrentInvocations: Option[Int] = None,\n                      firesPerMinute: Option[Int] = None,\n                      allowedKinds: Option[Set[String]] = None,\n                      storeActivations: Option[Boolean] = None,\n                      minActionMemory: Option[MemoryLimit] = None,\n                      maxActionMemory: Option[MemoryLimit] = None,\n                      minActionLogs: Option[LogLimit] = None,\n                      maxActionLogs: Option[LogLimit] = None,\n                      minActionTimeout: Option[TimeLimit] = None,\n                      maxActionTimeout: Option[TimeLimit] = None,\n                      minActionConcurrency: Option[IntraConcurrencyLimit] = None,\n                      maxActionConcurrency: Option[IntraConcurrencyLimit] = None,\n                      maxParameterSize: Option[ByteSize] = None,\n                      maxPayloadSize: Option[ByteSize] = None,\n                      truncationSize: Option[ByteSize] = None,\n                      warmedContainerKeepingCount: Option[Int] = None,\n                      warmedContainerKeepingTimeout: Option[String] = None,\n                      maxActionInstances: Option[Int] = None) {\n\n  def allowedMaxParameterSize: ByteSize = {\n    val namespaceLimit = maxParameterSize getOrElse (Parameters.MAX_SIZE_DEFAULT)\n    if (namespaceLimit > Parameters.MAX_SIZE) {\n      Parameters.MAX_SIZE\n    } else namespaceLimit\n  }\n\n  def allowedMaxPayloadSize: ByteSize = {\n    val namespaceLimit = maxPayloadSize getOrElse (ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT_DEFAULT)\n    if (namespaceLimit > ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT) {\n      ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT\n    } else namespaceLimit\n  }\n\n  def allowedTruncationSize: ByteSize = {\n    val namespaceLimit = truncationSize getOrElse (ActivationEntityLimit.MAX_ACTIVATION_ENTITY_TRUNCATION_LIMIT_DEFAULT)\n    if (namespaceLimit > ActivationEntityLimit.MAX_ACTIVATION_ENTITY_TRUNCATION_LIMIT) {\n      ActivationEntityLimit.MAX_ACTIVATION_ENTITY_TRUNCATION_LIMIT\n    } else namespaceLimit\n  }\n\n  def allowedMaxActionConcurrency: Int = {\n    val namespaceLimit = maxActionConcurrency.map(_.maxConcurrent) getOrElse (IntraConcurrencyLimit.MAX_CONCURRENT_DEFAULT)\n    if (namespaceLimit > IntraConcurrencyLimit.MAX_CONCURRENT) {\n      IntraConcurrencyLimit.MAX_CONCURRENT\n    } else namespaceLimit\n  }\n\n  def allowedMinActionConcurrency: Int = {\n    val namespaceLimit = minActionConcurrency.map(_.maxConcurrent) getOrElse (IntraConcurrencyLimit.MIN_CONCURRENT_DEFAULT)\n    if (namespaceLimit < IntraConcurrencyLimit.MIN_CONCURRENT) {\n      IntraConcurrencyLimit.MIN_CONCURRENT\n    } else namespaceLimit\n  }\n\n  def allowedMaxActionMemory: ByteSize = {\n    val namespaceLimit = maxActionMemory.map(_.toByteSize) getOrElse (MemoryLimit.MAX_MEMORY_DEFAULT)\n    if (namespaceLimit > MemoryLimit.MAX_MEMORY) {\n      MemoryLimit.MAX_MEMORY\n    } else namespaceLimit\n  }\n\n  def allowedMinActionMemory: ByteSize = {\n    val namespaceLimit = minActionMemory.map(_.toByteSize) getOrElse (MemoryLimit.MIN_MEMORY_DEFAULT)\n    if (namespaceLimit < MemoryLimit.MIN_MEMORY) {\n      MemoryLimit.MIN_MEMORY\n    } else namespaceLimit\n  }\n\n  def allowedMaxActionLogs: ByteSize = {\n    val namespaceLogsMax = maxActionLogs.map(_.toByteSize) getOrElse (LogLimit.MAX_LOGSIZE_DEFAULT)\n    if (namespaceLogsMax > LogLimit.MAX_LOGSIZE) {\n      LogLimit.MAX_LOGSIZE\n    } else namespaceLogsMax\n  }\n\n  def allowedMinActionLogs: ByteSize = {\n    val namespaceLimit = minActionLogs.map(_.toByteSize) getOrElse (LogLimit.MIN_LOGSIZE_DEFAULT)\n    if (namespaceLimit < LogLimit.MIN_LOGSIZE) {\n      LogLimit.MIN_LOGSIZE\n    } else namespaceLimit\n  }\n\n  def allowedMaxActionTimeout: FiniteDuration = {\n    val namespaceLimit = maxActionTimeout.map(_.duration) getOrElse (TimeLimit.MAX_DURATION_DEFAULT)\n    if (namespaceLimit > TimeLimit.MAX_DURATION) {\n      TimeLimit.MAX_DURATION\n    } else namespaceLimit\n  }\n\n  def allowedMinActionTimeout: FiniteDuration = {\n    val namespaceLimit = minActionTimeout.map(_.duration) getOrElse (TimeLimit.MIN_DURATION_DEFAULT)\n    if (namespaceLimit < TimeLimit.MIN_DURATION) {\n      TimeLimit.MIN_DURATION\n    } else namespaceLimit\n  }\n}\n\nobject UserLimits extends DefaultJsonProtocol {\n  val standardUserLimits = UserLimits()\n  private implicit val byteSizeSerdes = size.serdes\n  implicit val serdes = jsonFormat19(UserLimits.apply)\n}\n\nprotected[core] case class Namespace(name: EntityName, uuid: UUID)\n\nprotected[core] object Namespace extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat2(Namespace.apply)\n}\n\nprotected[core] case class Identity(subject: Subject,\n                                    namespace: Namespace,\n                                    authkey: GenericAuthKey,\n                                    rights: Set[Privilege] = Set.empty,\n                                    limits: UserLimits = UserLimits.standardUserLimits)\n\nobject Identity extends MultipleReadersSingleWriterCache[Option[Identity], DocInfo] with DefaultJsonProtocol {\n\n  private val viewName = WhiskQueries.view(WhiskQueries.dbConfig.subjectsDdoc, \"identities\").name\n\n  override val cacheEnabled = true\n  override val evictionPolicy = WriteTime\n  // upper bound for the auth cache to prevent memory pollution by sending\n  // malicious namespace patterns\n  override val fixedCacheSize = 100000\n\n  implicit val serdes = jsonFormat5(Identity.apply)\n\n  /**\n   * Retrieves a key for namespace.\n   * There may be more than one key for the namespace, in which case,\n   * one is picked arbitrarily.\n   */\n  def get(datastore: AuthStore, namespace: EntityName)(implicit transid: TransactionId): Future[Identity] = {\n    implicit val logger: Logging = datastore.logging\n    implicit val ec = datastore.executionContext\n    val ns = namespace.asString\n    val key = CacheKey(namespace)\n\n    cacheLookup(\n      key, {\n        list(datastore, List(ns), limit = 1) map { list =>\n          list.length match {\n            case 1 =>\n              Some(rowToIdentity(list.head, ns))\n            case 0 =>\n              logger.info(this, s\"$viewName[$namespace] does not exist\")\n              None\n            case _ =>\n              logger.error(this, s\"$viewName[$namespace] is not unique\")\n              throw new IllegalStateException(\"namespace is not unique\")\n          }\n        }\n      }).map(_.getOrElse(throw new NoDocumentException(\"namespace does not exist\")))\n  }\n\n  def get(datastore: AuthStore, authkey: BasicAuthenticationAuthKey)(\n    implicit transid: TransactionId): Future[Identity] = {\n    implicit val logger: Logging = datastore.logging\n    implicit val ec = datastore.executionContext\n\n    cacheLookup(\n      CacheKey(authkey), {\n        list(datastore, List(authkey.uuid.asString, authkey.key.asString)) map { list =>\n          list.length match {\n            case 1 =>\n              Some(rowToIdentity(list.head, authkey.uuid.asString))\n            case 0 =>\n              logger.info(this, s\"$viewName[${authkey.uuid}] does not exist\")\n              None\n            case _ =>\n              logger.error(this, s\"$viewName[${authkey.uuid}] is not unique\")\n              throw new IllegalStateException(\"uuid is not unique\")\n          }\n        }\n      }).map(_.getOrElse(throw new NoDocumentException(\"namespace does not exist\")))\n  }\n\n  def list(datastore: AuthStore, key: List[Any], limit: Int = 2)(\n    implicit transid: TransactionId): Future[List[JsObject]] = {\n    datastore.query(\n      viewName,\n      startKey = key,\n      endKey = key,\n      skip = 0,\n      limit = limit,\n      includeDocs = true,\n      descending = true,\n      reduce = false,\n      stale = StaleParameter.No)\n  }\n\n  protected[entity] def rowToIdentity(row: JsObject, key: String)(implicit transid: TransactionId, logger: Logging) = {\n    row.getFields(\"id\", \"value\", \"doc\") match {\n      case Seq(JsString(id), JsObject(value), doc) =>\n        val limits =\n          if (doc != JsNull) Try(doc.convertTo[UserLimits]).getOrElse(UserLimits.standardUserLimits)\n          else UserLimits.standardUserLimits\n        val subject = Subject(id)\n        val JsString(uuid) = value(\"uuid\")\n        val JsString(secret) = value(\"key\")\n        val JsString(namespace) = value(\"namespace\")\n        Identity(\n          subject,\n          Namespace(EntityName(namespace), UUID(uuid)),\n          BasicAuthenticationAuthKey(UUID(uuid), Secret(secret)),\n          Privilege.ALL,\n          limits)\n      case _ =>\n        logger.error(this, s\"$viewName[$key] has malformed view '${row.compactPrint}'\")\n        throw new IllegalStateException(\"identities view malformed\")\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/InstanceConcurrencyLimit.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport org.apache.openwhisk.http.Messages\n\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport spray.json._\n\n/**\n * InstanceConcurrencyLimit encapsulates max allowed container concurrency for an action within a given namespace.\n * A user is given a max concurrency for their entire namespace, but this doesn't allow for any fairness across their actions\n * during load spikes. This action limit allows a user to specify max container concurrency for a specific action within the\n * constraints of their namespace limit. By default, this limit does not exist and therefore the namespace concurrency limit is used.\n * The allowed range is thus [1, namespaceConcurrencyLimit]. If this config is not used by any actions, then the default behavior\n * of openwhisk continues in which any action can use the entire concurrency limit of the namespace. The limit less than namespace\n * limit check occurs at the api level.\n *\n * NOTE: This limit is only leveraged on openwhisk v2 with the scheduler service. If this limit is set on a deployment of openwhisk\n * not using the scheduler service, the limit will do nothing.\n *\n *\n * @param maxConcurrentInstances the max number of concurrent activations in a single container\n */\nprotected[entity] class InstanceConcurrencyLimit private (val maxConcurrentInstances: Int) extends AnyVal\n\nprotected[core] object InstanceConcurrencyLimit extends ArgNormalizer[InstanceConcurrencyLimit] {\n\n  /** These values are set once at the beginning. Dynamic configuration updates are not supported at the moment. */\n  protected[core] val MIN_INSTANCES_LIMIT: Int = 0\n\n  /**\n   * Creates ContainerConcurrencyLimit for limit, iff limit is within permissible range.\n   *\n   * @param maxConcurrenctInstances the limit, must be within permissible range\n   * @return ConcurrencyLimit with limit set\n   * @throws IllegalArgumentException if limit does not conform to requirements\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(maxConcurrenctInstances: Int): InstanceConcurrencyLimit = {\n    require(\n      maxConcurrenctInstances >= MIN_INSTANCES_LIMIT,\n      Messages.belowMinAllowedActionInstanceConcurrency(MIN_INSTANCES_LIMIT))\n    new InstanceConcurrencyLimit(maxConcurrenctInstances)\n  }\n\n  override protected[core] implicit val serdes = new RootJsonFormat[InstanceConcurrencyLimit] {\n    def write(m: InstanceConcurrencyLimit) = JsNumber(m.maxConcurrentInstances)\n\n    def read(value: JsValue) = {\n      Try {\n        val JsNumber(c) = value\n        require(c.isWhole, \"instance concurrency limit must be whole number\")\n\n        InstanceConcurrencyLimit(c.toInt)\n      } match {\n        case Success(limit)                       => limit\n        case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)\n        case Failure(e: Throwable)                => deserializationError(\"instance concurrency limit malformed\", e)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/InstanceId.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport spray.json.{deserializationError, DefaultJsonProtocol, JsNumber, JsObject, JsString, JsValue, RootJsonFormat}\nimport spray.json._\n\nimport scala.collection.mutable.ListBuffer\nimport scala.util.Try\n\n/**\n * An instance id representing an invoker\n *\n * @param instance a numeric value used for the load balancing and Kafka topic creation\n * @param uniqueName an identifier required for dynamic instance assignment by Zookeeper\n * @param displayedName an identifier that is required for the health protocol to correlate Kafka topics with invoker container names\n * @param userMemory invoker user memory\n * @param busyMemory invoker busy memory\n * @param tags actions which included specified annotation tags can be run on this invoker\n * @param dedicatedNamespaces only dedicatedNamespaces's actions can be run on this invoker\n */\ncase class InvokerInstanceId(val instance: Int,\n                             uniqueName: Option[String] = None,\n                             displayedName: Option[String] = None,\n                             val userMemory: ByteSize,\n                             val busyMemory: Option[ByteSize] = None,\n                             val tags: Seq[String] = Seq.empty[String],\n                             val dedicatedNamespaces: Seq[String] = Seq.empty)\n    extends InstanceId {\n  def toInt: Int = instance\n\n  override val instanceType = \"invoker\"\n\n  override val source = s\"$instanceType$instance\"\n\n  override val toString: String = (Seq(\"invoker\" + instance) ++ uniqueName ++ displayedName).mkString(\"/\")\n\n  override val toJson: JsValue = InvokerInstanceId.serdes.write(this)\n}\n\ncase class ControllerInstanceId(asString: String) extends InstanceId {\n  validate(asString)\n  override val instanceType = \"controller\"\n\n  override val source = s\"$instanceType$asString\"\n\n  override val toString: String = source\n\n  override val toJson: JsValue = ControllerInstanceId.serdes.write(this)\n}\n\ncase class SchedulerInstanceId(val asString: String) extends InstanceId {\n  validate(asString)\n  override val instanceType = \"scheduler\"\n\n  override val source = s\"$instanceType$asString\"\n\n  override val toString: String = source\n\n  override val toJson: JsValue = SchedulerInstanceId.serdes.write(this)\n}\n\nobject InvokerInstanceId extends DefaultJsonProtocol {\n  def parse(c: String): Try[InvokerInstanceId] = Try(serdes.read(c.parseJson))\n\n  implicit val serdes = new RootJsonFormat[InvokerInstanceId] {\n    override def write(i: InvokerInstanceId): JsValue = {\n      val fields = new ListBuffer[(String, JsValue)]\n      fields ++= List(\"instance\" -> JsNumber(i.instance))\n      fields ++= List(\"userMemory\" -> JsString(i.userMemory.toString))\n      i.busyMemory.foreach { busyMemory =>\n        fields ++= List(\"busyMemory\" -> JsString(busyMemory.toString))\n      }\n      fields ++= List(\"instanceType\" -> JsString(i.instanceType))\n      fields ++= List(\"tags\" -> JsArray(i.tags.map(_.toJson): _*))\n      fields ++= List(\"dedicatedNamespaces\" -> JsArray(i.dedicatedNamespaces.map(_.toJson): _*))\n      i.uniqueName.foreach(uniqueName => fields ++= List(\"uniqueName\" -> JsString(uniqueName)))\n      i.displayedName.foreach(displayedName => fields ++= List(\"displayedName\" -> JsString(displayedName)))\n      JsObject(fields.toSeq: _*)\n    }\n\n    override def read(json: JsValue): InvokerInstanceId = {\n      val instance = fromField[Int](json, \"instance\")\n      val uniqueName = fromField[Option[String]](json, \"uniqueName\")\n      val displayedName = fromField[Option[String]](json, \"displayedName\")\n      val userMemory = fromField[String](json, \"userMemory\")\n      val busyMemory = fromField[Option[String]](json, \"busyMemory\")\n      val instanceType = fromField[String](json, \"instanceType\")\n      val tags = fromField[Seq[String]](json, \"tags\")\n      val dedicatedNamespaces = fromField[Seq[String]](json, \"dedicatedNamespaces\")\n\n      if (instanceType == \"invoker\") {\n        new InvokerInstanceId(\n          instance,\n          uniqueName,\n          displayedName,\n          ByteSize.fromString(userMemory),\n          busyMemory.map(ByteSize.fromString(_)),\n          tags,\n          dedicatedNamespaces)\n      } else {\n        deserializationError(\"could not read InvokerInstanceId\")\n      }\n    }\n  }\n\n}\n\nobject ControllerInstanceId extends DefaultJsonProtocol {\n  def parse(c: String): Try[ControllerInstanceId] = Try(serdes.read(c.parseJson))\n\n  implicit val serdes = new RootJsonFormat[ControllerInstanceId] {\n    override def write(c: ControllerInstanceId): JsValue =\n      JsObject(\"asString\" -> JsString(c.asString), \"instanceType\" -> JsString(c.instanceType))\n\n    override def read(json: JsValue): ControllerInstanceId = {\n      json.asJsObject.getFields(\"asString\", \"instanceType\") match {\n        case Seq(JsString(asString), JsString(instanceType)) =>\n          if (instanceType == \"controller\") {\n            new ControllerInstanceId(asString)\n          } else {\n            deserializationError(\"could not read ControllerInstanceId\")\n          }\n        case Seq(JsString(asString)) =>\n          new ControllerInstanceId(asString)\n        case _ =>\n          deserializationError(\"could not read ControllerInstanceId\")\n      }\n    }\n  }\n}\n\nobject SchedulerInstanceId extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat(SchedulerInstanceId.apply _, \"asString\")\n}\n\ntrait InstanceId {\n\n  // controller ids become part of a kafka topic, hence, hence allow only certain characters\n  // see https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/internals/Topic.java#L29\n  private val LEGAL_CHARS = \"[a-zA-Z0-9._-]+\"\n\n  // reserve some number of characters as the prefix to be added to topic names\n  private val MAX_NAME_LENGTH = 249 - 121\n\n  def serialize: String = InstanceId.serdes.write(this).compactPrint\n\n  def validate(asString: String): Unit =\n    require(\n      asString.length <= MAX_NAME_LENGTH && asString.matches(LEGAL_CHARS),\n      s\"$instanceType instance id contains invalid characters\")\n\n  val instanceType: String\n\n  val source: String\n\n  val toJson: JsValue\n}\n\nobject InstanceId extends DefaultJsonProtocol {\n  def parse(i: String): Try[InstanceId] = Try(serdes.read(i.parseJson))\n\n  implicit val serdes = new RootJsonFormat[InstanceId] {\n    override def write(i: InstanceId): JsValue = i.toJson\n\n    override def read(json: JsValue): InstanceId = {\n      val JsObject(field) = json\n      field\n        .get(\"instanceType\")\n        .map(_.convertTo[String] match {\n          case \"invoker\" =>\n            json.convertTo[InvokerInstanceId]\n          case \"controller\" =>\n            json.convertTo[ControllerInstanceId]\n          case _ =>\n            deserializationError(\"could not read InstanceId\")\n        })\n        .getOrElse(deserializationError(\"could not read InstanceId\"))\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/IntraConcurrencyLimit.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport com.typesafe.config.ConfigFactory\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.http.Messages\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport spray.json._\n\ncase class NamespaceIntraConcurrencyLimitConfig(min: Int, max: Int)\ncase class IntraConcurrencyLimitConfig(min: Int, max: Int, std: Int)\n\n/**\n * IntraConcurrencyLimit encapsulates allowed concurrency in a single container for an action. The limit must be within a\n * permissible range (by default [1, 1]). This default range was chosen intentionally to reflect that concurrency\n * is disabled by default.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param maxConcurrent the max number of concurrent activations in a single container\n */\nprotected[entity] class IntraConcurrencyLimit private (val maxConcurrent: Int) extends AnyVal {\n\n  /** It checks the namespace memory limit setting value  */\n  @throws[ActionConcurrencyLimitException]\n  protected[core] def checkNamespaceLimit(user: Identity): Unit = {\n    val concurrencyMax = user.limits.allowedMaxActionConcurrency\n    val concurrencyMin = user.limits.allowedMinActionConcurrency\n    try {\n      require(\n        maxConcurrent <= concurrencyMax,\n        Messages.concurrencyExceedsAllowedThreshold(maxConcurrent, concurrencyMax))\n      require(maxConcurrent >= concurrencyMin, Messages.concurrencyBelowAllowedThreshold(maxConcurrent, concurrencyMin))\n    } catch {\n      case e: IllegalArgumentException => throw ActionConcurrencyLimitException(e.getMessage)\n    }\n  }\n}\n\nprotected[core] object IntraConcurrencyLimit extends ArgNormalizer[IntraConcurrencyLimit] {\n  //since tests require override to the default config, load the \"test\" config, with fallbacks to default\n  val config = ConfigFactory.load().getConfig(\"test\")\n  private val concurrencyConfig =\n    loadConfigWithFallbackOrThrow[IntraConcurrencyLimitConfig](config, ConfigKeys.concurrencyLimit)\n  private val namespaceConcurrencyDefaultConfig = try {\n    loadConfigWithFallbackOrThrow[NamespaceIntraConcurrencyLimitConfig](config, ConfigKeys.namespaceConcurrencyLimit)\n  } catch {\n    case _: Throwable =>\n      // Supports backwards compatibility for openwhisk that do not use the namespace default limit\n      NamespaceIntraConcurrencyLimitConfig(concurrencyConfig.min, concurrencyConfig.max)\n  }\n\n  /**\n   * These system limits and namespace default limits are set once at the beginning.\n   * Dynamic configuration updates are not supported at the moment.\n   */\n  protected[core] val MIN_CONCURRENT: Int = concurrencyConfig.min\n  protected[core] val MAX_CONCURRENT: Int = concurrencyConfig.max\n  protected[core] val STD_CONCURRENT: Int = concurrencyConfig.std\n\n  /** Default namespace limit used if there is no namespace-specific limit */\n  protected[core] val MIN_CONCURRENT_DEFAULT: Int = namespaceConcurrencyDefaultConfig.min\n  protected[core] val MAX_CONCURRENT_DEFAULT: Int = namespaceConcurrencyDefaultConfig.max\n\n  require(\n    MAX_CONCURRENT >= MAX_CONCURRENT_DEFAULT,\n    \"The system max limit must be greater than the namespace max limit.\")\n  require(MIN_CONCURRENT <= MIN_CONCURRENT_DEFAULT, \"The system min limit must be less than the namespace min limit.\")\n\n  /** A singleton ConcurrencyLimit with default value */\n  protected[core] val standardConcurrencyLimit = IntraConcurrencyLimit(STD_CONCURRENT)\n\n  /** Gets ConcurrencyLimit with default value */\n  protected[core] def apply(): IntraConcurrencyLimit = standardConcurrencyLimit\n\n  /**\n   * Creates ConcurrencyLimit for limit, iff limit is within permissible range.\n   *\n   * @param concurrency the limit, must be within permissible range\n   * @return ConcurrencyLimit with limit set\n   * @throws IllegalArgumentException if limit does not conform to requirements\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(concurrency: Int): IntraConcurrencyLimit = {\n    new IntraConcurrencyLimit(concurrency)\n  }\n\n  override protected[core] implicit val serdes = new RootJsonFormat[IntraConcurrencyLimit] {\n    def write(m: IntraConcurrencyLimit) = JsNumber(m.maxConcurrent)\n\n    def read(value: JsValue) = {\n      Try {\n        val JsNumber(c) = value\n        require(c.isWhole, \"intra concurrency limit must be whole number\")\n\n        IntraConcurrencyLimit(c.toInt)\n      } match {\n        case Success(limit)                       => limit\n        case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)\n        case Failure(e: Throwable)                => deserializationError(\"concurrency limit malformed\", e)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/Limits.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport scala.util.Try\nimport spray.json.JsValue\nimport spray.json.RootJsonFormat\nimport spray.json.deserializationError\nimport spray.json.DefaultJsonProtocol\n\n/**\n * Abstract type for limits on triggers and actions. This may\n * expand to include global limits as well (for example limits\n * that require global knowledge).\n */\nprotected[entity] abstract class Limits {\n  protected[entity] def toJson: JsValue\n  override def toString = toJson.compactPrint\n}\n\n/**\n * Limits on a specific action. Includes the following properties\n * {\n *   timeout: maximum duration in msecs an action is allowed to consume in [100 msecs, 5 minutes],\n *   memory: maximum memory in megabytes an action is allowed to consume within namespace limit, default [128 MB, 512 MB],\n *   logs: maximum logs line in megabytes an action is allowed to generate [10 MB],\n *   concurrency: maximum number of concurrently processed activations per container [1, 200]\n * }\n *\n * @param timeout the duration in milliseconds, assured to be non-null because it is a value\n * @param memory the memory limit in megabytes, assured to be non-null because it is a value\n * @param logs the limit for logs written by the container and stored in the activation record, assured to be non-null because it is a value\n * @param concurrency the limit on concurrently processed activations per container, assured to be non-null because it is a value\n * @param instances the limit in which an action can scale up to within the confines of the namespace's concurrency limit\n */\nprotected[core] case class ActionLimits(timeout: TimeLimit = TimeLimit(),\n                                        memory: MemoryLimit = MemoryLimit(),\n                                        logs: LogLimit = LogLimit(),\n                                        concurrency: IntraConcurrencyLimit = IntraConcurrencyLimit(),\n                                        instances: Option[InstanceConcurrencyLimit] = None)\n    extends Limits {\n  override protected[entity] def toJson = ActionLimits.serdes.write(this)\n\n  @throws[ActionLimitsException]\n  def checkLimits(user: Identity): Unit = {\n    timeout.checkNamespaceLimit(user)\n    memory.checkNamespaceLimit(user)\n    concurrency.checkNamespaceLimit(user)\n    logs.checkNamespaceLimit(user)\n  }\n}\n\n/**\n * Limits on a specific trigger. None yet.\n */\nprotected[core] case class TriggerLimits protected[core] () extends Limits {\n  override protected[entity] def toJson: JsValue = TriggerLimits.serdes.write(this)\n}\n\nprotected[core] object ActionLimits extends ArgNormalizer[ActionLimits] with DefaultJsonProtocol {\n\n  override protected[core] implicit val serdes = new RootJsonFormat[ActionLimits] {\n    val helper = jsonFormat5(ActionLimits.apply)\n\n    def read(value: JsValue) = {\n      val obj = Try {\n        value.asJsObject.convertTo[Map[String, JsValue]]\n      } getOrElse deserializationError(\"no valid json object passed\")\n\n      val time = TimeLimit.serdes.read(obj.getOrElse(\"timeout\", deserializationError(\"'timeout' is missing\")))\n      val memory = MemoryLimit.serdes.read(obj.getOrElse(\"memory\", deserializationError(\"'memory' is missing\")))\n      val logs = obj.get(\"logs\") map { LogLimit.serdes.read } getOrElse LogLimit()\n      val concurrency = obj.get(\"concurrency\") map { IntraConcurrencyLimit.serdes.read } getOrElse IntraConcurrencyLimit()\n      val instances = obj.get(\"instances\") map { InstanceConcurrencyLimit.serdes.read }\n      ActionLimits(time, memory, logs, concurrency, instances)\n    }\n\n    def write(a: ActionLimits) = helper.write(a)\n  }\n}\n\nprotected[core] object TriggerLimits extends ArgNormalizer[TriggerLimits] with DefaultJsonProtocol {\n\n  override protected[core] implicit val serdes = jsonFormat0(TriggerLimits.apply _)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/LimitsExceptions.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nsealed abstract class ActionLimitsException(message: String) extends IllegalArgumentException(message)\n\ncase class ActionTimeLimitException(message: String) extends ActionLimitsException(message)\n\ncase class ActionMemoryLimitException(message: String) extends ActionLimitsException(message)\n\ncase class ActionLogLimitException(message: String) extends ActionLimitsException(message)\n\ncase class ActionConcurrencyLimitException(message: String) extends ActionLimitsException(message)\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/LogLimit.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.language.postfixOps\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport spray.json._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\n\ncase class LogLimitConfig(min: ByteSize, max: ByteSize, std: ByteSize)\n\n/**\n * LogLimit encapsulates allowed amount of logs written by an action.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * Argument type is Int because of JSON deserializer vs. <code>ByteSize</code> and\n * compatibility with <code>MemoryLimit</code>.\n *\n * @param megabytes the log limit in megabytes for the action\n */\nprotected[core] class LogLimit private (val megabytes: Int) extends AnyVal {\n  protected[core] def asMegaBytes: ByteSize = megabytes.megabytes\n\n  def toByteSize: ByteSize = ByteSize(megabytes, SizeUnits.MB)\n\n  /** It checks the namespace memory limit setting value  */\n  @throws[ActionLogLimitException]\n  protected[core] def checkNamespaceLimit(user: Identity): Unit = {\n    val logMax = user.limits.allowedMaxActionLogs\n    val logMin = user.limits.allowedMinActionLogs\n    try {\n      require(\n        megabytes <= logMax.toMB,\n        Messages.sizeExceedsAllowedThreshold(LogLimit.logLimitFieldName, megabytes, logMax.toMB.toInt))\n      require(\n        megabytes >= logMin.toMB,\n        Messages.sizeBelowAllowedThreshold(LogLimit.logLimitFieldName, megabytes, logMin.toMB.toInt))\n    } catch {\n      case e: IllegalArgumentException => throw ActionLogLimitException(e.getMessage)\n    }\n  }\n}\n\nprotected[core] object LogLimit extends ArgNormalizer[LogLimit] {\n  val config = loadConfigOrThrow[MemoryLimitConfig](ConfigKeys.logLimit)\n  val namespaceDefaultConfig = try {\n    loadConfigOrThrow[NamespaceMemoryLimitConfig](ConfigKeys.namespaceLogLimit)\n  } catch {\n    case _: Throwable =>\n      // Supports backwards compatibility for openwhisk that do not use the namespace default limit\n      NamespaceMemoryLimitConfig(config.min, config.max)\n  }\n  val logLimitFieldName = \"log\"\n\n  /**\n   * These system limits and namespace default limits are set once at the beginning.\n   * Dynamic configuration updates are not supported at the moment.\n   */\n  protected[core] val MIN_LOGSIZE: ByteSize = config.min\n  protected[core] val MAX_LOGSIZE: ByteSize = config.max\n  protected[core] val STD_LOGSIZE: ByteSize = config.std\n\n  /** Default log limit used if there is no namespace-specific limit */\n  protected[core] val MIN_LOGSIZE_DEFAULT: ByteSize = namespaceDefaultConfig.min\n  protected[core] val MAX_LOGSIZE_DEFAULT: ByteSize = namespaceDefaultConfig.max\n\n  require(MAX_LOGSIZE >= MAX_LOGSIZE_DEFAULT, \"The system max limit must be greater than the namespace max limit.\")\n  require(MIN_LOGSIZE <= MIN_LOGSIZE_DEFAULT, \"The system min limit must be less than the namespace min limit.\")\n\n  /** A singleton LogLimit with default value */\n  protected[core] val standardLogLimit = LogLimit(STD_LOGSIZE)\n\n  /** Gets LogLimit with default log limit */\n  protected[core] def apply(): LogLimit = standardLogLimit\n\n  /**\n   * Creates LogLimit for limit. Only the default limit is allowed currently.\n   *\n   * @param megabytes the limit in megabytes, must be within permissible range\n   * @return LogLimit with limit set\n   * @throws IllegalArgumentException if limit does not conform to requirements\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(megabytes: ByteSize): LogLimit = {\n    new LogLimit(megabytes.toMB.toInt)\n  }\n\n  override protected[core] implicit val serdes = new RootJsonFormat[LogLimit] {\n    def write(m: LogLimit) = JsNumber(m.megabytes)\n\n    def read(value: JsValue) =\n      Try {\n        val JsNumber(mb) = value\n        require(mb.isWhole, \"log limit must be whole number\")\n        LogLimit(mb.intValue MB)\n      } match {\n        case Success(limit)                       => limit\n        case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)\n        case Failure(e: Throwable)                => deserializationError(\"log limit malformed\", e)\n      }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/MemoryLimit.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport scala.language.postfixOps\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport spray.json._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.http.Messages\nimport pureconfig._\nimport pureconfig.generic.auto._\n\ncase class MemoryLimitConfig(min: ByteSize, max: ByteSize, std: ByteSize)\ncase class NamespaceMemoryLimitConfig(min: ByteSize, max: ByteSize)\n\n/**\n * MemoryLimit encapsulates allowed memory for an action. The limit must be within a\n * permissible range (by default [128MB, 512MB]).\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param megabytes the memory limit in megabytes for the action\n */\nprotected[entity] class MemoryLimit private (val megabytes: Int) extends AnyVal {\n\n  def toByteSize: ByteSize = ByteSize(megabytes, SizeUnits.MB)\n\n  /** It checks the namespace memory limit setting value  */\n  @throws[ActionMemoryLimitException]\n  protected[core] def checkNamespaceLimit(user: Identity): Unit = {\n    val memoryMax = user.limits.allowedMaxActionMemory\n    val memoryMin = user.limits.allowedMinActionMemory\n    try {\n      require(\n        megabytes <= memoryMax.toMB,\n        Messages.sizeExceedsAllowedThreshold(MemoryLimit.memoryLimitFieldName, megabytes, memoryMax.toMB.toInt))\n      require(\n        megabytes >= memoryMin.toMB,\n        Messages.sizeBelowAllowedThreshold(MemoryLimit.memoryLimitFieldName, megabytes, memoryMin.toMB.toInt))\n    } catch {\n      case e: IllegalArgumentException => throw ActionMemoryLimitException(e.getMessage)\n    }\n  }\n}\n\nprotected[core] object MemoryLimit extends ArgNormalizer[MemoryLimit] {\n  val config = loadConfigOrThrow[MemoryLimitConfig](ConfigKeys.memory)\n  val namespaceDefaultConfig = try {\n    loadConfigOrThrow[NamespaceMemoryLimitConfig](ConfigKeys.namespaceMemoryLimit)\n  } catch {\n    case _: Throwable =>\n      // Supports backwards compatibility for openwhisk that do not use the namespace default limit\n      NamespaceMemoryLimitConfig(config.min, config.max)\n  }\n  val memoryLimitFieldName = \"memory\"\n\n  /**\n   * These system limits and namespace default limits are set once at the beginning.\n   * Dynamic configuration updates are not supported at the moment.\n   */\n  protected[core] val STD_MEMORY: ByteSize = config.std\n  protected[core] val MIN_MEMORY: ByteSize = config.min\n  protected[core] val MAX_MEMORY: ByteSize = config.max\n\n  /** Default namespace limit used if there is no namespace-specific limit */\n  protected[core] val MIN_MEMORY_DEFAULT: ByteSize = namespaceDefaultConfig.min\n  protected[core] val MAX_MEMORY_DEFAULT: ByteSize = namespaceDefaultConfig.max\n\n  /** A singleton MemoryLimit with default value */\n  protected[core] val standardMemoryLimit = MemoryLimit(STD_MEMORY)\n\n  /** Gets MemoryLimit with default value */\n  protected[core] def apply(): MemoryLimit = standardMemoryLimit\n\n  require(MAX_MEMORY >= MAX_MEMORY_DEFAULT, \"The system max limit must be greater than the namespace max limit.\")\n  require(MIN_MEMORY <= MIN_MEMORY_DEFAULT, \"The system min limit must be less than the namespace min limit.\")\n\n  /**\n   * Creates MemoryLimit for limit, iff limit is within permissible range.\n   *\n   * @param megabytes the limit in megabytes, must be within permissible range\n   * @return MemoryLimit with limit set\n   * @throws IllegalArgumentException if limit does not conform to requirements\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(megabytes: ByteSize): MemoryLimit = {\n    new MemoryLimit(megabytes.toMB.toInt)\n  }\n\n  override protected[core] implicit val serdes = new RootJsonFormat[MemoryLimit] {\n    def write(m: MemoryLimit) = JsNumber(m.megabytes)\n\n    def read(value: JsValue) =\n      Try {\n        val JsNumber(mb) = value\n        require(mb.isWhole, \"memory limit must be whole number\")\n        MemoryLimit(mb.intValue MB)\n      } match {\n        case Success(limit)                       => limit\n        case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)\n        case Failure(e: Throwable)                => deserializationError(\"memory limit malformed\", e)\n      }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/Parameter.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport org.apache.openwhisk.core.ConfigKeys\n\nimport scala.util.{Failure, Success, Try}\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.language.postfixOps\nimport org.apache.openwhisk.core.entity.size._\nimport pureconfig.loadConfigOrThrow\n\n/**\n * Parameters is a key-value map from parameter names to parameter values. The value of a\n * parameter is opaque, it is treated as string regardless of its actual type.\n *\n * @param key the parameter name, assured to be non-null because it is a value\n * @param value the parameter value, assured to be non-null because it is a value\n */\nprotected[core] class Parameters protected[entity] (protected[entity] val params: Map[ParameterName, ParameterValue])\n    extends AnyVal {\n\n  /**\n   * Calculates the size in Bytes of the Parameters-instance.\n   *\n   * @return Size of instance as ByteSize\n   */\n  def size = {\n    params\n      .map { case (name, value) => name.size + value.size }\n      .foldLeft(0 B)(_ + _)\n  }\n\n  protected[entity] def +(p: ParameterName, v: ParameterValue) = {\n    new Parameters(params + (p -> v))\n  }\n\n  /** Add parameters from p to existing map, overwriting existing values in case of overlap in keys. */\n  protected[core] def ++(p: Parameters) = new Parameters(params ++ p.params)\n\n  /** Add optional parameters from p to existing map, overwriting existing values in case of overlap in keys. */\n  protected[core] def ++(p: Option[Parameters]): Parameters = {\n    p.map(x => new Parameters(params ++ x.params)).getOrElse(this)\n  }\n\n  /** Remove parameter by name. */\n  protected[core] def -(p: String): Parameters = {\n    // wrap with try since parameter name may throw an exception for illegal p\n    Try(new Parameters(params - new ParameterName(p))) getOrElse this\n  }\n\n  /** Gets set of all defined parameters. */\n  protected[core] def definedParameters: Set[String] = {\n    params.keySet filter (params(_).isDefined) map (_.name)\n  }\n\n  /** Gets set of all defined parameters. */\n  protected[core] def initParameters: Set[String] = {\n    params.keySet filter (params(_).init) map (_.name)\n  }\n\n  /**\n   * Gets map of all locked (encrypted) parameters, excluding parameters from given set.\n   */\n  protected[core] def lockedParameters(exclude: Set[String] = Set.empty): Map[String, String] = {\n    params.collect {\n      case p if p._2.encryption.isDefined && !exclude.contains(p._1.name) => (p._1.name -> p._2.encryption.get)\n    }\n  }\n\n  protected[core] def toJsArray = {\n    JsArray(params map { p =>\n      val init = if (p._2.init) Some(\"init\" -> JsTrue) else None\n      val encrypt = p._2.encryption.map(e => (\"encryption\" -> JsString(e)))\n\n      JsObject(Map(\"key\" -> p._1.name.toJson, \"value\" -> p._2.value) ++ init ++ encrypt)\n    } toSeq: _*)\n  }\n\n  protected[core] def toJsObject = JsObject(params.map(p => (p._1.name -> p._2.value.toJson)))\n\n  override def toString = toJsArray.compactPrint\n\n  /**\n   * Converts parameters to JSON object and merge keys with payload if defined.\n   * In case of overlap, the keys in the payload supersede.\n   */\n  protected[core] def merge(payload: Option[JsObject]): Some[JsObject] = {\n    val args = payload getOrElse JsObject.empty\n    Some { (toJsObject.fields ++ args.fields).toJson.asJsObject }\n  }\n\n  /** Retrieves parameter by name if it exists. */\n  protected[core] def get(p: String): Option[JsValue] = params.get(new ParameterName(p)).map(_.value)\n\n  /** Retrieves parameter by name if it exists. Returns that parameter if it is deserializable to {@code T} */\n  protected[core] def getAs[T: JsonReader](p: String): Try[T] =\n    get(p)\n      .fold[Try[JsValue]](Failure(new IllegalStateException(s\"key '$p' does not exist\")))(Success.apply)\n      .flatMap(js => Try(js.convertTo[T]))\n\n  /**\n   *  Retrieves parameter by name if it exist.\n   *  @param p the parameter to check for a truthy value\n   *  @param valueForNonExistent the value to return for a missing parameter (default false)\n   *  @return true if parameter exists and has truthy value, otherwise returns the specified value for non-existent keys\n   */\n  protected[core] def isTruthy(p: String, valueForNonExistent: Boolean = false): Boolean = {\n    get(p) map {\n      case JsBoolean(b) => b\n      case JsNumber(n)  => n != 0\n      case JsString(s)  => s.nonEmpty\n      case JsNull       => false\n      case _            => true\n    } getOrElse valueForNonExistent\n  }\n\n  /**\n   * Encrypts any parameters that are not yet encoded.\n   *\n   * @param encoder the encoder to transform parameter values with\n   * @return parameters with all values encrypted\n   */\n  def lock(encoder: Option[Encrypter] = None): Parameters = {\n    encoder\n      .map { coder =>\n        new Parameters(params.map {\n          case (paramName, paramValue) if paramValue.encryption.isEmpty =>\n            paramName -> coder.encrypt(paramValue)\n          case p => p\n        })\n      }\n      .getOrElse(this)\n  }\n\n  /**\n   * Decodes parameters. If the encryption scheme for a parameter is not recognized, it is not modified.\n   *\n   * @param decoder the decoder to use to transform locked values\n   * @return parameters will all values decoded (where scheme is known)\n   */\n  def unlock(decoder: ParameterEncryption): Parameters = {\n    new Parameters(params.map {\n      case p @ (paramName, paramValue) =>\n        paramValue.encryption\n          .map(paramName -> decoder.encryptor(_).decrypt(paramValue))\n          .getOrElse(p)\n    })\n  }\n\n}\n\n/**\n * A ParameterName is a parameter name for an action or trigger to bind to its environment.\n * It wraps a normalized string as a value type.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param name the name of the parameter (its key)\n */\nprotected[entity] class ParameterName protected[entity] (val name: String) extends AnyVal {\n\n  /**\n   * The size of the ParameterName entity as ByteSize.\n   */\n  def size = name sizeInBytes\n}\n\n/**\n * A ParameterValue is a parameter value for an action or trigger to bind to its environment.\n * It wraps a normalized string as a value type. The string may be a JSON string. It may also\n * be undefined, such as when an action is created but the parameter value is not bound yet.\n * In general, this is an opaque value.\n *\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param v the value of the parameter, may be null\n * @param init if true, this parameter value is only offered to the action during initialization\n * @param encryption the name of the encryption algorithm used to store the parameter or none (plain text)\n */\nprotected[entity] case class ParameterValue protected[entity] (private val v: JsValue,\n                                                               val init: Boolean,\n                                                               val encryption: Option[String] = None) {\n\n  /** @return JsValue if defined else JsNull. */\n  protected[entity] def value = Option(v) getOrElse JsNull\n\n  /** @return true iff value is not JsNull. */\n  protected[entity] def isDefined = value != JsNull\n\n  /**\n   * The size of the ParameterValue entity as ByteSize.\n   */\n  def size = value.toString.sizeInBytes\n}\n\nprotected[core] object Parameters extends ArgNormalizer[Parameters] {\n\n  protected[core] val MAX_SIZE = loadConfigOrThrow[ByteSize](ConfigKeys.parameterSizeLimit) // system limit\n  protected[core] val MAX_SIZE_DEFAULT = try {\n    loadConfigOrThrow[ByteSize](ConfigKeys.namespaceParameterSizeLimit)\n  } catch {\n    case _: Throwable =>\n      // Supports backwards compatibility for openwhisk that do not use the namespace default limit\n      MAX_SIZE\n  }\n\n  require(MAX_SIZE >= MAX_SIZE_DEFAULT, \"The system limit must be greater than the namespace limit.\")\n\n  /** Name of parameter that indicates if action is a feed. */\n  protected[core] val Feed = \"feed\"\n  protected[core] val sizeLimit = MAX_SIZE\n\n  protected[core] def apply(): Parameters = new Parameters(Map.empty)\n\n  /**\n   * Creates a parameter tuple from a pair of strings.\n   * A convenience method for tests.\n   *\n   * @param p    the parameter name\n   * @param v    the parameter value\n   * @param init the parameter is for initialization\n   * @return (ParameterName, ParameterValue)\n   * @throws IllegalArgumentException if key is not defined\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(p: String, v: String, init: Boolean = false): Parameters = {\n    require(p != null && p.trim.nonEmpty, \"key undefined\")\n    Parameters() + (new ParameterName(ArgNormalizer.trim(p)),\n    ParameterValue(Option(v).map(_.trim.toJson).getOrElse(JsNull), init, None))\n  }\n\n  /**\n   * Creates a parameter tuple from a parameter name and JsValue.\n   *\n   * @param p    the parameter name\n   * @param v    the parameter value\n   * @param init the parameter is for initialization\n   * @return (ParameterName, ParameterValue)\n   * @throws IllegalArgumentException if key is not defined\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(p: String, v: JsValue, init: Boolean): Parameters = {\n    require(p != null && p.trim.nonEmpty, \"key undefined\")\n    Parameters() + (new ParameterName(ArgNormalizer.trim(p)),\n    ParameterValue(Option(v).getOrElse(JsNull), init, None))\n  }\n\n  /**\n   * Creates a parameter tuple from a parameter name and JsValue.\n   *\n   * @param p the parameter name\n   * @param v the parameter value\n   * @return (ParameterName, ParameterValue)\n   * @throws IllegalArgumentException if key is not defined\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(p: String, v: JsValue): Parameters = {\n    require(p != null && p.trim.nonEmpty, \"key undefined\")\n    Parameters() + (new ParameterName(ArgNormalizer.trim(p)),\n    ParameterValue(Option(v).getOrElse(JsNull), false, None))\n  }\n\n  override protected[core] implicit val serdes = new RootJsonFormat[Parameters] {\n    def write(p: Parameters) = p.toJsArray\n\n    /**\n     * Gets parameters as a Parameters instances. The argument should be a JArray\n     * [{key,value}], otherwise an IllegalParameter is thrown.\n     *\n     * @param parameters the JSON representation of an parameter array\n     * @return Parameters instance if parameters conforms to schema\n     */\n    def read(value: JsValue): Parameters = {\n      value match {\n        case JsArray(params) => read(params).getOrElse(deserializationError(\"parameters malformed!\"))\n        case _               => deserializationError(\"parameters malformed!\")\n      }\n    }\n\n    /**\n     * Gets parameters as a Parameters instances.\n     * The argument should be a [{key,value}].\n     *\n     * @param parameters the JSON representation of an parameter array\n     * @return Parameters instance if parameters conforms to schema\n     */\n    def read(params: Vector[JsValue]) = Try {\n      new Parameters(params.map {\n        case o @ JsObject(fields) =>\n          o.getFields(\"key\", \"value\", \"init\", \"encryption\") match {\n            case Seq(JsString(k), v: JsValue) if fields.contains(\"value\") =>\n              val key = new ParameterName(k)\n              val value = ParameterValue(v, false)\n              (key, value)\n            case Seq(JsString(k), v: JsValue, JsBoolean(i)) =>\n              val key = new ParameterName(k)\n              val value = ParameterValue(v, i)\n              (key, value)\n            case Seq(JsString(k), v: JsValue, JsBoolean(i), JsString(e)) =>\n              val key = new ParameterName(k)\n              val value = ParameterValue(v, i, Some(e))\n              (key, value)\n            case Seq(JsString(k), v: JsValue, JsBoolean(i), JsNull) =>\n              val key = new ParameterName(k)\n              val value = ParameterValue(v, i, None)\n              (key, value)\n            case Seq(JsString(k), v: JsValue, JsString(e))\n                if fields.contains(\"value\") && fields.contains(\"encryption\") =>\n              val key = new ParameterName(k)\n              val value = ParameterValue(v, false, Some(e))\n              (key, value)\n          }\n        case _ => deserializationError(\"invalid parameter\")\n      }.toMap)\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/ParameterEncryption.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.openwhisk.core.entity\n\nimport java.nio.ByteBuffer\nimport java.nio.charset.StandardCharsets\nimport java.security.SecureRandom\nimport java.util.Base64\n\nimport javax.crypto.Cipher\nimport javax.crypto.spec.{GCMParameterSpec, SecretKeySpec}\nimport org.apache.openwhisk.core.ConfigKeys\nimport pureconfig.loadConfig\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport pureconfig.generic.auto._\nimport spray.json._\n\nprotected[core] case class ParameterStorageConfig(current: String = ParameterEncryption.NO_ENCRYPTION,\n                                                  aes128: Option[String] = None,\n                                                  aes256: Option[String] = None)\n\nprotected[core] class ParameterEncryption(val default: Option[Encrypter], encryptors: Map[String, Encrypter]) {\n\n  /**\n   * Gets the coder for the given scheme name.\n   *\n   * @param name the name of the encryption algorithm (defaults to current from last configuration)\n   * @return the coder if there is one else no-op encryptor\n   */\n  def encryptor(name: String): Encrypter = {\n    encryptors.get(name).getOrElse(ParameterEncryption.noop)\n  }\n\n}\n\nprotected[core] object ParameterEncryption {\n\n  val NO_ENCRYPTION = \"noop\"\n  val AES128_ENCRYPTION = \"aes-128\"\n  val AES256_ENCRYPTION = \"aes-256\"\n\n  val noop = new Encrypter {\n    override val name = NO_ENCRYPTION\n  }\n\n  val singleton: ParameterEncryption = {\n    val configLoader = loadConfig[ParameterStorageConfig](ConfigKeys.parameterStorage)\n    val config = configLoader.getOrElse(ParameterStorageConfig(noop.name))\n    ParameterEncryption(config)\n  }\n\n  def apply(config: ParameterStorageConfig): ParameterEncryption = {\n    val availableEncoders = Map(noop.name -> noop) ++\n      config.aes128.map(k => AES128_ENCRYPTION -> new Aes128(k)) ++\n      config.aes256.map(k => AES256_ENCRYPTION -> new Aes256(k))\n\n    val current = config.current.toLowerCase match {\n      case \"\" | \"off\" | NO_ENCRYPTION => NO_ENCRYPTION\n      case s                          => s\n    }\n\n    val defaultEncoder: Encrypter = availableEncoders.get(current).getOrElse(noop)\n    new ParameterEncryption(Option(defaultEncoder).filter(_ != noop), availableEncoders)\n  }\n}\n\nprotected[core] trait Encrypter {\n  val name: String\n  def encrypt(p: ParameterValue): ParameterValue = p\n  def decrypt(p: ParameterValue): ParameterValue = p\n  def decrypt(v: JsString): JsValue = v\n}\n\nprotected[core] object Encrypter {\n  protected[entity] def getKeyBytes(key: String): Array[Byte] = {\n    if (key.length == 0) {\n      Array.empty\n    } else {\n      Base64.getDecoder.decode(key)\n    }\n  }\n}\n\nprotected[core] trait AesEncryption extends Encrypter {\n  val key: Array[Byte]\n  val ivLen: Int\n  val name: String\n  private val tLen = 128\n  private val secureRandom = new SecureRandom()\n  private lazy val secretKey = new SecretKeySpec(key, \"AES\")\n\n  override def encrypt(value: ParameterValue): ParameterValue = {\n    val iv = new Array[Byte](ivLen)\n    secureRandom.nextBytes(iv)\n    val gcmSpec = new GCMParameterSpec(tLen, iv)\n    val cipher = Cipher.getInstance(\"AES/GCM/NoPadding\")\n    cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)\n    val clearText = value.value.compactPrint.getBytes(StandardCharsets.UTF_8)\n    val cipherText = cipher.doFinal(clearText)\n\n    val byteBuffer = ByteBuffer.allocate(4 + iv.length + cipherText.length)\n    byteBuffer.putInt(iv.length)\n    byteBuffer.put(iv)\n    byteBuffer.put(cipherText)\n    val cipherMessage = byteBuffer.array\n    ParameterValue(JsString(Base64.getEncoder.encodeToString(cipherMessage)), value.init, Some(name))\n  }\n\n  override def decrypt(p: ParameterValue): ParameterValue = {\n    p.value match {\n      case s: JsString => p.copy(v = decrypt(s), encryption = None)\n      case _           => p\n    }\n  }\n\n  override def decrypt(value: JsString): JsValue = {\n    val cipherMessage = value.convertTo[String].getBytes(StandardCharsets.UTF_8)\n    val byteBuffer = ByteBuffer.wrap(Base64.getDecoder.decode(cipherMessage))\n    val ivLength = byteBuffer.getInt\n    if (ivLength != ivLen) {\n      throw new IllegalArgumentException(\"invalid iv length\")\n    }\n    val iv = new Array[Byte](ivLength)\n    byteBuffer.get(iv)\n    val cipherText = new Array[Byte](byteBuffer.remaining)\n    byteBuffer.get(cipherText)\n\n    val gcmSpec = new GCMParameterSpec(tLen, iv)\n    val cipher = Cipher.getInstance(\"AES/GCM/NoPadding\")\n    cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec)\n    val plainTextBytes = cipher.doFinal(cipherText)\n    val plainText = new String(plainTextBytes, StandardCharsets.UTF_8)\n    plainText.parseJson\n  }\n\n}\n\nprotected[core] class Aes128(val k: String) extends AesEncryption with Encrypter {\n  override val key = Encrypter.getKeyBytes(k)\n  override val name = ParameterEncryption.AES128_ENCRYPTION\n  override val ivLen = 12\n}\n\nprotected[core] class Aes256(val k: String) extends AesEncryption with Encrypter {\n  override val key = Encrypter.getKeyBytes(k)\n  override val name = ParameterEncryption.AES256_ENCRYPTION\n  override val ivLen = 128\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/Secret.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n/**\n * Secret, a cryptographic string such as a key used for authentication.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param key the secret key, required not null or empty\n */\nprotected[core] class Secret private (val key: String) extends AnyVal {\n  protected[core] def asString: String = toString\n  protected[entity] def toJson: JsString = JsString(toString)\n  override def toString: String = key\n}\n\nprotected[core] object Secret {\n\n  /** Minimum secret length */\n  private val MIN_LENGTH = 64\n\n  /** Maximum secret length */\n  private val MAX_LENGTH = 64\n\n  /**\n   * Creates a Secret from a string. The string must be a valid secret already.\n   *\n   * @param str the secret as string, at least 64 characters\n   * @return Secret instance\n   * @throws IllegalArgumentException is argument is not a valid Secret\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(str: String): Secret = {\n    require(str.length >= MIN_LENGTH, s\"secret must be at least $MIN_LENGTH characters\")\n    require(str.length <= MAX_LENGTH, s\"secret must be at most $MAX_LENGTH characters\")\n    new Secret(str)\n  }\n\n  /**\n   * Creates a new random secret.\n   *\n   * @return Secret\n   */\n  protected[core] def apply(): Secret = {\n    Secret(rand.alphanumeric.take(MIN_LENGTH).mkString)\n  }\n\n  implicit val serdes: RootJsonFormat[Secret] = new RootJsonFormat[Secret] {\n    def write(s: Secret): JsValue = s.toJson\n    def read(value: JsValue): Secret = Secret(value.convertTo[String])\n  }\n\n  private val rand = new scala.util.Random(new java.security.SecureRandom())\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/SemVer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport spray.json.deserializationError\nimport spray.json.JsString\nimport spray.json.JsValue\nimport spray.json.RootJsonFormat\nimport scala.util.Try\n\n/**\n * State needed to implement semantic versioning.\n *\n * @see http://semver.org\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param (major, minor, patch) for the semantic version\n */\nprotected[core] class SemVer private (private val version: (Int, Int, Int)) extends AnyVal {\n\n  protected[core] def major = version._1\n  protected[core] def minor = version._2\n  protected[core] def patch = version._3\n\n  protected[core] def upMajor = SemVer(major + 1, minor, patch)\n  protected[core] def upMinor = SemVer(major, minor + 1, patch)\n  protected[core] def upPatch = SemVer(major, minor, patch + 1)\n\n  protected[entity] def toJson = JsString(toString)\n  override def toString = s\"$major.$minor.$patch\"\n}\n\nprotected[core] object SemVer {\n  protected[core] val REGEX = \"\"\"(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\"\"\"\n\n  /** Default semantic version */\n  protected[core] def apply() = new SemVer(0, 0, 1)\n\n  /**\n   * Creates a semantic version.\n   *\n   * @param major the major >= 0\n   * @param minor the minor >= 0\n   * @param patch the patch >= 0\n   * @return SemVer instance\n   * @throws IllegalArgumentException if the parameters results in an invalid semantic version\n   */\n  protected[core] def apply(major: Int, minor: Int, patch: Int): SemVer = {\n    if ((major == 0 && minor == 0 && patch == 0) || (major < 0 || minor < 0 || patch < 0)) {\n      throw new IllegalArgumentException(s\"bad semantic version $major.$minor.$patch must not be negative\")\n    } else new SemVer(major, minor, patch)\n  }\n\n  /**\n   * Parses a string representation of a semantic version and creates\n   * a instance of SemVer. If the string is not properly formatted, an\n   * IllegalSemVer exception is thrown.\n   *\n   * @param str to parse to extract semantic version\n   * @return SemVer instance\n   * @thrown IllegalArgumentException if string is not a valid semantic version\n   */\n  protected[entity] def apply(str: String): SemVer = {\n    try {\n      val parts = if (str != null && str.nonEmpty) str.split('.') else Array[String]()\n      val major = if (parts.size >= 1) parts(0).toInt else 0\n      val minor = if (parts.size >= 2) parts(1).toInt else 0\n      val patch = if (parts.size >= 3) parts(2).toInt else 0\n      SemVer(major, minor, patch)\n    } catch {\n      case _: Throwable => throw new IllegalArgumentException(s\"bad semantic version $str\")\n    }\n  }\n\n  implicit val serdes = new RootJsonFormat[SemVer] {\n    def write(v: SemVer) = v.toJson\n\n    def read(value: JsValue) =\n      Try {\n        val JsString(v) = value\n        SemVer(v)\n      } getOrElse deserializationError(\"semantic version malformed\")\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/Size.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.nio.charset.StandardCharsets\n\nimport com.typesafe.config.ConfigValue\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\nimport ByteSize.formatError\n\nobject SizeUnits extends Enumeration {\n\n  sealed abstract class Unit() {\n    def toBytes(n: Long): Long\n\n    def toKBytes(n: Long): Long\n\n    def toMBytes(n: Long): Long\n\n    def toGBytes(n: Long): Long\n  }\n\n  case object BYTE extends Unit {\n    def toBytes(n: Long): Long = n\n\n    def toKBytes(n: Long): Long = n / 1024\n\n    def toMBytes(n: Long): Long = n / 1024 / 1024\n\n    def toGBytes(n: Long): Long = n / 1024 / 1024 / 1024\n  }\n\n  case object KB extends Unit {\n    def toBytes(n: Long): Long = n * 1024\n\n    def toKBytes(n: Long): Long = n\n\n    def toMBytes(n: Long): Long = n / 1024\n\n    def toGBytes(n: Long): Long = n / 1024 / 1024\n\n  }\n\n  case object MB extends Unit {\n    def toBytes(n: Long): Long = n * 1024 * 1024\n\n    def toKBytes(n: Long): Long = n * 1024\n\n    def toMBytes(n: Long): Long = n\n\n    def toGBytes(n: Long): Long = n / 1024\n  }\n\n  case object GB extends Unit {\n    def toBytes(n: Long): Long = n * 1024 * 1024 * 1024\n\n    def toKBytes(n: Long): Long = n * 1024 * 1024\n\n    def toMBytes(n: Long): Long = n * 1024\n\n    def toGBytes(n: Long): Long = n\n  }\n\n}\n\ncase class ByteSize(size: Long, unit: SizeUnits.Unit) extends Ordered[ByteSize] {\n\n  require(size >= 0, \"a negative size of an object is not allowed.\")\n\n  def toBytes = unit.toBytes(size)\n\n  def toKB = unit.toKBytes(size)\n\n  def toMB = unit.toMBytes(size)\n\n  def +(other: ByteSize): ByteSize = {\n    val commonUnit = SizeUnits.BYTE\n    val commonSize = other.toBytes + toBytes\n    ByteSize(commonSize, commonUnit)\n  }\n\n  def -(other: ByteSize): ByteSize = {\n    val commonUnit = SizeUnits.BYTE\n    val commonSize = toBytes - other.toBytes\n    ByteSize(commonSize, commonUnit)\n  }\n\n  def *(other: Int): ByteSize = {\n    ByteSize(toBytes * other, SizeUnits.BYTE)\n  }\n\n  def /(other: ByteSize): Double = {\n    // Without throwing the exception the result would be `Infinity` here\n    if (other.toBytes == 0) {\n      throw new ArithmeticException\n    } else {\n      (1.0 * toBytes) / (1.0 * other.toBytes)\n    }\n  }\n\n  def /(other: Int): ByteSize = {\n    ByteSize(toBytes / other, SizeUnits.BYTE)\n  }\n\n  def compare(other: ByteSize) = toBytes compare other.toBytes\n\n  override def equals(that: Any): Boolean = that match {\n    case t: ByteSize => compareTo(t) == 0\n    case _           => false\n  }\n\n  override def toString = {\n    unit match {\n      case SizeUnits.BYTE => s\"$size B\"\n      case SizeUnits.KB   => s\"$size KB\"\n      case SizeUnits.MB   => s\"$size MB\"\n      case SizeUnits.GB   => s\"$size GB\"\n    }\n  }\n}\n\nobject ByteSize {\n  private val regex = \"\"\"(?i)\\s?(\\d+)\\s?(GB|MB|KB|B|G|M|K)\\s?\"\"\".r.pattern\n  protected[entity] val formatError = \"\"\"Size Unit not supported. Only \"B\", \"K[B]\", \"M[B]\" and \"G[B]\" are supported.\"\"\"\n\n  def fromString(sizeString: String): ByteSize = {\n    val matcher = regex.matcher(sizeString)\n    if (matcher.matches()) {\n      val size = matcher.group(1).toLong\n      val unit = matcher.group(2).charAt(0).toUpper match {\n        case 'B' => SizeUnits.BYTE\n        case 'K' => SizeUnits.KB\n        case 'M' => SizeUnits.MB\n        case 'G' => SizeUnits.GB\n      }\n\n      ByteSize(size, unit)\n    } else {\n      throw new IllegalArgumentException(formatError)\n    }\n  }\n}\n\nobject size {\n\n  implicit class SizeInt(n: Int) extends SizeConversion {\n    def sizeIn(unit: SizeUnits.Unit): ByteSize = ByteSize(n, unit)\n  }\n\n  implicit class SizeLong(n: Long) extends SizeConversion {\n    def sizeIn(unit: SizeUnits.Unit): ByteSize = ByteSize(n, unit)\n  }\n\n  implicit class SizeString(n: String) extends SizeConversion {\n    def sizeIn(unit: SizeUnits.Unit): ByteSize = ByteSize(n.getBytes(StandardCharsets.UTF_8).length, unit)\n  }\n\n  implicit class SizeOptionString(n: Option[String]) extends SizeConversion {\n    def sizeIn(unit: SizeUnits.Unit): ByteSize =\n      n map { s =>\n        s.sizeIn(unit)\n      } getOrElse {\n        ByteSize(0, unit)\n      }\n  }\n\n  // Creation of an intermediary Config object is necessary here, since \"getBytes\" is only part of that interface.\n  implicit val pureconfigReader =\n    ConfigReader[ConfigValue].map(v => ByteSize(v.atKey(\"key\").getBytes(\"key\"), SizeUnits.BYTE))\n\n  protected[core] implicit val serdes = new RootJsonFormat[ByteSize] {\n    def write(b: ByteSize) = JsString(b.toString)\n\n    def read(value: JsValue): ByteSize = value match {\n      case JsString(s) => ByteSize.fromString(s)\n      case _           => deserializationError(formatError)\n    }\n  }\n}\n\ntrait SizeConversion {\n  def B = sizeIn(SizeUnits.BYTE)\n\n  def KB = sizeIn(SizeUnits.KB)\n\n  def MB = sizeIn(SizeUnits.MB)\n\n  def GB: ByteSize = sizeIn(SizeUnits.GB)\n\n  def bytes = B\n\n  def kilobytes = KB\n\n  def megabytes = MB\n\n  def gigabytes: ByteSize = GB\n\n  def sizeInBytes = sizeIn(SizeUnits.BYTE)\n\n  def sizeIn(unit: SizeUnits.Unit): ByteSize\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/Subject.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport scala.util.Try\n\nimport spray.json.JsString\nimport spray.json.JsValue\nimport spray.json.RootJsonFormat\nimport spray.json.deserializationError\n\nprotected[core] class Subject private (private val subject: String) extends AnyVal {\n  protected[core] def asString = subject // to make explicit that this is a string conversion\n  protected[entity] def toJson = JsString(subject)\n  override def toString = subject\n}\n\nprotected[core] object Subject extends ArgNormalizer[Subject] {\n\n  /** Minimum subject length */\n  protected[core] val MIN_LENGTH = 5\n\n  /**\n   * Creates a Subject from a string.\n   *\n   * @param str the subject name, at least 6 characters\n   * @return Subject instance\n   * @throws IllegalArgumentException is argument is undefined\n   */\n  @throws[IllegalArgumentException]\n  override protected[entity] def factory(str: String): Subject = {\n    require(str.length >= MIN_LENGTH, s\"subject must be at least $MIN_LENGTH characters\")\n    new Subject(str)\n  }\n\n  /**\n   * Creates a random subject\n   *\n   * @return Subject\n   */\n  protected[core] def apply(): Subject = {\n    Subject(\"anon-\" + rand.alphanumeric.take(27).mkString)\n  }\n\n  override protected[core] implicit val serdes = new RootJsonFormat[Subject] {\n    def write(s: Subject) = s.toJson\n\n    def read(value: JsValue) =\n      Try {\n        val JsString(s) = value\n        Subject(s)\n      } getOrElse deserializationError(\"subject malformed\")\n  }\n\n  private val rand = new scala.util.Random()\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/TimeLimit.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.duration._\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport spray.json.JsNumber\nimport spray.json.JsValue\nimport spray.json.RootJsonFormat\nimport spray.json.deserializationError\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.http.Messages\n\n/**\n * TimeLimit encapsulates a duration for an action. The duration must be within a\n * permissible range (currently [100 msecs, 5 minutes]).\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param duration the duration for the action, required not null\n */\nprotected[entity] class TimeLimit private (val duration: FiniteDuration) extends AnyVal {\n  protected[core] def millis = duration.toMillis.toInt\n  override def toString = duration.toString\n\n  /** It checks the namespace duration limit setting value  */\n  @throws[ActionTimeLimitException]\n  protected[core] def checkNamespaceLimit(user: Identity): Unit = {\n    val durationMax = user.limits.allowedMaxActionTimeout\n    val durationMix = user.limits.allowedMinActionTimeout\n    try {\n      require(\n        duration <= durationMax,\n        Messages.durationExceedsAllowedThreshold(TimeLimit.timeLimitFieldName, duration, durationMax))\n      require(\n        duration >= durationMix,\n        Messages.durationBelowAllowedThreshold(TimeLimit.timeLimitFieldName, duration, durationMix))\n    } catch {\n      case e: IllegalArgumentException => throw ActionTimeLimitException(e.getMessage)\n    }\n  }\n}\n\ncase class NamespaceTimeLimitConfig(max: FiniteDuration, min: FiniteDuration)\ncase class TimeLimitConfig(max: FiniteDuration, min: FiniteDuration, std: FiniteDuration)\n\nprotected[core] object TimeLimit extends ArgNormalizer[TimeLimit] {\n  val config = loadConfigOrThrow[TimeLimitConfig](ConfigKeys.timeLimit)\n  val namespaceDefaultConfig = try {\n    loadConfigOrThrow[NamespaceTimeLimitConfig](ConfigKeys.namespaceTimeLimit)\n  } catch {\n    case _: Throwable =>\n      // Supports backwards compatibility for openwhisk that do not use the namespace default limit\n      NamespaceTimeLimitConfig(config.max, config.min)\n  }\n  val timeLimitFieldName = \"duration\"\n\n  /**\n   * These system limits and namespace default limits are set once at the beginning.\n   * Dynamic configuration updates are not supported at the moment.\n   */\n  protected[core] val STD_DURATION: FiniteDuration = config.std\n  protected[core] val MIN_DURATION: FiniteDuration = config.min\n  protected[core] val MAX_DURATION: FiniteDuration = config.max\n\n  /** Default namespace limit used if there is no namespace-specific limit */\n  protected[core] val MIN_DURATION_DEFAULT: FiniteDuration = namespaceDefaultConfig.min\n  protected[core] val MAX_DURATION_DEFAULT: FiniteDuration = namespaceDefaultConfig.max\n\n  require(MAX_DURATION >= MAX_DURATION_DEFAULT, \"The system max limit must be greater than the namespace max limit.\")\n  require(MIN_DURATION <= MIN_DURATION_DEFAULT, \"The system min limit must be less than the namespace min limit.\")\n\n  /** A singleton TimeLimit with default value */\n  protected[core] val standardTimeLimit = TimeLimit(STD_DURATION)\n\n  /** Gets TimeLimit with default duration */\n  protected[core] def apply(): TimeLimit = standardTimeLimit\n\n  /**\n   * Creates TimeLimit for duration, iff duration is within permissible range.\n   *\n   * @param duration the duration in milliseconds, must be within permissible range\n   * @return TimeLimit with duration set\n   * @throws IllegalArgumentException if duration does not conform to requirements\n   */\n  @throws[IllegalArgumentException]\n  protected[core] def apply(duration: FiniteDuration): TimeLimit = {\n    require(duration != null, s\"duration undefined\")\n    new TimeLimit(duration)\n  }\n\n  override protected[core] implicit val serdes = new RootJsonFormat[TimeLimit] {\n    def write(t: TimeLimit) = JsNumber(t.millis)\n\n    def read(value: JsValue) =\n      Try {\n        val JsNumber(ms) = value\n        require(ms.isWhole, \"time limit must be whole number\")\n        TimeLimit(Duration(ms.intValue, MILLISECONDS))\n      } match {\n        case Success(limit)                       => limit\n        case Failure(e: IllegalArgumentException) => deserializationError(e.getMessage, e)\n        case Failure(e: Throwable)                => deserializationError(\"time limit malformed\", e)\n      }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/UUID.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.security.SecureRandom\n\nimport com.fasterxml.uuid.Generators\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n/**\n * Represents a user's username and/or a namespace identifier (generally looks like UUIDs)\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param asString the uuid in string representation\n */\nprotected[core] class UUID private (val asString: String) extends AnyVal {\n  protected[core] def snippet: String = asString.substring(0, 8)\n  protected[entity] def toJson: JsString = JsString(asString)\n  override def toString: String = asString\n}\n\nprotected[core] object UUID {\n\n  /**\n   * Generates a random UUID using java.util.UUID factory.\n   *\n   * @return new UUID\n   */\n  protected[core] def apply(): UUID = new UUID(UUIDs.randomUUID().toString)\n\n  protected[core] def apply(str: String): UUID = new UUID(str)\n\n  implicit val serdes: RootJsonFormat[UUID] = new RootJsonFormat[UUID] {\n    def write(u: UUID): JsValue = u.toJson\n    def read(value: JsValue): UUID = new UUID(value.convertTo[String])\n  }\n}\n\nobject UUIDs {\n  private val generator = new ThreadLocal[SecureRandom] {\n    override def initialValue() = new SecureRandom()\n  }\n\n  /**\n   * Static factory to retrieve a type 4 (pseudo randomly generated) UUID.\n   *\n   * The {@code java.util.UUID} is generated using a pseudo random number\n   * generator local to the thread.\n   *\n   * @return  A randomly generated {@code java.util.UUID}\n   */\n  def randomUUID(): java.util.UUID = Generators.randomBasedGenerator(generator.get()).generate()\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskAction.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.io.{ByteArrayInputStream, ByteArrayOutputStream}\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.time.Instant\nimport java.util.Base64\nimport org.apache.pekko.http.scaladsl.model.ContentTypes\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.util.{Failure, Success, Try}\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.{ArtifactStore, CacheChangeNotification, DocumentFactory, NoDocumentException}\nimport org.apache.openwhisk.core.entity.Attachments._\nimport org.apache.openwhisk.core.entity.types.EntityStore\n\n/**\n * ActionLimitsOption mirrors ActionLimits but makes both the timeout and memory\n * limit optional so that it is convenient to override just one limit at a time.\n */\ncase class ActionLimitsOption(timeout: Option[TimeLimit],\n                              memory: Option[MemoryLimit],\n                              logs: Option[LogLimit],\n                              concurrency: Option[IntraConcurrencyLimit],\n                              instances: Option[InstanceConcurrencyLimit] = None)\n\n/**\n * WhiskActionPut is a restricted WhiskAction view that eschews properties\n * that are auto-assigned or derived from URI: namespace and name. It\n * also replaces limits with an optional counterpart for convenience of\n * overriding only one value at a time.\n */\ncase class WhiskActionPut(exec: Option[Exec] = None,\n                          parameters: Option[Parameters] = None,\n                          limits: Option[ActionLimitsOption] = None,\n                          version: Option[SemVer] = None,\n                          publish: Option[Boolean] = None,\n                          annotations: Option[Parameters] = None,\n                          delAnnotations: Option[Array[String]] = None) {\n\n  protected[core] def replace(exec: Exec) = {\n    WhiskActionPut(Some(exec), parameters, limits, version, publish, annotations)\n  }\n\n  /**\n   * Resolves sequence components if they contain default namespace.\n   */\n  protected[core] def resolve(userNamespace: Namespace): WhiskActionPut = {\n    exec map {\n      case SequenceExec(components) =>\n        val newExec = SequenceExec(components map { c =>\n          FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace), c.name)\n        })\n        WhiskActionPut(Some(newExec), parameters, limits, version, publish, annotations)\n      case _ => this\n    } getOrElse this\n  }\n}\n\nabstract class WhiskActionLike(override val name: EntityName) extends WhiskEntity(name, \"action\") {\n  def exec: Exec\n  def parameters: Parameters\n  def limits: ActionLimits\n\n  /** @return true iff action has appropriate annotation. */\n  def hasFinalParamsAnnotation = {\n    annotations.getAs[Boolean](Annotations.FinalParamsAnnotationName) getOrElse false\n  }\n\n  /** @return a Set of immutable parameternames */\n  def immutableParameters =\n    if (hasFinalParamsAnnotation) {\n      parameters.definedParameters\n    } else Set.empty[String]\n\n  def toJson =\n    JsObject(\n      \"namespace\" -> namespace.toJson,\n      \"name\" -> name.toJson,\n      \"exec\" -> exec.toJson,\n      \"parameters\" -> parameters.toJson,\n      \"limits\" -> limits.toJson,\n      \"version\" -> version.toJson,\n      \"publish\" -> publish.toJson,\n      \"annotations\" -> annotations.toJson)\n}\n\nabstract class WhiskActionLikeMetaData(override val name: EntityName) extends WhiskActionLike(name) {\n  override def exec: ExecMetaDataBase\n}\n\n/**\n * A WhiskAction provides an abstraction of the meta-data\n * for a whisk action.\n *\n * The WhiskAction object is used as a helper to adapt objects between\n * the schema used by the database and the WhiskAction abstraction.\n *\n * @param namespace the namespace for the action\n * @param name the name of the action\n * @param exec the action executable details\n * @param parameters the set of parameters to bind to the action environment\n * @param limits the limits to impose on the action\n * @param version the semantic version\n * @param publish true to share the action or false otherwise\n * @param annotations the set of annotations to attribute to the action\n * @param updated the timestamp when the action is updated\n * @throws IllegalArgumentException if any argument is undefined\n */\n@throws[IllegalArgumentException]\ncase class WhiskAction(namespace: EntityPath,\n                       override val name: EntityName,\n                       exec: Exec,\n                       parameters: Parameters = Parameters(),\n                       limits: ActionLimits = ActionLimits(),\n                       version: SemVer = SemVer(),\n                       publish: Boolean = false,\n                       annotations: Parameters = Parameters(),\n                       override val updated: Instant = WhiskEntity.currentMillis())\n    extends WhiskActionLike(name) {\n\n  require(exec != null, \"exec undefined\")\n  require(limits != null, \"limits undefined\")\n\n  /**\n   * Merges parameters (usually from package) with existing action parameters.\n   * Existing parameters supersede those in p.\n   */\n  def inherit(p: Parameters): WhiskAction = copy(parameters = p ++ parameters).revision[WhiskAction](rev)\n\n  /**\n   * Resolves sequence components if they contain default namespace.\n   */\n  protected[core] def resolve(userNamespace: Namespace): WhiskAction = {\n    resolve(userNamespace.name)\n  }\n\n  /**\n   * Resolves sequence components if they contain default namespace.\n   */\n  protected[core] def resolve(userNamespace: EntityName): WhiskAction = {\n    exec match {\n      case SequenceExec(components) =>\n        val newExec = SequenceExec(components map { c =>\n          FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace), c.name)\n        })\n        copy(exec = newExec).revision[WhiskAction](rev)\n      case _ => this\n    }\n  }\n\n  def toExecutableWhiskAction: Option[ExecutableWhiskAction] = exec match {\n    case codeExec: CodeExec[_] =>\n      Some(\n        ExecutableWhiskAction(namespace, name, codeExec, parameters, limits, version, publish, annotations)\n          .revision[ExecutableWhiskAction](rev))\n    case _ => None\n  }\n\n  /**\n   * This the action summary as computed by the database view.\n   * Strictly used in view testing to enforce alignment.\n   */\n  override def summaryAsJson: JsObject = {\n    val binary = exec match {\n      case c: CodeExec[_] => c.binary\n      case _              => false\n    }\n\n    JsObject(\n      super.summaryAsJson.fields +\n        (\"limits\" -> limits.toJson) +\n        (\"exec\" -> JsObject(\"binary\" -> JsBoolean(binary))))\n  }\n}\n\n@throws[IllegalArgumentException]\ncase class WhiskActionMetaData(namespace: EntityPath,\n                               override val name: EntityName,\n                               exec: ExecMetaDataBase,\n                               parameters: Parameters = Parameters(),\n                               limits: ActionLimits = ActionLimits(),\n                               version: SemVer = SemVer(),\n                               publish: Boolean = false,\n                               annotations: Parameters = Parameters(),\n                               override val updated: Instant = WhiskEntity.currentMillis(),\n                               binding: Option[EntityPath] = None)\n    extends WhiskActionLikeMetaData(name) {\n\n  require(exec != null, \"exec undefined\")\n  require(limits != null, \"limits undefined\")\n\n  /**\n   * Merges parameters (usually from package) with existing action parameters.\n   * Existing parameters supersede those in p.\n   */\n  def inherit(p: Parameters, binding: Option[EntityPath] = None) =\n    copy(parameters = p ++ parameters, binding = binding).revision[WhiskActionMetaData](rev)\n\n  /**\n   * Resolves sequence components if they contain default namespace.\n   */\n  protected[core] def resolve(userNamespace: Namespace): WhiskActionMetaData = {\n    exec match {\n      case SequenceExecMetaData(components) =>\n        val newExec = SequenceExecMetaData(components map { c =>\n          FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace.name), c.name)\n        })\n        copy(exec = newExec).revision[WhiskActionMetaData](rev)\n      case _ => this\n    }\n  }\n\n  def toExecutableWhiskAction = exec match {\n    case execMetaData: ExecMetaData =>\n      Some(\n        ExecutableWhiskActionMetaData(\n          namespace,\n          name,\n          execMetaData,\n          parameters,\n          limits,\n          version,\n          publish,\n          annotations,\n          binding)\n          .revision[ExecutableWhiskActionMetaData](rev))\n    case _ =>\n      None\n  }\n}\n\n/**\n * Variant of WhiskAction which only includes information necessary to be\n * executed by an Invoker.\n *\n * exec is typed to CodeExec to guarantee executability by an Invoker.\n *\n * Note: Two actions are equal regardless of their DocRevision if there is one.\n * The invoker uses action equality when matching actions to warm containers.\n * That means creating an action, invoking it, then deleting/recreating/reinvoking\n * it will reuse the previous container. The delete/recreate restores the SemVer to 0.0.1.\n *\n * @param namespace the namespace for the action\n * @param name the name of the action\n * @param exec the action executable details\n * @param parameters the set of parameters to bind to the action environment\n * @param limits the limits to impose on the action\n * @param version the semantic version\n * @param publish true to share the action or false otherwise\n * @param annotations the set of annotations to attribute to the action\n * @param binding the path of the package binding if any\n * @throws IllegalArgumentException if any argument is undefined\n */\n@throws[IllegalArgumentException]\ncase class ExecutableWhiskAction(namespace: EntityPath,\n                                 override val name: EntityName,\n                                 exec: CodeExec[_],\n                                 parameters: Parameters = Parameters(),\n                                 limits: ActionLimits = ActionLimits(),\n                                 version: SemVer = SemVer(),\n                                 publish: Boolean = false,\n                                 annotations: Parameters = Parameters(),\n                                 binding: Option[EntityPath] = None)\n    extends WhiskActionLike(name) {\n\n  require(exec != null, \"exec undefined\")\n  require(limits != null, \"limits undefined\")\n\n  /**\n   * Gets initializer for action. This typically includes the code to execute,\n   * or a zip file containing the executable artifacts.\n   *\n   * @param env optional map of properties to be exported to the environment\n   */\n  def containerInitializer(env: Map[String, JsValue] = Map.empty): JsObject = {\n    val code = Option(exec.codeAsJson).filter(_ != JsNull).map(\"code\" -> _)\n    val envargs = if (env.nonEmpty) {\n      val stringifiedEnvVars = env.map {\n        case (k, v: JsString)  => (k, v)\n        case (k, JsNull)       => (k, JsString.empty)\n        case (k, JsBoolean(v)) => (k, JsString(v.toString))\n        case (k, JsNumber(v))  => (k, JsString(v.toString))\n        case (k, v)            => (k, JsString(v.compactPrint))\n      }\n\n      Some(\"env\" -> JsObject(stringifiedEnvVars))\n    } else None\n\n    val base =\n      Map(\"name\" -> name.toJson, \"binary\" -> exec.binary.toJson, \"main\" -> exec.entryPoint.getOrElse(\"main\").toJson)\n\n    JsObject(base ++ envargs ++ code)\n  }\n\n  def toWhiskAction =\n    WhiskAction(namespace, name, exec, parameters, limits, version, publish, annotations)\n      .revision[WhiskAction](rev)\n}\n\n@throws[IllegalArgumentException]\ncase class ExecutableWhiskActionMetaData(namespace: EntityPath,\n                                         override val name: EntityName,\n                                         exec: ExecMetaData,\n                                         parameters: Parameters = Parameters(),\n                                         limits: ActionLimits = ActionLimits(),\n                                         version: SemVer = SemVer(),\n                                         publish: Boolean = false,\n                                         annotations: Parameters = Parameters(),\n                                         binding: Option[EntityPath] = None)\n    extends WhiskActionLikeMetaData(name) {\n\n  require(exec != null, \"exec undefined\")\n  require(limits != null, \"limits undefined\")\n\n  def toWhiskAction =\n    WhiskActionMetaData(namespace, name, exec, parameters, limits, version, publish, annotations, updated)\n      .revision[WhiskActionMetaData](rev)\n\n  /**\n   * Some fully qualified name only if there's a binding, else None.\n   */\n  def bindingFullyQualifiedName: Option[FullyQualifiedEntityName] =\n    binding.map(ns => FullyQualifiedEntityName(ns, name, None))\n\n}\n\nobject WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[WhiskAction] with DefaultJsonProtocol {\n  import WhiskActivation.instantSerdes\n\n  val execFieldName = \"exec\"\n  val requireWhiskAuthHeader = \"x-require-whisk-auth\"\n\n  override val collectionName = \"actions\"\n  override val cacheEnabled = true\n\n  override implicit val serdes = jsonFormat(\n    WhiskAction.apply,\n    \"namespace\",\n    \"name\",\n    \"exec\",\n    \"parameters\",\n    \"limits\",\n    \"version\",\n    \"publish\",\n    \"annotations\",\n    \"updated\")\n\n  // overridden to store attached code\n  override def put[A >: WhiskAction](db: ArtifactStore[A], doc: WhiskAction, old: Option[WhiskAction])(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo] = {\n\n    def putWithAttachment(code: String, binary: Boolean, exec: AttachedCode) = {\n      implicit val logger = db.logging\n      implicit val ec = db.executionContext\n\n      val oldAttachment = old.flatMap(getAttachment)\n      val (bytes, attachmentType) = if (binary) {\n        (Base64.getDecoder.decode(code), ContentTypes.`application/octet-stream`)\n      } else {\n        (code.getBytes(UTF_8), ContentTypes.`text/plain(UTF-8)`)\n      }\n      val stream = new ByteArrayInputStream(bytes)\n      super.putAndAttach(\n        db,\n        doc\n          .copy(parameters = doc.parameters.lock(ParameterEncryption.singleton.default))\n          .revision[WhiskAction](doc.rev),\n        attachmentUpdater,\n        attachmentType,\n        stream,\n        oldAttachment,\n        Some { a: WhiskAction =>\n          a.copy(exec = exec.inline(code.getBytes(UTF_8)))\n        })\n    }\n\n    Try {\n      require(db != null, \"db undefined\")\n      require(doc != null, \"doc undefined\")\n    } map { _ =>\n      doc.exec match {\n        case exec @ CodeExecAsAttachment(_, Inline(code), _, binary) =>\n          putWithAttachment(code, binary, exec)\n        case exec @ BlackBoxExec(_, Some(Inline(code)), _, _, binary) =>\n          putWithAttachment(code, binary, exec)\n        case _ =>\n          super.put(\n            db,\n            doc\n              .copy(parameters = doc.parameters.lock(ParameterEncryption.singleton.default))\n              .revision[WhiskAction](doc.rev),\n            old)\n      }\n    } match {\n      case Success(f) => f\n      case Failure(f) => Future.failed(f)\n    }\n  }\n\n  // overridden to retrieve attached code\n  override def get[A >: WhiskAction](db: ArtifactStore[A],\n                                     doc: DocId,\n                                     rev: DocRevision = DocRevision.empty,\n                                     fromCache: Boolean,\n                                     ignoreMissingAttachment: Boolean = false)(\n    implicit transid: TransactionId,\n    mw: Manifest[WhiskAction]): Future[WhiskAction] = {\n\n    implicit val ec = db.executionContext\n\n    val inlineActionCode: WhiskAction => Future[WhiskAction] = { action =>\n      def getWithAttachment(attached: Attached, binary: Boolean, exec: AttachedCode) = {\n        val boas = new ByteArrayOutputStream()\n        val wrapped = if (binary) Base64.getEncoder().wrap(boas) else boas\n\n        getAttachment[A](db, action, attached, wrapped, Some { a: WhiskAction =>\n          wrapped.close()\n          val newAction = a.copy(exec = exec.inline(boas.toByteArray))\n          newAction.revision(a.rev)\n          newAction\n        }).recover({\n          case _: NoDocumentException if ignoreMissingAttachment =>\n            action\n        })\n      }\n\n      action.exec match {\n        case exec @ CodeExecAsAttachment(_, attached: Attached, _, binary) =>\n          getWithAttachment(attached, binary, exec)\n        case exec @ BlackBoxExec(_, Some(attached: Attached), _, _, binary) =>\n          getWithAttachment(attached, binary, exec)\n        case _ =>\n          Future.successful(action)\n      }\n    }\n    super.getWithAttachment(db, doc, rev, fromCache, attachmentHandler, inlineActionCode)\n  }\n\n  def attachmentHandler(action: WhiskAction, attached: Attached): WhiskAction = {\n    def checkName(name: String) = {\n      require(\n        name == attached.attachmentName,\n        s\"Attachment name '${attached.attachmentName}' does not match the expected name '$name'\")\n    }\n    val eu = action.exec match {\n      case exec @ CodeExecAsAttachment(_, Attached(attachmentName, _, _, _), _, _) =>\n        checkName(attachmentName)\n        exec.attach(attached)\n      case exec @ BlackBoxExec(_, Some(Attached(attachmentName, _, _, _)), _, _, _) =>\n        checkName(attachmentName)\n        exec.attach(attached)\n      case exec => exec\n    }\n    action.copy(exec = eu).revision[WhiskAction](action.rev)\n  }\n\n  def attachmentUpdater(action: WhiskAction, updatedAttachment: Attached): WhiskAction = {\n    action.exec match {\n      case exec: AttachedCode =>\n        action.copy(exec = exec.attach(updatedAttachment)).revision[WhiskAction](action.rev)\n      case _ => action\n    }\n  }\n\n  def getAttachment(action: WhiskAction): Option[Attached] = {\n    action.exec match {\n      case CodeExecAsAttachment(_, a: Attached, _, _)  => Some(a)\n      case BlackBoxExec(_, Some(a: Attached), _, _, _) => Some(a)\n      case _                                           => None\n    }\n  }\n\n  override def del[Wsuper >: WhiskAction](db: ArtifactStore[Wsuper], doc: DocInfo)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[Boolean] = {\n    Try {\n      require(db != null, \"db undefined\")\n      require(doc != null, \"doc undefined\")\n    }.map { _ =>\n      val fa = super.del(db, doc)\n      implicit val ec = db.executionContext\n      fa.flatMap { _ =>\n        super.deleteAttachments(db, doc)\n      }\n    } match {\n      case Success(f) => f\n      case Failure(f) => Future.failed(f)\n    }\n  }\n\n  /**\n   * Resolves an action name if it is contained in a package.\n   * Look up the package to determine if it is a binding or the actual package.\n   * If it's a binding, rewrite the fully qualified name of the action using the actual package path name.\n   * If it's the actual package, use its name directly as the package path name.\n   */\n  def resolveAction(db: EntityStore, fullyQualifiedActionName: FullyQualifiedEntityName)(\n    implicit ec: ExecutionContext,\n    transid: TransactionId): Future[FullyQualifiedEntityName] = {\n    // first check that there is a package to be resolved\n    val entityPath = fullyQualifiedActionName.path\n    if (entityPath.defaultPackage) {\n      // this is the default package, nothing to resolve\n      Future.successful(fullyQualifiedActionName)\n    } else {\n      // there is a package to be resolved\n      val pkgDocId = fullyQualifiedActionName.path.toDocId\n      val actionName = fullyQualifiedActionName.name\n      WhiskPackage.resolveBinding(db, pkgDocId) map {\n        _.fullyQualifiedName(withVersion = false).add(actionName)\n      }\n    }\n  }\n\n  /**\n   * Resolves an action name if it is contained in a package.\n   * Look up the package to determine if it is a binding or the actual package.\n   * If it's a binding, rewrite the fully qualified name of the action using the actual package path name.\n   * If it's the actual package, use its name directly as the package path name.\n   * While traversing the package bindings, merge the parameters.\n   */\n  def resolveActionAndMergeParameters(entityStore: EntityStore, fullyQualifiedName: FullyQualifiedEntityName)(\n    implicit ec: ExecutionContext,\n    transid: TransactionId): Future[WhiskAction] = {\n    // first check that there is a package to be resolved\n    val entityPath = fullyQualifiedName.path\n    if (entityPath.defaultPackage) {\n      // this is the default package, nothing to resolve\n      WhiskAction.get(entityStore, fullyQualifiedName.toDocId)\n    } else {\n      // there is a package to be resolved\n      val pkgDocid = fullyQualifiedName.path.toDocId\n      val actionName = fullyQualifiedName.name\n      val wp = WhiskPackage.resolveBinding(entityStore, pkgDocid, mergeParameters = true)\n      wp flatMap { resolvedPkg =>\n        // fully resolved name for the action\n        val fqnAction = resolvedPkg.fullyQualifiedName(withVersion = false).add(actionName)\n        // get the whisk action associate with it and inherit the parameters from the package/binding\n        WhiskAction.get(entityStore, fqnAction.toDocId) map {\n          _.inherit(resolvedPkg.parameters)\n        }\n      }\n    }\n  }\n}\n\nobject WhiskActionMetaData\n    extends DocumentFactory[WhiskActionMetaData]\n    with WhiskEntityQueries[WhiskActionMetaData]\n    with DefaultJsonProtocol {\n\n  import WhiskActivation.instantSerdes\n\n  override val collectionName = \"actions\"\n  override val cacheEnabled = true\n\n  override implicit val serdes = jsonFormat(\n    WhiskActionMetaData.apply,\n    \"namespace\",\n    \"name\",\n    \"exec\",\n    \"parameters\",\n    \"limits\",\n    \"version\",\n    \"publish\",\n    \"annotations\",\n    \"updated\",\n    \"binding\")\n\n  /**\n   * Resolves an action name if it is contained in a package.\n   * Look up the package to determine if it is a binding or the actual package.\n   * If it's a binding, rewrite the fully qualified name of the action using the actual package path name.\n   * If it's the actual package, use its name directly as the package path name.\n   */\n  def resolveAction(db: EntityStore, fullyQualifiedActionName: FullyQualifiedEntityName)(\n    implicit ec: ExecutionContext,\n    transid: TransactionId): Future[FullyQualifiedEntityName] = {\n    // first check that there is a package to be resolved\n    val entityPath = fullyQualifiedActionName.path\n    if (entityPath.defaultPackage) {\n      // this is the default package, nothing to resolve\n      Future.successful(fullyQualifiedActionName)\n    } else {\n      // there is a package to be resolved\n      val pkgDocId = fullyQualifiedActionName.path.toDocId\n      val actionName = fullyQualifiedActionName.name\n      WhiskPackage.resolveBinding(db, pkgDocId) map {\n        _.fullyQualifiedName(withVersion = false).add(actionName)\n      }\n    }\n  }\n\n  /**\n   * Resolves an action name if it is contained in a package.\n   * Look up the package to determine if it is a binding or the actual package.\n   * If it's a binding, rewrite the fully qualified name of the action using the actual package path name.\n   * If it's the actual package, use its name directly as the package path name.\n   * While traversing the package bindings, merge the parameters.\n   */\n  def resolveActionAndMergeParameters(entityStore: EntityStore, fullyQualifiedName: FullyQualifiedEntityName)(\n    implicit ec: ExecutionContext,\n    transid: TransactionId): Future[WhiskActionMetaData] = {\n    // first check that there is a package to be resolved\n    val entityPath = fullyQualifiedName.path\n    if (entityPath.defaultPackage) {\n      // this is the default package, nothing to resolve\n      WhiskActionMetaData.get(entityStore, fullyQualifiedName.toDocId)\n    } else {\n      // there is a package to be resolved\n      val pkgDocid = fullyQualifiedName.path.toDocId\n      val actionName = fullyQualifiedName.name\n      val wp = WhiskPackage.resolveBinding(entityStore, pkgDocid, mergeParameters = true)\n      wp flatMap { resolvedPkg =>\n        // fully resolved name for the action\n        val fqnAction = resolvedPkg.fullyQualifiedName(withVersion = false).add(actionName)\n        // get the whisk action associate with it and inherit the parameters from the package/binding\n        WhiskActionMetaData.get(entityStore, fqnAction.toDocId) map {\n          _.inherit(\n            resolvedPkg.parameters,\n            if (fullyQualifiedName.path.equals(resolvedPkg.fullPath)) None\n            else Some(fullyQualifiedName.path))\n        }\n      }\n    }\n  }\n}\n\nobject ActionLimitsOption extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat5(ActionLimitsOption.apply)\n}\n\nobject WhiskActionPut extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat7(WhiskActionPut.apply)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskActivation.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.time.Instant\n\nimport scala.concurrent.Future\nimport scala.util.Try\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.{ArtifactStore, CacheChangeNotification, DocumentFactory, StaleParameter}\n\n/**\n * A WhiskActivation provides an abstraction of the meta-data\n * for a whisk action activation record.\n *\n * The WhiskActivation object is used as a helper to adapt objects between\n * the schema used by the database and the WhiskAuth abstraction.\n *\n * @param namespace the namespace for the activation\n * @param name the name of the activated entity\n * @param subject the subject activating the entity\n * @param activationId the activation id\n * @param start the start of the activation in epoch millis\n * @param end the end of the activation in epoch millis\n * @param cause the activation id of the activated entity that causes this activation\n * @param response the activation response\n * @param logs the activation logs\n * @param version the semantic version (usually matches the activated entity)\n * @param publish true to share the activation or false otherwise\n * @param annotations the set of annotations to attribute to the activation\n * @param duration of the activation in milliseconds\n * @throws IllegalArgumentException if any required argument is undefined\n */\n@throws[IllegalArgumentException]\ncase class WhiskActivation(namespace: EntityPath,\n                           override val name: EntityName,\n                           subject: Subject,\n                           activationId: ActivationId,\n                           start: Instant,\n                           end: Instant,\n                           cause: Option[ActivationId] = None,\n                           response: ActivationResponse = ActivationResponse.success(),\n                           logs: ActivationLogs = ActivationLogs(),\n                           version: SemVer = SemVer(),\n                           publish: Boolean = false,\n                           annotations: Parameters = Parameters(),\n                           duration: Option[Long] = None)\n    extends WhiskEntity(EntityName(activationId.asString), \"activation\") {\n\n  require(cause != null, \"cause undefined\")\n  require(start != null, \"start undefined\")\n  require(end != null, \"end undefined\")\n  require(response != null, \"response undefined\")\n\n  def toJson = WhiskActivation.serdes.write(this).asJsObject\n\n  /**\n   * This the activation summary as computed by the database view.\n   * Strictly used in view testing to enforce alignment.\n   */\n  override def summaryAsJson = {\n    import WhiskActivation.instantSerdes\n\n    def actionOrNot() = {\n      if (end != Instant.EPOCH) {\n        Map(\n          \"end\" -> end.toJson,\n          \"duration\" -> (duration getOrElse (end.toEpochMilli - start.toEpochMilli)).toJson,\n          \"statusCode\" -> response.statusCode.toJson)\n      } else Map.empty\n    }\n\n    JsObject(\n      super.summaryAsJson.fields - \"updated\" +\n        (\"activationId\" -> activationId.toJson) +\n        (\"start\" -> start.toJson) ++\n        cause.map((\"cause\" -> _.toJson)) ++\n        actionOrNot())\n  }\n\n  def resultAsJson = response.result.toJson.asJsObject\n\n  def toExtendedJson(removeFields: Seq[String] = Seq.empty, addFields: Map[String, JsValue] = Map.empty) = {\n    val JsObject(baseFields) = WhiskActivation.serdes.write(this).asJsObject\n\n    val newFields = (baseFields - \"response\") + (\"response\" -> response.toExtendedJson) -- removeFields ++ addFields\n    if (end != Instant.EPOCH) {\n      val durationValue = (duration getOrElse (end.toEpochMilli - start.toEpochMilli)).toJson\n      JsObject(newFields + (\"duration\" -> durationValue))\n    } else {\n      JsObject(newFields - \"end\")\n    }\n  }\n\n  def metadata =\n    copy(response = response.withoutResult, annotations = Parameters(), logs = ActivationLogs())\n      .revision[WhiskActivation](rev)\n\n  def withoutResult =\n    copy(response = response.withoutResult)\n      .revision[WhiskActivation](rev)\n\n  def withoutLogsOrResult =\n    copy(response = response.withoutResult, logs = ActivationLogs()).revision[WhiskActivation](rev)\n\n  def withoutLogs = copy(logs = ActivationLogs()).revision[WhiskActivation](rev)\n\n  def withLogs(logs: ActivationLogs) = copy(logs = logs).revision[WhiskActivation](rev)\n\n  def isTimedoutActivation = annotations.getAs[Boolean](WhiskActivation.timeoutAnnotation).getOrElse(false)\n\n}\n\nobject WhiskActivation\n    extends DocumentFactory[WhiskActivation]\n    with WhiskEntityQueries[WhiskActivation]\n    with DefaultJsonProtocol {\n\n  /** Some field names for annotations */\n  val pathAnnotation = \"path\"\n  val bindingAnnotation = \"binding\"\n  val kindAnnotation = \"kind\"\n  val limitsAnnotation = \"limits\"\n  val topmostAnnotation = \"topmost\"\n  val causedByAnnotation = \"causedBy\"\n  val initTimeAnnotation = \"initTime\"\n  val waitTimeAnnotation = \"waitTime\"\n  val conductorAnnotation = \"conductor\"\n  val timeoutAnnotation = \"timeout\"\n\n  val memory = \"memory\"\n  val duration = \"duration\"\n  val statusCode = \"statusCode\"\n\n  /** Some field names for compositions */\n  val actionField = \"action\"\n  val paramsField = \"params\"\n  val stateField = \"state\"\n  val valueField = \"value\"\n\n  protected[entity] implicit val instantSerdes: RootJsonFormat[Instant] = new RootJsonFormat[Instant] {\n    def write(t: Instant) = t.toEpochMilli.toJson\n\n    def read(value: JsValue) =\n      Try {\n        value match {\n          case JsString(t) => Instant.parse(t)\n          case JsNumber(i) => Instant.ofEpochMilli(i.bigDecimal.longValue)\n          case _           => deserializationError(\"timestamp malformed\")\n        }\n      } getOrElse deserializationError(\"timestamp malformed\")\n  }\n\n  override val collectionName = \"activations\"\n\n  /** The main view for activations, keyed by namespace, sorted by date. */\n  override lazy val view = WhiskQueries.view(WhiskQueries.dbConfig.activationsDdoc, collectionName)\n\n  /**\n   * A view for activations in a namespace additionally keyed by action name\n   * (and package name if present) sorted by date.\n   */\n  lazy val filtersView = WhiskQueries.view(WhiskQueries.dbConfig.activationsFilterDdoc, collectionName)\n\n  override implicit val serdes = jsonFormat13(WhiskActivation.apply)\n\n  // Caching activations doesn't make much sense in the common case as usually,\n  // an activation is only asked for once.\n  override val cacheEnabled = false\n\n  /**\n   * Queries datastore for activation records which have an entity name matching the\n   * given parameter.\n   *\n   * @return list of records as JSON object if docs parameter is false, as Left\n   *         and a list of the WhiskActivations if including the full record, as Right\n   */\n  def listActivationsMatchingName(db: ArtifactStore[WhiskActivation],\n                                  namespace: EntityPath,\n                                  path: EntityPath,\n                                  skip: Int,\n                                  limit: Int,\n                                  includeDocs: Boolean = false,\n                                  since: Option[Instant] = None,\n                                  upto: Option[Instant] = None,\n                                  stale: StaleParameter = StaleParameter.No)(\n    implicit transid: TransactionId): Future[Either[List[JsObject], List[WhiskActivation]]] = {\n    import WhiskQueries.TOP\n    val convert = if (includeDocs) Some((o: JsObject) => Try { serdes.read(o) }) else None\n    val startKey = List(namespace.addPath(path).asString, since map { _.toEpochMilli } getOrElse 0)\n    val endKey = List(namespace.addPath(path).asString, upto map { _.toEpochMilli } getOrElse TOP, TOP)\n    query(db, filtersView, startKey, endKey, skip, limit, reduce = false, stale, convert)\n  }\n\n  def put[Wsuper >: WhiskActivation](db: ArtifactStore[Wsuper], doc: WhiskActivation)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo] =\n    //As activations are not updated we just pass None for the old document\n    super.put(db, doc, None)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskAuth.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport spray.json._\nimport scala.util.Try\n\n/**\n * Represents a namespace for a subject as stored in the authentication\n * database. Each namespace has its own key which is used to determine\n * the {@ Identity} of the user calling.\n */\nprotected[core] case class WhiskNamespace(namespace: Namespace, authkey: BasicAuthenticationAuthKey)\n\nprotected[core] object WhiskNamespace extends DefaultJsonProtocol {\n  implicit val serdes = new RootJsonFormat[WhiskNamespace] {\n    def write(w: WhiskNamespace) =\n      JsObject(\"name\" -> w.namespace.name.toJson, \"uuid\" -> w.namespace.uuid.toJson, \"key\" -> w.authkey.key.toJson)\n\n    def read(value: JsValue) =\n      Try {\n        value.asJsObject.getFields(\"name\", \"uuid\", \"key\") match {\n          case Seq(JsString(n), JsString(u), JsString(k)) =>\n            WhiskNamespace(Namespace(EntityName(n), UUID(u)), BasicAuthenticationAuthKey(UUID(u), Secret(k)))\n        }\n      } getOrElse deserializationError(\"namespace record malformed\")\n  }\n}\n\n/**\n * Represents the new version of entries in the subjects database. No\n * top-level authkey is given but each subject has a set of namespaces,\n * which in turn have the keys.\n */\nprotected[core] case class WhiskAuth(subject: Subject, namespaces: Set[WhiskNamespace]) extends WhiskDocument {\n\n  override def docid = DocId(subject.asString)\n\n  def toJson = JsObject(\"subject\" -> subject.toJson, \"namespaces\" -> namespaces.toJson)\n}\n\nprotected[core] object WhiskAuth extends DefaultJsonProtocol {\n  // Need to explicitly set field names since WhiskAuth extends WhiskDocument\n  // which defines more than the 2 \"standard\" fields\n  implicit val serdes = jsonFormat(WhiskAuth.apply, \"subject\", \"namespaces\")\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskEntity.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.time.Clock\nimport java.time.Instant\nimport java.time.temporal.ChronoUnit\n\nimport scala.util.Try\nimport spray.json._\nimport org.apache.openwhisk.core.database.DocumentUnreadable\nimport org.apache.openwhisk.core.database.DocumentTypeMismatchException\nimport org.apache.openwhisk.http.Messages\n\n/**\n * An abstract superclass that encapsulates properties common to all whisk entities (actions, rules, triggers).\n * The class has a private constructor argument and abstract fields so that case classes that extend this base\n * type can use the default spray JSON ser/des. An abstract entity has the following four properties.\n *\n * @param en the name of the entity, this is part of the primary key for the document\n * @param namespace the namespace for the entity as an abstract field\n * @param version the semantic version as an abstract field\n * @param publish true to share the entity and false to keep it private as an abstract field\n * @param annotations the set of annotations to attribute to the entity\n *\n * @throws IllegalArgumentException if any argument is undefined\n */\n@throws[IllegalArgumentException]\nabstract class WhiskEntity protected[entity] (en: EntityName, val entityType: String) extends WhiskDocument {\n\n  val namespace: EntityPath\n  val name = en\n  val version: SemVer\n  val publish: Boolean\n  val annotations: Parameters\n  val updated = WhiskEntity.currentMillis()\n\n  /**\n   * The name of the entity qualified with its namespace and version for\n   * creating unique keys in backend services.\n   */\n  final def fullyQualifiedName(withVersion: Boolean) =\n    FullyQualifiedEntityName(namespace, en, if (withVersion) Some(version) else None)\n\n  /** The primary key for the entity in the datastore */\n  override final def docid = fullyQualifiedName(false).toDocId\n\n  /**\n   * Returns a JSON object with the fields specific to this abstract class.\n   */\n  protected def entityDocumentRecord: JsObject =\n    JsObject(\n      \"name\" -> JsString(name.toString),\n      \"updated\" -> JsNumber(updated.toEpochMilli()),\n      \"entityType\" -> JsString(entityType))\n\n  override def toDocumentRecord: JsObject = {\n    val extraFields = entityDocumentRecord.fields\n    val base = super.toDocumentRecord\n\n    // In this order to make sure the subclass can rewrite using toJson.\n    JsObject(extraFields ++ base.fields)\n  }\n\n  /**\n   * @return the primary key (name) of the entity as a pithy description\n   */\n  override def toString = s\"${this.getClass.getSimpleName}/${fullyQualifiedName(true)}\"\n\n  /**\n   * A JSON view of the entity, that should match the result returned in a list operation.\n   * This should be synchronized with the views computed in the database.\n   * Strictly used in view testing to enforce alignment.\n   */\n  def summaryAsJson: JsObject = {\n    import WhiskActivation.instantSerdes\n    JsObject(\n      \"namespace\" -> namespace.toJson,\n      \"name\" -> name.toJson,\n      \"version\" -> version.toJson,\n      WhiskEntity.sharedFieldName -> JsBoolean(publish),\n      \"annotations\" -> annotations.toJsArray,\n      \"updated\" -> updated.toJson)\n  }\n}\n\nobject WhiskEntity {\n\n  val sharedFieldName = \"publish\"\n  val paramsFieldName = \"parameters\"\n  val annotationsFieldName = \"annotations\"\n\n  /**\n   * Gets fully qualified name of an activation based on its namespace and activation id.\n   */\n  def qualifiedName(namespace: EntityPath, activationId: ActivationId) = {\n    s\"$namespace${EntityPath.PATHSEP}$activationId\"\n  }\n\n  /**\n   * Get Instant object with a millisecond precision\n   * timestamp of whisk entity is stored in milliseconds in the db\n   */\n  def currentMillis() = {\n    Instant.now(Clock.systemUTC()).truncatedTo(ChronoUnit.MILLIS)\n  }\n\n}\n\nobject WhiskDocumentReader extends DocumentReader {\n  override def read[A](ma: Manifest[A], value: JsValue) = {\n    val doc = ma.runtimeClass match {\n      case x if x == classOf[WhiskAction]         => WhiskAction.serdes.read(value)\n      case x if x == classOf[WhiskActionMetaData] => WhiskActionMetaData.serdes.read(value)\n      case x if x == classOf[WhiskPackage]        => WhiskPackage.serdes.read(value)\n      case x if x == classOf[WhiskActivation]     => WhiskActivation.serdes.read(value)\n      case x if x == classOf[WhiskTrigger]        => WhiskTrigger.serdes.read(value)\n      case x if x == classOf[WhiskRule]           => WhiskRule.serdes.read(value)\n      case _                                      => throw DocumentUnreadable(Messages.corruptedEntity)\n    }\n    value.asJsObject.fields.get(\"entityType\").foreach {\n      case JsString(entityType) if (doc.entityType != entityType) =>\n        throw DocumentTypeMismatchException(s\"document type ${doc.entityType} did not match expected type $entityType.\")\n      case _ =>\n    }\n    doc\n  }\n}\n\n/**\n * Dispatches to appropriate serdes. This object is not itself implicit so as to\n * avoid multiple implicit alternatives when working with one of the subtypes.\n */\nobject WhiskEntityJsonFormat extends RootJsonFormat[WhiskEntity] {\n  // THE ORDER MATTERS! E.g. some triggers can deserialize as packages, but not\n  // the other way around. Try most specific first!\n  private def readers: Stream[JsValue => WhiskEntity] =\n    Stream(\n      WhiskAction.serdes.read,\n      WhiskActivation.serdes.read,\n      WhiskRule.serdes.read,\n      WhiskTrigger.serdes.read,\n      WhiskPackage.serdes.read)\n\n  // Not necessarily the smartest way to go about this. In theory, whenever\n  // a more precise type is known, this method shouldn't be used.\n  override def read(js: JsValue): WhiskEntity = {\n    val successes: Stream[WhiskEntity] = readers.flatMap(r => Try(r(js)).toOption)\n    successes.headOption.getOrElse {\n      throw DocumentUnreadable(Messages.corruptedEntity)\n    }\n  }\n\n  override def write(we: WhiskEntity): JsValue = we match {\n    case a: WhiskAction     => WhiskAction.serdes.write(a)\n    case a: WhiskActivation => WhiskActivation.serdes.write(a)\n    case p: WhiskPackage    => WhiskPackage.serdes.write(p)\n    case r: WhiskRule       => WhiskRule.serdes.write(r)\n    case t: WhiskTrigger    => WhiskTrigger.serdes.write(t)\n  }\n}\n\n/**\n * Trait for the objects we want to size. The size will be defined as ByteSize.\n */\ntrait ByteSizeable {\n\n  /**\n   * Method to calculate the size of the object.\n   * The size of the object is defined as the sum of sizes of all parameters, that is stored in the object.\n   *\n   * @return the size of the object as ByteSize\n   */\n  def size: ByteSize\n}\n\nobject LimitedWhiskEntityPut extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat3(LimitedWhiskEntityPut.apply)\n}\n\ncase class SizeError(field: String, is: ByteSize, allowed: ByteSize)\n\ncase class LimitedWhiskEntityPut(exec: Option[Exec] = None,\n                                 parameters: Option[Parameters] = None,\n                                 annotations: Option[Parameters] = None) {\n\n  def isWithinSizeLimits(userLimits: UserLimits): Option[SizeError] = {\n\n    val parameterSizeLimit = userLimits.allowedMaxParameterSize\n\n    exec.flatMap { e =>\n      val is = e.size\n      if (is <= Exec.sizeLimit) None\n      else\n        Some {\n          SizeError(WhiskAction.execFieldName, is, Exec.sizeLimit)\n        }\n    } orElse parameters.flatMap { p =>\n      val is = p.size\n      if (is <= parameterSizeLimit) None\n      else\n        Some {\n          SizeError(WhiskEntity.paramsFieldName, is, parameterSizeLimit)\n        }\n    } orElse annotations.flatMap { a =>\n      val is = a.size\n      if (is <= parameterSizeLimit) None\n      else\n        Some {\n          SizeError(WhiskEntity.annotationsFieldName, is, parameterSizeLimit)\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskPackage.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.time.Instant\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.language.postfixOps\nimport scala.util.Try\nimport spray.json.DefaultJsonProtocol\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.{ArtifactStore, CacheChangeNotification, DocumentFactory}\nimport org.apache.openwhisk.core.entity.types.EntityStore\n\n/**\n * WhiskPackagePut is a restricted WhiskPackage view that eschews properties\n * that are auto-assigned or derived from URI: namespace and name.\n */\ncase class WhiskPackagePut(binding: Option[Binding] = None,\n                           parameters: Option[Parameters] = None,\n                           version: Option[SemVer] = None,\n                           publish: Option[Boolean] = None,\n                           annotations: Option[Parameters] = None) {\n\n  /**\n   * Resolves the binding if it contains the default namespace.\n   */\n  protected[core] def resolve(namespace: EntityName): WhiskPackagePut = {\n    WhiskPackagePut(binding.map(_.resolve(namespace)), parameters, version, publish, annotations)\n  }\n}\n\n/**\n * A WhiskPackage provides an abstraction of the meta-data for a whisk package\n * or package binding.\n *\n * The WhiskPackage object is used as a helper to adapt objects between\n * the schema used by the database and the WhiskPackage abstraction.\n *\n * @param namespace the namespace for the action\n * @param name the name of the action\n * @param binding an optional binding, None for provider, Some for binding\n * @param parameters the set of parameters to bind to the action environment\n * @param version the semantic version\n * @param publish true to share the action or false otherwise\n * @param annotations the set of annotations to attribute to the package\n * @param updated the timestamp when the package is updated\n * @throws IllegalArgumentException if any argument is undefined\n */\n@throws[IllegalArgumentException]\ncase class WhiskPackage(namespace: EntityPath,\n                        override val name: EntityName,\n                        binding: Option[Binding] = None,\n                        parameters: Parameters = Parameters(),\n                        version: SemVer = SemVer(),\n                        publish: Boolean = false,\n                        annotations: Parameters = Parameters(),\n                        override val updated: Instant = WhiskEntity.currentMillis())\n    extends WhiskEntity(name, \"package\") {\n\n  require(binding != null || (binding map { _ != null } getOrElse true), \"binding undefined\")\n\n  /**\n   * Merges parameters into existing set of parameters for package.\n   * Existing parameters supersede those in p.\n   */\n  def inherit(p: Parameters): WhiskPackage = copy(parameters = p ++ parameters).revision[WhiskPackage](rev)\n\n  /**\n   * Merges parameters into existing set of parameters for package.\n   * The parameters from p supersede parameters from this.\n   */\n  def mergeParameters(p: Parameters): WhiskPackage = copy(parameters = parameters ++ p).revision[WhiskPackage](rev)\n\n  /**\n   * Gets the full path for the package.\n   * This is equivalent to calling this this.fullyQualifiedName(withVersion = false).fullPath.\n   */\n  def fullPath: EntityPath = namespace.addPath(name)\n\n  /**\n   * Gets binding for package iff this is not already a package reference.\n   */\n  def bind: Option[Binding] = if (binding.isEmpty) Some(Binding(namespace.root, name)) else None\n\n  /**\n   * Adds actions to package. The actions list is filtered so that only actions that\n   * match the package are included (must match package namespace/name).\n   */\n  def withActions(actions: List[WhiskAction] = List.empty): WhiskPackageWithActions = {\n    withPackageActions(actions filter { a =>\n      val pkgns = binding map { b =>\n        b.namespace.addPath(b.name)\n      } getOrElse { namespace.addPath(name) }\n      a.namespace == pkgns\n    } map { a =>\n      WhiskPackageAction(a.name, a.version, a.annotations)\n    })\n  }\n\n  /**\n   * Adds package actions to package as actions or feeds. An action is considered a feed\n   * is it defined the property \"feed\" in the annotation. The value of the property is ignored\n   * for this check.\n   */\n  def withPackageActions(actions: List[WhiskPackageAction] = List.empty): WhiskPackageWithActions = {\n    val (feedActions, nonFeedActions) = actions.partition(_.annotations.get(Parameters.Feed).isDefined)\n    WhiskPackageWithActions(this, nonFeedActions, feedActions)\n  }\n\n  def toJson = WhiskPackage.serdes.write(this).asJsObject\n\n  /**\n   * This the package summary as computed by the database view.\n   * Strictly used in view testing to enforce alignment.\n   */\n  override def summaryAsJson = {\n    JsObject(\n      super.summaryAsJson.fields +\n        (WhiskPackage.bindingFieldName -> binding.map(Binding.serdes.write(_)).getOrElse(JsFalse)))\n  }\n}\n\n/**\n * A specialized view of a whisk action contained in a package.\n * Eschews fields that are implied by package in a GET package response..\n */\ncase class WhiskPackageAction(name: EntityName, version: SemVer, annotations: Parameters)\n\n/**\n * Extends WhiskPackage to include list of actions contained in package.\n * This is used in GET package response.\n */\ncase class WhiskPackageWithActions(wp: WhiskPackage, actions: List[WhiskPackageAction], feeds: List[WhiskPackageAction])\n\nobject WhiskPackage\n    extends DocumentFactory[WhiskPackage]\n    with WhiskEntityQueries[WhiskPackage]\n    with DefaultJsonProtocol {\n\n  import WhiskActivation.instantSerdes\n\n  val bindingFieldName = \"binding\"\n  override val collectionName = \"packages\"\n\n  /**\n   * Traverses a binding recursively to find the root package and\n   * merges parameters along the way if mergeParameters flag is set.\n   *\n   * @param db the entity store containing packages\n   * @param pkg the package document id to start resolving\n   * @param mergeParameters flag that indicates whether parameters should be merged during package resolution\n   * @return the same package if there is no binding, or the actual reference package otherwise\n   */\n  def resolveBinding(db: EntityStore, pkg: DocId, mergeParameters: Boolean = false)(\n    implicit ec: ExecutionContext,\n    transid: TransactionId): Future[WhiskPackage] = {\n    WhiskPackage.get(db, pkg) flatMap { wp =>\n      // if there is a binding resolve it\n      val resolved = wp.binding map { binding =>\n        if (mergeParameters) {\n          resolveBinding(db, binding.docid, true) map { resolvedPackage =>\n            resolvedPackage.mergeParameters(wp.parameters)\n          }\n        } else resolveBinding(db, binding.docid)\n      }\n      resolved getOrElse Future.successful(wp)\n    }\n  }\n\n  override implicit val serdes = {\n\n    /**\n     * Custom serdes for a binding - this property must be present in the datastore records for\n     * packages so that views can map over packages vs bindings.\n     */\n    implicit val bindingOverride = new JsonFormat[Option[Binding]] {\n      override def write(b: Option[Binding]) = Binding.optionalBindingSerializer.write(b)\n      override def read(js: JsValue) = Binding.optionalBindingDeserializer.read(js)\n    }\n    jsonFormat8(WhiskPackage.apply)\n  }\n\n  override val cacheEnabled = true\n\n  lazy val publicPackagesView: View = WhiskQueries.entitiesView(collection = s\"$collectionName-public\")\n\n  // overridden to store encrypted parameters.\n  override def put[A >: WhiskPackage](db: ArtifactStore[A], doc: WhiskPackage, old: Option[WhiskPackage])(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): Future[DocInfo] = {\n    super.put(\n      db,\n      doc.copy(parameters = doc.parameters.lock(ParameterEncryption.singleton.default)).revision[WhiskPackage](doc.rev),\n      old)\n  }\n}\n\n/**\n * A package binding holds a reference to the providing package\n * namespace and package name.\n */\ncase class Binding(namespace: EntityName, name: EntityName) {\n  def fullyQualifiedName = FullyQualifiedEntityName(namespace.toPath, name)\n  def docid = fullyQualifiedName.toDocId\n  override def toString = fullyQualifiedName.toString\n\n  /**\n   * Returns a Binding namespace if it is the default namespace\n   * to the given one, otherwise this is an identity.\n   */\n  def resolve(ns: EntityName): Binding = {\n    namespace.toPath match {\n      case EntityPath.DEFAULT => Binding(ns, name)\n      case _                  => this\n    }\n  }\n}\n\nobject Binding extends ArgNormalizer[Binding] with DefaultJsonProtocol {\n\n  override protected[core] val serdes = jsonFormat2(Binding.apply)\n\n  protected[entity] val optionalBindingDeserializer = new JsonReader[Option[Binding]] {\n    override def read(js: JsValue) = {\n      if (js == JsObject.empty) None else Some(serdes.read(js))\n    }\n\n  }\n\n  protected[entity] val optionalBindingSerializer = new JsonWriter[Option[Binding]] {\n    override def write(b: Option[Binding]) = b match {\n      case None    => JsObject.empty\n      case Some(n) => Binding.serdes.write(n)\n    }\n  }\n}\n\nobject WhiskPackagePut extends DefaultJsonProtocol {\n  implicit val serdes = {\n    implicit val bindingSerdes = Binding.serdes\n    implicit val optionalBindingSerdes = new OptionFormat[Binding] {\n      override def read(js: JsValue) = Binding.optionalBindingDeserializer.read(js)\n      override def write(n: Option[Binding]) = Binding.optionalBindingSerializer.write(n)\n    }\n    jsonFormat5(WhiskPackagePut.apply)\n  }\n}\n\nobject WhiskPackageAction extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat3(WhiskPackageAction.apply)\n}\n\nobject WhiskPackageWithActions {\n  implicit val serdes = new RootJsonFormat[WhiskPackageWithActions] {\n    def write(w: WhiskPackageWithActions) = {\n      val JsObject(pkg) = WhiskPackage.serdes.write(w.wp)\n      JsObject(pkg + (\"actions\" -> w.actions.toJson) + (\"feeds\" -> w.feeds.toJson))\n    }\n\n    def read(value: JsValue) =\n      Try {\n        val pkg = WhiskPackage.serdes.read(value)\n        val actions = value.asJsObject.getFields(\"actions\") match {\n          case Seq(JsArray(as)) =>\n            as map { a =>\n              WhiskPackageAction.serdes.read(a)\n            } toList\n          case _ => List.empty\n        }\n        val feeds = value.asJsObject.getFields(\"feeds\") match {\n          case Seq(JsArray(as)) =>\n            as map { a =>\n              WhiskPackageAction.serdes.read(a)\n            } toList\n          case _ => List.empty\n        }\n        WhiskPackageWithActions(pkg, actions, feeds)\n      } getOrElse deserializationError(\"whisk package with actions malformed\")\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskRule.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.time.Instant\n\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport spray.json.DefaultJsonProtocol\nimport spray.json.DeserializationException\nimport spray.json.JsObject\nimport spray.json.JsString\nimport spray.json.JsValue\nimport spray.json.RootJsonFormat\nimport spray.json.deserializationError\nimport org.apache.openwhisk.core.database.DocumentFactory\n\n/**\n * WhiskRulePut is a restricted WhiskRule view that eschews properties\n * that are auto-assigned or derived from URI: namespace, name, status.\n */\ncase class WhiskRulePut(trigger: Option[FullyQualifiedEntityName] = None,\n                        action: Option[FullyQualifiedEntityName] = None,\n                        version: Option[SemVer] = None,\n                        publish: Option[Boolean] = None,\n                        annotations: Option[Parameters] = None) {\n\n  /**\n   * Resolves the trigger and action name if they contains the default namespace.\n   */\n  protected[core] def resolve(namespace: EntityName): WhiskRulePut = {\n    val t = trigger map { _.resolve(namespace) }\n    val a = action map { _.resolve(namespace) }\n    WhiskRulePut(t, a, version, publish, annotations)\n  }\n}\n\n/**\n * A WhiskRule provides an abstraction of the meta-data for a whisk rule.\n * A rule encodes a trigger to action mapping, such that when a trigger\n * event occurs, the corresponding action is invoked. Both trigger and\n * action are entities in the same namespace as the rule.\n *\n * The WhiskRule object is used as a helper to adapt objects between\n * the schema used by the database and the WhiskRule abstraction.\n *\n * @param namespace the namespace for the rule\n * @param name the name of the rule\n * @param trigger the trigger name to subscribe to\n * @param action the action name to invoke invoke when trigger is fired\n * @param version the semantic version\n * @param publish true to share the action or false otherwise\n * @param annotations the set of annotations to attribute to the rule\n * @param updated the timestamp when the rule is updated\n * @throws IllegalArgumentException if any argument is undefined\n */\n@throws[IllegalArgumentException]\ncase class WhiskRule(namespace: EntityPath,\n                     override val name: EntityName,\n                     trigger: FullyQualifiedEntityName,\n                     action: FullyQualifiedEntityName,\n                     version: SemVer = SemVer(),\n                     publish: Boolean = false,\n                     annotations: Parameters = Parameters(),\n                     override val updated: Instant = WhiskEntity.currentMillis())\n    extends WhiskEntity(name, \"rule\") {\n\n  def withStatus(s: Status) =\n    WhiskRuleResponse(namespace, name, s, trigger, action, version, publish, annotations, updated)\n\n  def toJson = WhiskRule.serdes.write(this).asJsObject\n}\n\n/**\n * Rule as it is returned by the controller. Basically the same as WhiskRule,\n * but including the Status which is gotten from the WhiskTrigger the rule\n * refers to.\n *\n * @param namespace the namespace for the rule\n * @param name the name of the rule\n * @param status the status of the rule (one of active, inactive, changing)\n * @param trigger the trigger name to subscribe to\n * @param action the action name to invoke invoke when trigger is fired\n * @param version the semantic version\n * @param publish true to share the action or false otherwise\n * @param annotations the set of annotations to attribute to the rule\n */\ncase class WhiskRuleResponse(namespace: EntityPath,\n                             name: EntityName,\n                             status: Status,\n                             trigger: FullyQualifiedEntityName,\n                             action: FullyQualifiedEntityName,\n                             version: SemVer = SemVer(),\n                             publish: Boolean = false,\n                             annotations: Parameters = Parameters(),\n                             updated: Instant) {\n\n  def toWhiskRule = WhiskRule(namespace, name, trigger, action, version, publish, annotations)\n}\n\n/**\n * Status of a rule, recorded in the datastore. It is one of these states:\n * 1) active when the rule is active and ready to respond to triggers\n * 2) inactive when the rule is not active and does not respond to trigger\n * 3) activating when the rule is between status change from inactive to active\n * 4) deactivating when the rule is between status change from active to inactive\n *\n * The \"[de]activating\" status are used as a mutex in the datastore to exclude\n * other state changes on a rule, and to facilitate synchronous activations\n * and deactivations of a rule.\n *\n * It is a value type (hence == is .equals, immutable and cannot be assigned null).\n * The constructor is private so that argument requirements are checked and normalized\n * before creating a new instance.\n *\n * @param status, one of allowed status strings\n */\nclass Status private (private val status: String) extends AnyVal {\n  override def toString = status\n}\n\nprotected[core] object Status extends ArgNormalizer[Status] {\n  val ACTIVE = new Status(\"active\")\n  val INACTIVE = new Status(\"inactive\")\n\n  protected[core] def next(status: Status): Status = {\n    status match {\n      case ACTIVE   => INACTIVE\n      case INACTIVE => ACTIVE\n    }\n  }\n\n  /**\n   * Creates a rule Status from a string.\n   *\n   * @param str the rule status as string\n   * @return Status instance\n   * @throws IllegalArgumentException is argument is undefined or not a valid status\n   */\n  @throws[IllegalArgumentException]\n  override protected[entity] def factory(str: String): Status = {\n    val status = new Status(str)\n    require(status == ACTIVE || status == INACTIVE, s\"$str is not a recognized rule state\")\n    status\n  }\n\n  override protected[core] implicit val serdes = new RootJsonFormat[Status] {\n    def write(s: Status) = JsString(s.status)\n\n    def read(value: JsValue) =\n      Try {\n        val JsString(v) = value\n        Status(v)\n      } match {\n        case Success(s) => s\n        case Failure(t) => deserializationError(t.getMessage)\n      }\n  }\n\n  /**\n   * A serializer for status POST entities. This is a restricted\n   * Status view with only ACTIVE and INACTIVE values allowed.\n   */\n  protected[core] val serdesRestricted = new RootJsonFormat[Status] {\n    def write(s: Status) = JsObject(\"status\" -> JsString(s.status))\n\n    def read(value: JsValue) =\n      Try {\n        val JsObject(fields) = value\n        val JsString(s) = fields(\"status\")\n        Status(s)\n      } match {\n        case Success(status) =>\n          if (status == ACTIVE || status == INACTIVE) {\n            status\n          } else {\n            val msg =\n              s\"\"\"'$status' is not a recognized rule state, must be one of ['${Status.ACTIVE}', '${Status.INACTIVE}']\"\"\"\n            deserializationError(msg)\n          }\n        case Failure(t) => deserializationError(t.getMessage)\n      }\n  }\n}\n\nobject WhiskRule extends DocumentFactory[WhiskRule] with WhiskEntityQueries[WhiskRule] with DefaultJsonProtocol {\n  import WhiskActivation.instantSerdes\n\n  override val collectionName = \"rules\"\n\n  private implicit val fqnSerdes = FullyQualifiedEntityName.serdes\n  private val caseClassSerdes = jsonFormat8(WhiskRule.apply)\n\n  override implicit val serdes = new RootJsonFormat[WhiskRule] {\n    def write(r: WhiskRule) = caseClassSerdes.write(r)\n\n    def read(value: JsValue) =\n      Try {\n        caseClassSerdes.read(value)\n      } recover {\n        case DeserializationException(_, _, List(\"trigger\")) | DeserializationException(_, _, List(\"action\")) =>\n          val namespace = value.asJsObject.fields(\"namespace\").convertTo[EntityPath]\n          val actionName = value.asJsObject.fields(\"action\")\n          val triggerName = value.asJsObject.fields(\"trigger\")\n\n          val refs = Seq(actionName, triggerName).map { name =>\n            Try {\n              FullyQualifiedEntityName(namespace, EntityName.serdes.read(name))\n            } match {\n              case Success(n) => n\n              case Failure(t) => deserializationError(t.getMessage)\n            }\n          }\n          val fields = value.asJsObject.fields + (\"action\" -> refs(0).toDocId.toJson) + (\"trigger\" -> refs(1).toDocId.toJson)\n          caseClassSerdes.read(JsObject(fields))\n      } match {\n        case Success(r) => r\n        case Failure(t) => deserializationError(t.getMessage)\n      }\n  }\n\n  override val cacheEnabled = false\n}\n\nobject WhiskRuleResponse extends DefaultJsonProtocol {\n  import WhiskActivation.instantSerdes\n  private implicit val fqnSerdes = FullyQualifiedEntityName.serdes\n  implicit val serdes = jsonFormat9(WhiskRuleResponse.apply)\n}\n\nobject WhiskRulePut extends DefaultJsonProtocol {\n  private implicit val fqnSerdes = FullyQualifiedEntityName.serdes\n  implicit val serdes = jsonFormat5(WhiskRulePut.apply)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.time.Instant\n\nimport scala.concurrent.Future\nimport scala.language.postfixOps\nimport scala.util.Try\nimport org.apache.pekko.actor.ActorSystem\nimport spray.json.JsNumber\nimport spray.json.JsObject\nimport spray.json.JsString\nimport spray.json.RootJsonFormat\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.ArtifactStore\nimport org.apache.openwhisk.core.database.ArtifactStoreProvider\nimport org.apache.openwhisk.core.database.DocumentRevisionProvider\nimport org.apache.openwhisk.core.database.DocumentSerializer\nimport org.apache.openwhisk.core.database.StaleParameter\nimport org.apache.openwhisk.spi.SpiLoader\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport scala.reflect.classTag\n\nobject types {\n  type AuthStore = ArtifactStore[WhiskAuth]\n  type EntityStore = ArtifactStore[WhiskEntity]\n}\n\ncase class DBConfig(subjectsDdoc: String, actionsDdoc: String, activationsDdoc: String, activationsFilterDdoc: String)\n\nprotected[core] trait WhiskDocument extends DocumentSerializer with DocumentRevisionProvider {\n\n  /**\n   * Gets unique document identifier for the document.\n   */\n  protected def docid: DocId\n\n  /**\n   * Creates DocId from the unique document identifier and the\n   * document revision if one exists.\n   */\n  protected[core] final def docinfo: DocInfo = DocInfo(docid, rev)\n\n  /**\n   * The representation as JSON, e.g. for REST calls. Does not include id/rev.\n   */\n  def toJson: JsObject\n\n  /**\n   * Database JSON representation. Includes id/rev when appropriate. May\n   * differ from `toJson` in exceptional cases.\n   */\n  override def toDocumentRecord: JsObject = {\n    val id = docid.id\n    val revOrNull = rev.rev\n\n    // Building up the fields.\n    val base = this.toJson.fields\n    val withId = base + (\"_id\" -> JsString(id))\n    val withRev = if (revOrNull == null) withId else { withId + (\"_rev\" -> JsString(revOrNull)) }\n    JsObject(withRev)\n  }\n}\n\nobject WhiskAuthStore {\n  implicit val docReader = WhiskDocumentReader\n\n  def datastore()(implicit system: ActorSystem, logging: Logging) =\n    SpiLoader.get[ArtifactStoreProvider].makeStore[WhiskAuth]()\n}\n\nobject WhiskEntityStore {\n\n  def datastore()(implicit system: ActorSystem, logging: Logging) =\n    SpiLoader\n      .get[ArtifactStoreProvider]\n      .makeStore[WhiskEntity]()(classTag[WhiskEntity], WhiskEntityJsonFormat, WhiskDocumentReader, system, logging)\n}\n\nobject WhiskActivationStore {\n  implicit val docReader = WhiskDocumentReader\n\n  def datastore()(implicit system: ActorSystem, logging: Logging) =\n    SpiLoader.get[ArtifactStoreProvider].makeStore[WhiskActivation](useBatching = true)\n}\n\n/**\n * A class to type the design doc and view within a database.\n *\n * @param ddoc the design document\n * @param view the view name within the design doc\n */\nprotected[core] case class View(ddoc: String, view: String) {\n\n  /** The name of the table to query. */\n  val name = s\"$ddoc/$view\"\n}\n\n/**\n * This object provides some utilities that query the whisk datastore.\n * The datastore is assumed to have views (pre-computed joins or indexes)\n * for each of the whisk collection types. Entities may be queries by\n * [path, date] where\n *\n * - path is the either root namespace for an entity (the owning subject\n *   or organization) or a packaged qualified namespace,\n * - date is the date the entity was created or last updated, or for activations\n *   this is the start of the activation. See EntityRecord for the last updated\n *   property.\n *\n * This order is important because the datastore is assumed to sort lexicographically\n * and hence either the fields are ordered according to the set of queries that are\n * desired: all entities in a namespace (by type), further refined by date, further\n * refined by name.\n *\n */\nobject WhiskQueries {\n  val TOP = \"\\ufff0\"\n\n  /** The view name for the collection, within the design document. */\n  def view(ddoc: String, collection: String) = new View(ddoc, collection)\n\n  /** The view name for the collection, within the design document. */\n  def entitiesView(collection: String) = new View(entitiesDesignDoc, collection)\n\n  /** The db configuration. */\n  protected[entity] val dbConfig = loadConfigOrThrow[DBConfig](ConfigKeys.db)\n\n  /** The design document to use for queries. */\n  private val entitiesDesignDoc = dbConfig.actionsDdoc\n}\n\ntrait WhiskEntityQueries[T] {\n  val collectionName: String\n  val serdes: RootJsonFormat[T]\n  import WhiskQueries._\n\n  /** The view name for the collection, within the design document. */\n  lazy val view: View = WhiskQueries.entitiesView(collection = collectionName)\n\n  /**\n   * Queries the datastore for records from a specific collection (i.e., type) matching\n   * the given path (which should be one namespace, or namespace + package name).\n   *\n   * @return list of records as JSON object if docs parameter is false, as Left\n   *         and a list of the records as their type T if including the full record, as Right\n   */\n  def listCollectionInNamespace[A <: WhiskEntity](\n    db: ArtifactStore[A],\n    path: EntityPath, // could be a namespace or namespace + package name\n    skip: Int,\n    limit: Int,\n    includeDocs: Boolean = false,\n    since: Option[Instant] = None,\n    upto: Option[Instant] = None,\n    stale: StaleParameter = StaleParameter.No,\n    viewName: View = view)(implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {\n    val convert = if (includeDocs) Some((o: JsObject) => Try { serdes.read(o) }) else None\n    val startKey = List(path.asString, since map { _.toEpochMilli } getOrElse 0)\n    val endKey = List(path.asString, upto map { _.toEpochMilli } getOrElse TOP, TOP)\n    query(db, viewName, startKey, endKey, skip, limit, reduce = false, stale, convert)\n  }\n\n  /**\n   * Queries the datastore for the records count in a specific collection (i.e., type) matching\n   * the given path (which should be one namespace, or namespace + package name).\n   *\n   * @return JSON object with a single key, the collection name, and a value equal to the view length\n   */\n  def countCollectionInNamespace[A <: WhiskEntity](\n    db: ArtifactStore[A],\n    path: EntityPath, // could be a namespace or namespace + package name\n    skip: Int,\n    since: Option[Instant] = None,\n    upto: Option[Instant] = None,\n    stale: StaleParameter = StaleParameter.No,\n    viewName: View = view)(implicit transid: TransactionId): Future[JsObject] = {\n    implicit val ec = db.executionContext\n    val startKey = List(path.asString, since map { _.toEpochMilli } getOrElse 0)\n    val endKey = List(path.asString, upto map { _.toEpochMilli } getOrElse TOP, TOP)\n    db.count(viewName.name, startKey, endKey, skip, stale) map { count =>\n      JsObject(collectionName -> JsNumber(count))\n    }\n  }\n\n  protected[entity] def query[A <: WhiskEntity](\n    db: ArtifactStore[A],\n    view: View,\n    startKey: List[Any],\n    endKey: List[Any],\n    skip: Int,\n    limit: Int,\n    reduce: Boolean,\n    stale: StaleParameter = StaleParameter.No,\n    convert: Option[JsObject => Try[T]])(implicit transid: TransactionId): Future[Either[List[JsObject], List[T]]] = {\n    implicit val ec = db.executionContext\n    val includeDocs = convert.isDefined\n    db.query(view.name, startKey, endKey, skip, limit, includeDocs, descending = true, reduce, stale) map { rows =>\n      convert map { fn =>\n        Right(rows flatMap { row =>\n          fn(row.fields(\"doc\").asJsObject) toOption\n        })\n      } getOrElse {\n        Left(rows flatMap { normalizeRow(_, reduce) toOption })\n      }\n    }\n  }\n\n  /**\n   * Normalizes the raw JsObject response from the datastore since the\n   * response differs in the case of a reduction.\n   */\n  protected def normalizeRow(row: JsObject, reduce: Boolean) = Try {\n    if (!reduce) {\n      row.fields(\"value\").asJsObject\n    } else row\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskTrigger.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity\n\nimport java.time.Instant\n\nimport spray.json.DefaultJsonProtocol\nimport org.apache.openwhisk.core.database.DocumentFactory\nimport spray.json._\n\n/**\n * WhiskTriggerPut is a restricted WhiskTrigger view that eschews properties\n * that are auto-assigned or derived from URI: namespace and name.\n */\ncase class WhiskTriggerPut(parameters: Option[Parameters] = None,\n                           limits: Option[TriggerLimits] = None,\n                           version: Option[SemVer] = None,\n                           publish: Option[Boolean] = None,\n                           annotations: Option[Parameters] = None)\n\n/**\n * Representation of a rule to be stored inside a trigger. Contains all\n * information needed to be able to determine if and which action is to\n * be fired.\n *\n * @param action the fully qualified name of the action to be fired\n * @param status status of the rule\n */\ncase class ReducedRule(action: FullyQualifiedEntityName, status: Status)\n\n/**\n * A WhiskTrigger provides an abstraction of the meta-data\n * for a whisk trigger.\n *\n * The WhiskTrigger object is used as a helper to adapt objects between\n * the schema used by the database and the WhiskTrigger abstraction.\n *\n * @param namespace the namespace for the trigger\n * @param name the name of the trigger\n * @param parameters the set of parameters to bind to the trigger environment\n * @param limits the limits to impose on the trigger\n * @param version the semantic version\n * @param publish true to share the action or false otherwise\n * @param annotations the set of annotations to attribute to the trigger\n * @param rules the map of the rules that are associated with this trigger. Key is the rulename and value is the ReducedRule\n * @param updated the timestamp when the trigger is updated\n * @throws IllegalArgumentException if any argument is undefined\n */\n@throws[IllegalArgumentException]\ncase class WhiskTrigger(namespace: EntityPath,\n                        override val name: EntityName,\n                        parameters: Parameters = Parameters(),\n                        limits: TriggerLimits = TriggerLimits(),\n                        version: SemVer = SemVer(),\n                        publish: Boolean = false,\n                        annotations: Parameters = Parameters(),\n                        rules: Option[Map[FullyQualifiedEntityName, ReducedRule]] = None,\n                        override val updated: Instant = WhiskEntity.currentMillis())\n    extends WhiskEntity(name, \"trigger\") {\n\n  require(limits != null, \"limits undefined\")\n\n  def toJson = WhiskTrigger.serdes.write(this).asJsObject\n\n  def withoutRules = copy(rules = None).revision[WhiskTrigger](rev)\n\n  /**\n   * Inserts the rulename, its status and the action to be fired into the trigger.\n   *\n   * @param rulename The fully qualified name of the rule, that will be fired by this trigger.\n   * @param rule The rule, that will be fired by this trigger. It's from type ReducedRule. This type\n   * contains the fully qualified name of the action to be fired by the rule and the status of the rule.\n   */\n  def addRule(rulename: FullyQualifiedEntityName, rule: ReducedRule) = {\n    val entry = rulename -> rule\n    val links = rules getOrElse Map.empty[FullyQualifiedEntityName, ReducedRule]\n    copy(rules = Some(links + entry)).revision[WhiskTrigger](docinfo.rev)\n  }\n\n  /**\n   * Removes the rule from the trigger.\n   *\n   * @param rule The fully qualified name of the rule, that should be removed from the\n   * trigger. After removing the rule, it won't be fired anymore by this trigger.\n   */\n  def removeRule(rule: FullyQualifiedEntityName) = {\n    copy(rules = rules.map(_ - rule)).revision[WhiskTrigger](docinfo.rev)\n  }\n}\n\nobject ReducedRule extends DefaultJsonProtocol {\n  private implicit val fqnSerdes = FullyQualifiedEntityName.serdes\n  implicit val serdes = jsonFormat2(ReducedRule.apply)\n}\n\nobject WhiskTrigger\n    extends DocumentFactory[WhiskTrigger]\n    with WhiskEntityQueries[WhiskTrigger]\n    with DefaultJsonProtocol {\n  import WhiskActivation.instantSerdes\n\n  override val collectionName = \"triggers\"\n\n  private implicit val fqnSerdesAsDocId = FullyQualifiedEntityName.serdesAsDocId\n  override implicit val serdes = jsonFormat9(WhiskTrigger.apply)\n\n  override val cacheEnabled = true\n}\n\nobject WhiskTriggerPut extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat5(WhiskTriggerPut.apply)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/etcd/EtcdClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.etcd\n\nimport com.google.common.util.concurrent.{FutureCallback, Futures, ListenableFuture}\nimport com.ibm.etcd.api._\nimport com.ibm.etcd.client.kv.KvClient.Watch\nimport com.ibm.etcd.client.kv.{KvClient, WatchUpdate}\nimport com.ibm.etcd.client.{EtcdClient => Client}\nimport io.grpc.stub.StreamObserver\nimport java.util.concurrent.Executors\n\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.etcd.EtcdType._\nimport org.apache.openwhisk.core.service.Lease\nimport pureconfig.loadConfigOrThrow\nimport spray.json.DefaultJsonProtocol\n\nimport scala.language.implicitConversions\nimport scala.annotation.tailrec\nimport scala.concurrent.{ExecutionContextExecutor, Future, Promise}\n\nobject RichListenableFuture {\n  implicit def convertToFuture[T](lf: ListenableFuture[T])(implicit ece: ExecutionContextExecutor): Future[T] = {\n    val p = Promise[T]()\n    Futures.addCallback(lf, new FutureCallback[T] {\n      def onFailure(t: Throwable): Unit = p failure t\n      def onSuccess(result: T): Unit = p success result\n    }, ece)\n    p.future\n  }\n}\n\nobject EtcdClient {\n  // hostAndPorts format: {HOST}:{PORT}[,{HOST}:{PORT},{HOST}:{PORT}, ...]\n  def apply(config: EtcdConfig)(implicit ece: ExecutionContextExecutor): EtcdClient = {\n    require(config.hosts != null)\n    require(\n      (config.username.nonEmpty && config.password.nonEmpty) || (config.username.isEmpty && config.password.isEmpty))\n    val clientBuilder = Client.forEndpoints(config.hosts).withPlainText()\n    if (config.username.nonEmpty && config.password.nonEmpty) {\n      new EtcdClient(clientBuilder.withCredentials(config.username.get, config.password.get).build())\n    } else {\n      new EtcdClient(clientBuilder.build())(ece)\n    }\n  }\n\n  def apply(client: Client)(implicit ece: ExecutionContextExecutor): EtcdClient = {\n    new EtcdClient(client)(ece)\n  }\n}\n\nclass EtcdClient(val client: Client)(override implicit val ece: ExecutionContextExecutor)\n    extends EtcdKeyValueApi\n    with EtcdLeaseApi\n    with EtcdWatchApi\n    with EtcdLeadershipApi {\n\n  def close() = {\n    client.close()\n  }\n}\n\ntrait EtcdKeyValueApi extends KeyValueStore {\n  import RichListenableFuture._\n  protected[etcd] val client: Client\n\n  override def get(key: String): Future[RangeResponse] =\n    client.getKvClient.get(key).async()\n\n  override def getPrefix(prefixKey: String): Future[RangeResponse] = {\n    client.getKvClient.get(prefixKey).asPrefix().async()\n  }\n\n  override def getCount(prefixKey: String): Future[Long] = {\n    client.getKvClient.get(prefixKey).asPrefix().countOnly().async().map(_.getCount)\n  }\n\n  override def put(key: String, value: String): Future[PutResponse] =\n    client.getKvClient.put(key, value).async().recoverWith {\n      case t =>\n        Future.failed[PutResponse](getNestedException(t))\n    }\n\n  override def put(key: String, value: String, leaseId: Long): Future[PutResponse] =\n    client.getKvClient\n      .put(key, value, leaseId)\n      .async()\n      .recoverWith {\n        case t =>\n          Future.failed[PutResponse](getNestedException(t))\n      }\n\n  def put(key: String, value: Boolean): Future[PutResponse] = {\n    put(key, value.toString)\n  }\n\n  def put(key: String, value: Boolean, leaseId: Long): Future[PutResponse] = {\n    put(key, value.toString, leaseId)\n  }\n\n  override def del(key: String): Future[DeleteRangeResponse] =\n    client.getKvClient.delete(key).async().recoverWith {\n      case t =>\n        Future.failed[DeleteRangeResponse](getNestedException(t))\n    }\n\n  override def putTxn[T](key: String, value: T, cmpVersion: Long, leaseId: Long): Future[TxnResponse] = {\n    client.getKvClient\n      .txnIf()\n      .cmpEqual(key)\n      .version(cmpVersion)\n      .`then`()\n      .put(client.getKvClient\n        .put(key, value.toString, leaseId)\n        .asRequest())\n      .async()\n      .recoverWith {\n        case t =>\n          Future.failed[TxnResponse](getNestedException(t))\n      }\n  }\n\n  @tailrec\n  private def getNestedException(t: Throwable): Throwable = {\n    if (t.getCause == null) t\n    else getNestedException(t.getCause)\n  }\n}\n\ntrait KeyValueStore {\n\n  implicit val ece: ExecutionContextExecutor\n\n  def get(key: String): Future[RangeResponse]\n\n  def getPrefix(prefixKey: String): Future[RangeResponse]\n\n  def getCount(prefixKey: String): Future[Long]\n\n  def put(key: String, value: String): Future[PutResponse]\n\n  def put(key: String, value: String, leaseId: Long): Future[PutResponse]\n\n  def del(key: String): Future[DeleteRangeResponse]\n\n  def putTxn[T](key: String, value: T, cmpVersion: Long, leaseId: Long): Future[TxnResponse]\n}\n\ntrait EtcdLeaseApi {\n  import RichListenableFuture._\n  implicit val ece: ExecutionContextExecutor\n\n  protected[etcd] val client: Client\n  protected val DEFAULT_TTL = 2\n\n  def grant(ttl: Long = DEFAULT_TTL): Future[LeaseGrantResponse] = {\n    client.getLeaseClient.grant(ttl).async()\n  }\n\n  def revoke(leaseId: Long): Future[LeaseRevokeResponse] = {\n    client.getLeaseClient.revoke(leaseId)\n  }\n\n  def keepAliveOnce(leaseId: Long): Future[LeaseKeepAliveResponse] = {\n    client.getLeaseClient.keepAliveOnce(leaseId)\n  }\n}\n\ntrait EtcdWatchApi {\n  val nThreads = loadConfigOrThrow[Int](ConfigKeys.etcdPoolThreads)\n  val threadpool = Executors.newFixedThreadPool(nThreads);\n  protected[etcd] val client: Client\n\n  def watchAllKeys(next: WatchUpdate => Unit = (_: WatchUpdate) => {},\n                   error: Throwable => Unit = (_: Throwable) => {},\n                   completed: () => Unit = () => {}): Watch = {\n    client.getKvClient\n      .watch(KvClient.ALL_KEYS)\n      .prevKv()\n      .executor(threadpool)\n      .start(new StreamObserver[WatchUpdate]() {\n        override def onNext(value: WatchUpdate): Unit = {\n          next(value)\n        }\n\n        override def onError(t: Throwable): Unit = {\n          error(t)\n        }\n\n        override def onCompleted(): Unit = {\n          completed()\n        }\n      })\n  }\n\n  def watch(key: String, isPrefix: Boolean = false)(next: WatchUpdate => Unit = (_: WatchUpdate) => {},\n                                                    error: Throwable => Unit = (_: Throwable) => {},\n                                                    completed: () => Unit = () => {}): Watch = {\n    val watchRequest = if (isPrefix) {\n      client.getKvClient.watch(key).asPrefix().prevKv()\n    } else {\n      client.getKvClient.watch(key).prevKv()\n    }\n    watchRequest\n      .executor(threadpool)\n      .start(new StreamObserver[WatchUpdate]() {\n        override def onNext(value: WatchUpdate): Unit = {\n          next(value)\n        }\n\n        override def onError(t: Throwable): Unit = {\n          error(t)\n        }\n\n        override def onCompleted(): Unit = {\n          completed()\n        }\n      })\n  }\n\n}\n\ntrait EtcdLeadershipApi extends EtcdKeyValueApi with EtcdLeaseApi with EtcdWatchApi {\n\n  protected[etcd] val client: Client\n\n  val initVersion = 0\n\n  def electLeader(key: String, value: String, timeout: Long = 60): Future[Either[EtcdFollower, EtcdLeader]] =\n    for {\n      leaseResp <- grant(timeout)\n      txnResp <- putTxn(key, value, initVersion, leaseResp.getID)\n      result <- Future {\n        if (txnResp.getSucceeded) {\n          Right(EtcdLeader(key, value, leaseResp.getID))\n        } else {\n          Left(EtcdFollower(key, value))\n        }\n      }\n    } yield result\n\n  def electLeader(key: String, value: String, lease: Lease): Future[Either[EtcdFollower, EtcdLeader]] =\n    for {\n      txnResp <- putTxn(key, value, initVersion, lease.id)\n      result <- Future {\n        if (txnResp.getSucceeded) {\n          Right(EtcdLeader(key, value, lease.id))\n        } else {\n          Left(EtcdFollower(key, value))\n        }\n      }\n    } yield result\n\n  def keepAliveLeader(leaseId: Long): Future[Long] =\n    keepAliveOnce(leaseId).map(res => res.getID)\n\n}\ncase class EtcdLeader(key: String, value: String, leaseId: Long)\n\nobject EtcdLeader extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat3(EtcdLeader.apply)\n}\n\ncase class EtcdFollower(key: String, value: String)\n\nobject EtcdFollower extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat2(EtcdFollower.apply)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/etcd/EtcdUtils.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.etcd\n\nimport java.nio.charset.StandardCharsets\n\nimport com.google.protobuf.ByteString\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.ContainerId\nimport org.apache.openwhisk.core.entity.SizeUnits.MB\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport pureconfig.loadConfigOrThrow\n\nimport scala.language.implicitConversions\nimport scala.util.Try\n\ncase class EtcdConfig(hosts: String, username: Option[String], password: Option[String])\n\ncase class EtcdException(msg: String) extends Exception(msg)\n\n/**\n * If you import the line below, it implicitly converts ByteString type to Scala Type.\n *\n * import org.apache.openwhisk.core.etcd.EtcdType._\n */\nobject EtcdType {\n\n  implicit def stringToByteString(str: String): ByteString = ByteString.copyFromUtf8(str)\n\n  implicit def ByteStringToString(byteString: ByteString): String = byteString.toString(StandardCharsets.UTF_8)\n\n  implicit def ByteStringToInt(byteString: ByteString): Int = byteString.toString(StandardCharsets.UTF_8).toInt\n\n  implicit def IntToByteString(int: Int): ByteString = ByteString.copyFromUtf8(int.toString)\n\n  implicit def ByteStringToLong(byteString: ByteString): Long = byteString.toString(StandardCharsets.UTF_8).toLong\n\n  implicit def LongToByteString(long: Long): ByteString = ByteString.copyFromUtf8(long.toString)\n\n  implicit def ByteStringToBoolean(byteString: ByteString): Boolean =\n    byteString.toString(StandardCharsets.UTF_8).toBoolean\n\n  implicit def BooleanToByteString(bool: Boolean): ByteString = ByteString.copyFromUtf8(bool.toString)\n\n  implicit def ByteStringToByteSize(byteString: ByteString): ByteSize =\n    ByteSize(byteString.toString(StandardCharsets.UTF_8).toLong, MB)\n\n  implicit def ByteSizeToByteString(byteSize: ByteSize): ByteString = ByteString.copyFromUtf8(byteSize.toMB.toString)\n\n}\n\nobject EtcdKV {\n\n  val TOP = \"\\ufff0\"\n\n  val clusterName = loadConfigOrThrow[String](ConfigKeys.whiskClusterName)\n\n  object SchedulerKeys {\n    val prefix = s\"$clusterName/scheduler\"\n\n    val scheduler = s\"$prefix\"\n\n    /**\n     *  The keys for states of schedulers\n     */\n    def scheduler(instanceId: SchedulerInstanceId) = s\"$prefix/${instanceId.asString}\"\n\n  }\n\n  object QueueKeys {\n\n    val inProgressPrefix = s\"$clusterName/in-progress\"\n    val queuePrefix = s\"$clusterName/queue\"\n\n    /**\n     * The keys for in-progress queue\n     */\n    def inProgressQueue(invocationNamespace: String, fqn: FullyQualifiedEntityName) =\n      s\"$inProgressPrefix/queue/$invocationNamespace/${fqn.copy(version = None)}\"\n\n    /**\n     * The prefix key for state in the queue\n     */\n    def queuePrefix(invocationNamespace: String, fqn: FullyQualifiedEntityName): String =\n      s\"$queuePrefix/$invocationNamespace/${fqn.copy(version = None)}\"\n\n    /**\n     * The keys for state in the queue\n     *\n     * Example\n     *  - queue/invocationNs/ns/pkg/act/leader\n     *  - queue/invocationNs/ns/pkg/act/follower/scheduler1\n     *  - queue/invocationNs/ns/pkg/act/follower/scheduler2\n     *\n     */\n    def queue(invocationNamespace: String,\n              fqn: FullyQualifiedEntityName,\n              leader: Boolean,\n              schedulerInstanceId: Option[SchedulerInstanceId] = None): String = {\n      require(leader || (!leader && schedulerInstanceId.isDefined))\n      val prefix = s\"$queuePrefix/$invocationNamespace/${fqn.copy(version = None)}\"\n      if (leader)\n        s\"$prefix/leader\"\n      else\n        s\"$prefix/follower/${schedulerInstanceId.get.asString}\"\n    }\n  }\n\n  object ThrottlingKeys {\n    val prefix = s\"$clusterName/throttling\"\n\n    /**\n     *  The keys for namespace throttling\n     */\n    def namespace(namespace: EntityName) = s\"$prefix/$namespace\"\n\n    /**\n     *  The keys for action throttling\n     */\n    def action(invocationNamespace: String, fqn: FullyQualifiedEntityName) =\n      s\"$prefix/$invocationNamespace/${fqn.copy(version = None)}\"\n\n    /**\n     *  The keys for action throttling\n     */\n    def action(invocationNamespace: String, fqn: String) = s\"$prefix/$invocationNamespace/$fqn\"\n\n  }\n\n  object ContainerKeys {\n    val namespacePrefix = s\"$clusterName/namespace\"\n    val inProgressPrefix = s\"$clusterName/in-progress\"\n    val warmedPrefix = s\"$clusterName/warmed\"\n\n    /**\n     * The keys for the number of container\n     */\n    def containerPrefix(containerType: String,\n                        invocationNamespace: String,\n                        fqn: FullyQualifiedEntityName,\n                        revision: Option[DocRevision] = None): String =\n      s\"$containerType/$invocationNamespace/${fqn.copy(version = None)}/${revision.map(r => s\"$r/\").getOrElse(\"\")}\"\n\n    /**\n     * The keys for in-progress container\n     *\n     * For count queries, fqn must be at the front.\n     */\n    def inProgressContainer(invocationNamespace: String,\n                            fqn: FullyQualifiedEntityName,\n                            revision: DocRevision,\n                            sid: SchedulerInstanceId,\n                            cid: CreationId): String =\n      s\"${containerPrefix(inProgressPrefix, invocationNamespace, fqn, Some(revision))}scheduler/${sid.asString}/creationId/$cid\"\n\n    /**\n     * The keys for the number of warmed container\n     */\n    def warmedContainers(invocationNamespace: String,\n                         fqn: FullyQualifiedEntityName,\n                         revision: DocRevision,\n                         invokerInstanceId: InvokerInstanceId,\n                         containerId: ContainerId): String =\n      s\"${containerPrefix(warmedPrefix, invocationNamespace, fqn, Some(revision))}invoker/${invokerInstanceId.instance}/container/${containerId.asString}\"\n\n    /**\n     * The keys for the number of existing container\n     */\n    def existingContainers(invocationNamespace: String,\n                           fqn: FullyQualifiedEntityName,\n                           revision: DocRevision,\n                           invoker: Option[InvokerInstanceId] = None,\n                           containerId: Option[ContainerId] = None): String =\n      containerPrefix(namespacePrefix, invocationNamespace, fqn, Some(revision)) + invoker\n        .map(id => s\"invoker${id.toInt}/\")\n        .getOrElse(\"\") + containerId\n        .map(id => s\"container/${id.asString}\")\n        .getOrElse(\"\")\n\n    /**\n     * The keys for the number of in-progress container by namespace\n     */\n    def inProgressContainerPrefixByNamespace(invocationNamespace: String): String =\n      s\"$inProgressPrefix/$invocationNamespace/\"\n\n    /**\n     * The keys for the number of existing container by namespace\n     */\n    def existingContainersPrefixByNamespace(invocationNamespace: String): String =\n      s\"$namespacePrefix/$invocationNamespace/\"\n\n  }\n\n  object InvokerKeys {\n    val prefix = s\"$clusterName/invokers\"\n\n    /**\n     * If displayName only exists in the etcd key, we cannot differentiate it with the uniqueName\n     */\n    def health(invokerInstanceId: InvokerInstanceId) = {\n      (invokerInstanceId.uniqueName, invokerInstanceId.displayedName) match {\n        case (Some(uniqueName), Some(displayName)) => s\"$prefix/${invokerInstanceId.toInt}/$uniqueName/$displayName\"\n        case (Some(uniqueName), None)              => s\"$prefix/${invokerInstanceId.toInt}/$uniqueName\"\n        case _                                     => s\"$prefix/${invokerInstanceId.toInt}\"\n      }\n    }\n\n    // id is not supposed to be -1\n    private def getId(id: String): Int = {\n      Try { id.toInt } getOrElse (-1)\n    }\n\n    def getInstanceId(invokerKey: String): InvokerInstanceId = {\n      val constructs = invokerKey.split(\"\\\\b/+\")\n      constructs match {\n        case Array(_, _, id, uniqueName, displayName) =>\n          InvokerInstanceId(getId(id), Some(uniqueName), Some(displayName), 0.B)\n        case Array(_, _, id, uniqueName) =>\n          InvokerInstanceId(getId(id), Some(uniqueName), userMemory = 0.B)\n        case Array(_, _, id) =>\n          InvokerInstanceId(getId(id), userMemory = 0.B)\n      }\n    }\n  }\n\n  object InstanceKeys {\n\n    val instancePrefix = s\"$clusterName/instance\"\n\n    def instanceLease(instanceId: InstanceId): String =\n      s\"$instancePrefix/$instanceId/lease\"\n  }\n\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/etcd/EtcdWorker.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.etcd\n\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorSystem, Props, Timers}\nimport io.grpc.StatusRuntimeException\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.etcd.EtcdWorker.GetLeaseAndRetry\nimport org.apache.openwhisk.core.service.DataManagementService.retryInterval\nimport org.apache.openwhisk.core.service.{\n  AlreadyExist,\n  Done,\n  ElectLeader,\n  ElectionResult,\n  FinishWork,\n  GetLease,\n  InitialDataStorageResults,\n  Lease,\n  RegisterData,\n  RegisterInitialData,\n  WatcherClosed\n}\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.duration.FiniteDuration\nimport scala.util.Success\n\nclass EtcdWorker(etcdClient: EtcdClient, leaseService: ActorRef)(implicit val ec: ExecutionContext,\n                                                                 actorSystem: ActorSystem,\n                                                                 logging: Logging)\n    extends Actor\n    with Timers {\n\n  private val dataManagementService = context.parent\n  private var lease: Option[Lease] = None\n  leaseService ! GetLease\n\n  override def receive: Receive = {\n    case msg: Lease =>\n      lease = Some(msg)\n    case msg: GetLeaseAndRetry =>\n      logging.warn(this, msg.log)\n      if (!msg.skipLeaseRefresh) {\n        if (msg.clearLease) {\n          lease = None\n        }\n        leaseService ! GetLease\n      }\n      sendMessageToSelfAfter(msg.request, retryInterval)\n    // leader election + endpoint management\n    case request: ElectLeader =>\n      lease match {\n        case Some(l) =>\n          etcdClient\n            .electLeader(request.key, request.value, l)\n            .andThen {\n              case Success(msg) =>\n                request.recipient ! ElectionResult(msg)\n                dataManagementService ! FinishWork(request.key)\n            }\n            .recover {\n              // if there is no lease, reissue it and retry immediately\n              case t: StatusRuntimeException =>\n                self ! GetLeaseAndRetry(request, s\"a lease is expired while leader election, reissue it: $t\")\n              // it should retry forever until the data is stored\n              case t: Throwable =>\n                self ! GetLeaseAndRetry(\n                  request,\n                  s\"unexpected error happened: $t, retry storing data\",\n                  skipLeaseRefresh = true)\n            }\n        case None =>\n          self ! GetLeaseAndRetry(request, s\"lease not found, retry storing data ${request.key}\", clearLease = false)\n      }\n\n    // only endpoint management\n    case request: RegisterData =>\n      lease match {\n        case Some(l) =>\n          etcdClient\n            .put(request.key, request.value, l.id)\n            .andThen {\n              case Success(_) =>\n                dataManagementService ! FinishWork(request.key)\n            }\n            .recover {\n              // if there is no lease, reissue it and retry immediately\n              case t: StatusRuntimeException =>\n                self ! GetLeaseAndRetry(\n                  request,\n                  s\"a lease is expired while registering data ${request.key}, reissue it: $t\")\n              // it should retry forever until the data is stored\n              case t: Throwable =>\n                self ! GetLeaseAndRetry(\n                  request,\n                  s\"unexpected error happened: $t, retry storing data ${request.key}\",\n                  skipLeaseRefresh = true)\n            }\n        case None =>\n          self ! GetLeaseAndRetry(request, s\"lease not found, retry storing data ${request.key}\", clearLease = false)\n      }\n    // it stores the data iif there is no such one\n    case request: RegisterInitialData =>\n      lease match {\n        case Some(l) =>\n          etcdClient\n            .putTxn(request.key, request.value, 0, l.id)\n            .map { res =>\n              dataManagementService ! FinishWork(request.key)\n              if (res.getSucceeded) {\n                logging.info(this, s\"initial data storing succeeds for ${request.key}\")\n                request.recipient.map(_ ! InitialDataStorageResults(request.key, Right(Done())))\n              } else {\n                logging.info(this, s\"data is already stored for: $request, cancel the initial data storing\")\n                request.recipient.map(_ ! InitialDataStorageResults(request.key, Left(AlreadyExist())))\n              }\n            }\n            .recover {\n              // if there is no lease, reissue it and retry immediately\n              case t: StatusRuntimeException =>\n                self ! GetLeaseAndRetry(\n                  request,\n                  s\"a lease is expired while registering an initial data ${request.key}, reissue it: $t\")\n              // it should retry forever until the data is stored\n              case t: Throwable =>\n                self ! GetLeaseAndRetry(\n                  request,\n                  s\"unexpected error happened: $t, retry storing data ${request.key}\",\n                  skipLeaseRefresh = true)\n            }\n        case None =>\n          self ! GetLeaseAndRetry(request, s\"lease not found, retry storing data ${request.key}\", clearLease = false)\n      }\n\n    case msg: WatcherClosed =>\n      etcdClient\n        .del(msg.key)\n        .andThen {\n          case Success(_) =>\n            dataManagementService ! FinishWork(msg.key)\n        }\n        .recover {\n          // if there is no lease, reissue it and retry immediately\n          case t: StatusRuntimeException =>\n            self ! GetLeaseAndRetry(msg, s\"a lease is expired while deleting data ${msg.key}, reissue it: $t\")\n          // it should retry forever until the data is stored\n          case t: Throwable =>\n            self ! GetLeaseAndRetry(\n              msg,\n              s\"unexpected error happened: $t, retry storing data for ${msg.key}\",\n              skipLeaseRefresh = true)\n        }\n  }\n\n  private def sendMessageToSelfAfter(msg: Any, retryInterval: FiniteDuration) = {\n    timers.startSingleTimer(msg, msg, retryInterval)\n  }\n}\n\nobject EtcdWorker {\n  case class GetLeaseAndRetry(request: Any, log: String, clearLease: Boolean = true, skipLeaseRefresh: Boolean = false)\n\n  def props(etcdClient: EtcdClient, leaseService: ActorRef)(implicit ec: ExecutionContext,\n                                                            actorSystem: ActorSystem,\n                                                            logging: Logging): Props = {\n    Props(new EtcdWorker(etcdClient, leaseService))\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/service/DataManagementService.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.service\n\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, ActorSystem, Props}\nimport org.apache.pekko.util.Timeout\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.etcd.{EtcdFollower, EtcdLeader}\nimport pureconfig.loadConfigOrThrow\n\nimport scala.collection.concurrent.TrieMap\nimport scala.collection.mutable.{Map, Queue}\nimport scala.concurrent.duration._\n\n// messages received by the actor\n// it is required to specify a recipient directly for the retryable message processing\ncase class ElectLeader(key: String, value: String, recipient: ActorRef, watchEnabled: Boolean = true)\ncase class RegisterInitialData(key: String,\n                               value: String,\n                               failoverEnabled: Boolean = true,\n                               recipient: Option[ActorRef] = None)\n\ncase class RegisterData(key: String, value: String, failoverEnabled: Boolean = true)\ncase class UnregisterData(key: String)\ncase class UpdateDataOnChange(key: String, value: String)\n\n// messages sent by the actor\ncase class ElectionResult(leadership: Either[EtcdFollower, EtcdLeader])\ncase class FinishWork(key: String)\ncase class InitialDataStorageResults(key: String, result: Either[AlreadyExist, Done])\ncase class Done()\ncase class AlreadyExist()\n\n/**\n * This service is in charge of storing given data to ETCD.\n * In the event any issue occurs while storing data, the actor keeps trying until the data is stored guaranteeing delivery to ETCD.\n * So it guarantees the data is eventually stored.\n */\nclass DataManagementService(watcherService: ActorRef, workerFactory: ActorRefFactory => ActorRef)(\n  implicit logging: Logging,\n  actorSystem: ActorSystem)\n    extends Actor {\n  private implicit val ec = context.dispatcher\n\n  implicit val requestTimeout: Timeout = Timeout(5.seconds)\n  private[service] val dataCache = TrieMap[String, String]()\n  private val operations = Map.empty[String, Queue[Any]]\n  private var inProgressKeys = Set.empty[String]\n  private val watcherName = \"data-management-service\"\n\n  private val worker = workerFactory(context)\n\n  override def receive: Receive = {\n    case FinishWork(key) =>\n      // send waiting operation to worker if there is any, else update the inProgressKeys\n      val ops = operations.get(key)\n      if (ops.nonEmpty && ops.get.nonEmpty) {\n        val operation = ops.get.dequeue()\n        worker ! operation\n      } else {\n        inProgressKeys = inProgressKeys - key\n        operations.remove(key) // remove empty queue from the map to free memory\n      }\n\n    // normally these messages will be sent when queues are created.\n    case request: ElectLeader =>\n      if (inProgressKeys.contains(request.key)) {\n        logging.info(this, s\"save a request $request into a buffer\")\n        operations.getOrElseUpdate(request.key, Queue.empty[Any]).enqueue(request)\n      } else {\n        worker ! request\n        inProgressKeys = inProgressKeys + request.key\n      }\n\n    case request: RegisterInitialData =>\n      // send WatchEndpoint first as the put operation will be retried until success if failed\n      if (request.failoverEnabled)\n        watcherService ! WatchEndpoint(request.key, request.value, isPrefix = false, watcherName, Set(DeleteEvent))\n      if (inProgressKeys.contains(request.key)) {\n        logging.info(this, s\"save request $request into a buffer\")\n        operations.getOrElseUpdate(request.key, Queue.empty[Any]).enqueue(request)\n      } else {\n        worker ! request\n        inProgressKeys = inProgressKeys + request.key\n      }\n\n    case request: RegisterData =>\n      // send WatchEndpoint first as the put operation will be retried until success if failed\n      if (request.failoverEnabled)\n        watcherService ! WatchEndpoint(request.key, request.value, isPrefix = false, watcherName, Set(DeleteEvent))\n      if (inProgressKeys.contains(request.key)) {\n        // the new put|delete operation will erase influences made by older operations like put&delete\n        // so we can remove these old operations\n        logging.info(this, s\"save request $request into a buffer\")\n        val queue = operations.getOrElseUpdate(request.key, Queue.empty[Any]).filter { value =>\n          value match {\n            case _: RegisterData | _: WatcherClosed | _: RegisterInitialData => false\n            case _                                                           => true\n          }\n        }\n        queue.enqueue(request)\n        operations.update(request.key, queue)\n      } else {\n        worker ! request\n        inProgressKeys = inProgressKeys + request.key\n      }\n\n    case request: WatcherClosed =>\n      if (inProgressKeys.contains(request.key)) {\n        // The put|delete operations against the same key will overwrite the previous results.\n        // For example, if we put a value, delete it and put a new value again, the final result will be the new value.\n        // So we can remove these old operations\n        logging.info(this, s\"save request $request into a buffer\")\n        val queue = operations.getOrElseUpdate(request.key, Queue.empty[Any]).filter { value =>\n          value match {\n            case _: RegisterData | _: WatcherClosed | _: RegisterInitialData => false\n            case _                                                           => true\n          }\n        }\n        queue.enqueue(request)\n        operations.update(request.key, queue)\n      } else {\n        worker ! request\n        inProgressKeys = inProgressKeys + request.key\n      }\n\n    // It is required to close the watcher first before deleting etcd data\n    // It is supposed to receive the WatcherClosed message after the watcher is stopped.\n    case msg: UnregisterData =>\n      watcherService ! UnwatchEndpoint(msg.key, isPrefix = false, watcherName, needFeedback = true)\n\n    case WatchEndpointRemoved(_, key, value, false) =>\n      self ! RegisterInitialData(key, value, failoverEnabled = false) // the watcher is already setup\n\n    // It should not receive \"prefixed\" data\n    case WatchEndpointRemoved(_, key, value, true) =>\n      logging.error(this, s\"unexpected data received: ${WatchEndpoint(key, value, isPrefix = true, watcherName)}\")\n\n    case msg: UpdateDataOnChange =>\n      dataCache.get(msg.key) match {\n        case Some(cached) if cached == msg.value =>\n          logging.debug(this, s\"skip publishing data ${msg.key} because the data is not changed.\")\n\n        case Some(cached) if cached != msg.value =>\n          dataCache.update(msg.key, msg.value)\n          self ! RegisterData(msg.key, msg.value, failoverEnabled = false) // the watcher is already setup\n\n        case None =>\n          dataCache.put(msg.key, msg.value)\n          self ! RegisterData(msg.key, msg.value)\n\n      }\n  }\n}\n\nobject DataManagementService {\n  val retryInterval: FiniteDuration = loadConfigOrThrow[FiniteDuration](ConfigKeys.dataManagementServiceRetryInterval)\n\n  def props(watcherService: ActorRef, workerFactory: ActorRefFactory => ActorRef)(implicit logging: Logging,\n                                                                                  actorSystem: ActorSystem): Props = {\n    Props(new DataManagementService(watcherService, workerFactory))\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/service/LeaseKeepAliveService.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.service\n\nimport org.apache.pekko.actor.Status.{Failure => FailureMessage}\nimport org.apache.pekko.actor.{ActorRef, ActorSystem, Cancellable, FSM, Props, Stash}\nimport org.apache.pekko.pattern.pipe\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, MetricEmitter}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.InstanceId\nimport org.apache.openwhisk.core.etcd.EtcdClient\nimport org.apache.openwhisk.core.etcd.EtcdKV.InstanceKeys.instanceLease\nimport pureconfig.loadConfigOrThrow\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContextExecutor, Future}\nimport scala.util.{Failure, Success}\n\n// States\nsealed trait KeepAliveServiceState\ncase object Ready extends KeepAliveServiceState\ncase object Active extends KeepAliveServiceState\n\n// Data\nsealed trait KeepAliveServiceData\ncase object NoData extends KeepAliveServiceData\ncase class Lease(id: Long, ttl: Long) extends KeepAliveServiceData\ncase class ActiveStates(worker: Cancellable, lease: Lease) extends KeepAliveServiceData\n\n// Events received by the actor\ncase object RegrantLease\ncase object GetLease\ncase object GrantLease\n\n// Events internally used\ncase class SetLease(lease: Lease)\ncase class SetWatcher(worker: Cancellable)\n\nclass LeaseKeepAliveService(etcdClient: EtcdClient, instanceId: InstanceId, watcherService: ActorRef)(\n  implicit logging: Logging,\n  actorSystem: ActorSystem)\n    extends FSM[KeepAliveServiceState, KeepAliveServiceData]\n    with Stash {\n\n  implicit val ec: ExecutionContextExecutor = context.dispatcher\n\n  private val leaseTimeout = loadConfigOrThrow[Int](ConfigKeys.etcdLeaseTimeout).seconds\n  private val key = instanceLease(instanceId)\n  private val watcherName = \"lease-service\"\n\n  self ! GrantLease\n  startWith(Ready, NoData)\n\n  when(Ready) {\n    case Event(GrantLease, NoData) =>\n      etcdClient\n        .grant(leaseTimeout.toSeconds)\n        .map { res =>\n          SetLease(Lease(res.getID, res.getTTL))\n        }\n        .pipeTo(self)\n      stay\n\n    case Event(SetLease(lease), NoData) =>\n      startKeepAliveService(lease)\n        .pipeTo(self)\n      logging.info(this, s\"Granted a new lease $lease\")\n      stay using lease\n\n    case Event(SetWatcher(w), l: Lease) =>\n      goto(Active) using ActiveStates(w, l)\n\n    case Event(t: FailureMessage, _) =>\n      logging.warn(this, s\"Failed to grant new lease caused by: $t\")\n      self ! GrantLease\n      stay()\n\n    case _ => delay\n  }\n\n  when(Active) {\n    case Event(WatchEndpointRemoved(`key`, `key`, _, false), ActiveStates(worker, lease)) =>\n      logging.info(this, s\"endpoint ie removed so recreate a lease\")\n      recreateLease(worker, lease)\n\n    case Event(RegrantLease, ActiveStates(worker, lease)) =>\n      logging.info(this, s\"ReGrant a lease, old lease:${lease}\")\n      recreateLease(worker, lease)\n\n    case Event(GetLease, ActiveStates(_, lease)) =>\n      logging.info(this, s\"send the lease(${lease}) to ${sender()}\")\n      sender() ! lease\n      stay()\n\n    case _ => delay\n  }\n\n  initialize()\n\n  private def startKeepAliveService(lease: Lease): Future[SetWatcher] = {\n    val worker =\n      actorSystem.scheduler.scheduleAtFixedRate(initialDelay = 0.second, interval = 500.milliseconds)(() =>\n        keepAliveOnce(lease))\n\n    /**\n     * To verify that lease has been deleted since timeout,\n     * create a key using lease, watch the key, and receive an event for deletion.\n     */\n    etcdClient.put(key, s\"${lease.id}\", lease.id).map { _ =>\n      watcherService ! WatchEndpoint(key, s\"${lease.id}\", false, watcherName, Set(DeleteEvent))\n      SetWatcher(worker)\n    }\n  }\n\n  private def keepAliveOnce(lease: Lease): Future[Long] = {\n    etcdClient\n      .keepAliveOnce(lease.id)\n      .map(_.getID)\n      .andThen {\n        case Success(_) => MetricEmitter.emitCounterMetric(LoggingMarkers.SCHEDULER_KEEP_ALIVE(lease.id))\n        case Failure(t) =>\n          logging.warn(this, s\"Failed to keep-alive of ${lease.id} caused by ${t}\")\n          self ! RegrantLease\n      }\n  }\n\n  private def recreateLease(worker: Cancellable, lease: Lease) = {\n    logging.info(this, s\"recreate a lease, old lease: $lease\")\n    worker.cancel() // stop scheduler\n    watcherService ! UnwatchEndpoint(key, false, watcherName) // stop watcher\n    etcdClient\n      .revoke(lease.id) // delete lease\n      .onComplete(_ => self ! GrantLease) // create lease\n    goto(Ready) using NoData\n  }\n\n  // Unstash all messages stashed while in intermediate state\n  onTransition {\n    case _ -> Ready  => unstashAll()\n    case _ -> Active => unstashAll()\n  }\n\n  /** Delays all incoming messages until unstashAll() is called */\n  def delay = {\n    stash()\n    stay\n  }\n\n  override def postStop(): Unit = {\n    stateData match {\n      case ActiveStates(w, _) => w.cancel() // stop scheduler if that exist\n      case _                  => // do nothing\n    }\n    watcherService ! UnwatchEndpoint(key, false, watcherName)\n  }\n}\n\nobject LeaseKeepAliveService {\n  def props(etcdClient: EtcdClient, instanceId: InstanceId, watcherService: ActorRef)(\n    implicit logging: Logging,\n    actorSystem: ActorSystem): Props = {\n    Props(new LeaseKeepAliveService(etcdClient, instanceId, watcherService))\n      .withDispatcher(\"dispatchers.lease-service-dispatcher\")\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/service/WatcherService.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.service\n\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorSystem, Props}\nimport com.ibm.etcd.api.Event.EventType\nimport com.ibm.etcd.client.kv.KvClient\nimport org.apache.openwhisk.common.{GracefulShutdown, Logging}\nimport org.apache.openwhisk.core.etcd.EtcdClient\nimport org.apache.openwhisk.core.etcd.EtcdType._\n\nimport scala.collection.JavaConverters._\nimport scala.collection.concurrent.TrieMap\n\n// messages received by this actor\ncase class WatchEndpoint(key: String,\n                         value: String,\n                         isPrefix: Boolean,\n                         name: String,\n                         listenEvents: Set[EtcdEvent] = Set.empty)\ncase class UnwatchEndpoint(watchKey: String, isPrefix: Boolean, watchName: String, needFeedback: Boolean = false)\n\ncase object RestartWatcher\n\n// the watchKey is the string user want to watch, it can be a prefix, the key is a record's key in Etcd\n// so if `isPrefix = true`, the `watchKey != key`, else the `watchKey == key`\nsealed abstract class WatchEndpointOperation(val watchKey: String,\n                                             val key: String,\n                                             val value: String,\n                                             val isPrefix: Boolean)\ncase class WatchEndpointRemoved(override val watchKey: String,\n                                override val key: String,\n                                override val value: String,\n                                override val isPrefix: Boolean)\n    extends WatchEndpointOperation(watchKey, key, value, isPrefix)\ncase class WatchEndpointInserted(override val watchKey: String,\n                                 override val key: String,\n                                 override val value: String,\n                                 override val isPrefix: Boolean)\n    extends WatchEndpointOperation(watchKey, key, value, isPrefix)\ncase class WatcherClosed(key: String, isPrefix: Boolean)\n\n// These are abstraction for event from ETCD.\nsealed trait EtcdEvent\ncase object PutEvent extends EtcdEvent\ncase object DeleteEvent extends EtcdEvent\n\n// there may be several watchers for a same watcher key, so add a watcherName to distinguish them\ncase class WatcherKey(watchKey: String, watchName: String)\n\nclass WatcherService(etcdClient: EtcdClient)(implicit logging: Logging, actorSystem: ActorSystem) extends Actor {\n\n  implicit val ec = context.dispatcher\n\n  private[service] val putWatchers = TrieMap[WatcherKey, ActorRef]()\n  private[service] val deleteWatchers = TrieMap[WatcherKey, ActorRef]()\n  private[service] val prefixPutWatchers = TrieMap[WatcherKey, ActorRef]()\n  private[service] val prefixDeleteWatchers = TrieMap[WatcherKey, ActorRef]()\n\n  private def startWatch(): KvClient.Watch = {\n    etcdClient.watchAllKeys(\n      res =>\n        res.getEvents.asScala.foreach { event =>\n          event.getType match {\n            case EventType.DELETE =>\n              val key = ByteStringToString(event.getPrevKv.getKey)\n              val value = ByteStringToString(event.getPrevKv.getValue)\n              val watchEvent = WatchEndpointRemoved(key, key, value, false)\n              deleteWatchers\n                .foreach { watcher =>\n                  if (watcher._1.watchKey == key) {\n                    watcher._2 ! watchEvent\n                  }\n                }\n              prefixDeleteWatchers\n                .foreach { watcher =>\n                  if (key.startsWith(watcher._1.watchKey)) {\n                    watcher._2 ! WatchEndpointRemoved(watcher._1.watchKey, key, value, true)\n                  }\n                }\n            case EventType.PUT =>\n              val key = ByteStringToString(event.getKv.getKey)\n              val value = ByteStringToString(event.getKv.getValue)\n              val watchEvent = WatchEndpointInserted(key, key, value, false)\n              putWatchers\n                .foreach { watcher =>\n                  if (watcher._1.watchKey == key) {\n                    watcher._2 ! watchEvent\n                  }\n                }\n              prefixPutWatchers\n                .foreach { watcher =>\n                  if (key.startsWith(watcher._1.watchKey)) {\n                    watcher._2 ! WatchEndpointInserted(watcher._1.watchKey, key, value, true)\n                  }\n                }\n            case msg =>\n              logging.debug(this, s\"watch event received: $msg.\")\n          }\n      },\n      error => {\n        logging.error(this, s\"encountered error, restarting watcher service: $error\")\n        self ! RestartWatcher\n      },\n      () => {\n        logging.warn(this, s\"watch stream completed, restarting watcher service\")\n        self ! RestartWatcher\n      })\n  }\n\n  private def watchBehavior(watcher: KvClient.Watch): Receive = {\n    case request: WatchEndpoint =>\n      logging.info(this, s\"watch endpoint: $request\")\n      val watcherKey = WatcherKey(request.key, request.name)\n      if (request.listenEvents.contains(PutEvent))\n        if (request.isPrefix)\n          prefixPutWatchers.update(watcherKey, sender())\n        else\n          putWatchers.update(watcherKey, sender())\n\n      if (request.listenEvents.contains(DeleteEvent))\n        if (request.isPrefix)\n          prefixDeleteWatchers.update(watcherKey, sender())\n        else\n          deleteWatchers.update(watcherKey, sender())\n\n    case request: UnwatchEndpoint =>\n      logging.info(this, s\"unwatch endpoint: $request\")\n      val watcherKey = WatcherKey(request.watchKey, request.watchName)\n      if (request.isPrefix) {\n        prefixPutWatchers.remove(watcherKey)\n        prefixDeleteWatchers.remove(watcherKey)\n      } else {\n        putWatchers.remove(watcherKey)\n        deleteWatchers.remove(watcherKey)\n      }\n\n      // always send WatcherClosed back to sender if it need a feedback\n      if (request.needFeedback)\n        sender ! WatcherClosed(request.watchKey, request.isPrefix)\n\n    case RestartWatcher =>\n      watcher.close()\n      context.become(watchBehavior(startWatch()))\n\n    case GracefulShutdown =>\n      watcher.close()\n      putWatchers.clear()\n      deleteWatchers.clear()\n      prefixPutWatchers.clear()\n      prefixDeleteWatchers.clear()\n  }\n\n  override def receive: Receive = watchBehavior(startWatch())\n\n}\nobject WatcherService {\n  def props(etcdClient: EtcdClient)(implicit logging: Logging, actorSystem: ActorSystem): Props = {\n    Props(new WatcherService(etcdClient))\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/yarn/YARNComponentActor.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.yarn\n\nimport org.apache.pekko.actor.{Actor, ActorSystem}\nimport org.apache.pekko.http.scaladsl.model.{HttpMethods, StatusCodes}\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.core.yarn.YARNComponentActor.{CreateContainerAsync, RemoveContainer}\nimport spray.json.{JsArray, JsNumber, JsObject, JsString}\n\nimport scala.concurrent.ExecutionContext\n\n/** Submits create and decommission commands to YARN */\nobject YARNComponentActor {\n  case object CreateContainerAsync\n  case class RemoveContainer(component_instance_name: String)\n}\n\nclass YARNComponentActor(actorSystem: ActorSystem,\n                         logging: Logging,\n                         yarnConfig: YARNConfig,\n                         serviceName: String,\n                         imageName: ImageName)\n    extends Actor {\n\n  implicit val as: ActorSystem = actorSystem\n  implicit val ec: ExecutionContext = actorSystem.dispatcher\n\n  //Adding a container via the YARN REST API is actually done by flexing the component's container pool to a certain size.\n  // This actor must track the current containerCount in order to make the correct scale-up request.\n  var containerCount: Int = 0\n\n  def receive: PartialFunction[Any, Unit] = {\n    case CreateContainerAsync =>\n      sender ! createContainerAsync\n\n    case RemoveContainer(component_instance_name) =>\n      sender ! removeContainer(component_instance_name)\n\n    case input =>\n      throw new IllegalArgumentException(\"Unknown input: \" + input)\n      sender ! false\n  }\n\n  def createContainerAsync(): Unit = {\n    logging.info(this, s\"Using YARN to create a container with image ${imageName.name}...\")\n\n    val body = JsObject(\"number_of_containers\" -> JsNumber(containerCount + 1)).compactPrint\n    val response = YARNRESTUtil.submitRequestWithAuth(\n      yarnConfig.authType,\n      HttpMethods.PUT,\n      s\"${yarnConfig.masterUrl}/app/v1/services/$serviceName/components/${imageName.name}\",\n      body)\n    response match {\n      case httpresponse(StatusCodes.OK, content) =>\n        logging.info(this, s\"Added container: ${imageName.name}. Response: $content\")\n        containerCount += 1\n\n      case httpresponse(_, _) => YARNRESTUtil.handleYARNRESTError(logging)\n    }\n  }\n\n  def removeContainer(component_instance_name: String): Unit = {\n    logging.info(this, s\"Removing ${imageName.name} container: $component_instance_name \")\n    if (containerCount <= 0) {\n      logging.warn(this, \"Already at 0 containers\")\n    } else {\n      val body = JsObject(\n        \"components\" -> JsArray(\n          JsObject(\n            \"name\" -> JsString(imageName.name),\n            \"decommissioned_instances\" -> JsArray(JsString(component_instance_name))))).compactPrint\n      val response = YARNRESTUtil.submitRequestWithAuth(\n        yarnConfig.authType,\n        HttpMethods.PUT,\n        s\"${yarnConfig.masterUrl}/app/v1/services/$serviceName\",\n        body)\n      response match {\n        case httpresponse(StatusCodes.OK, content) =>\n          logging.info(\n            this,\n            s\"Successfully removed ${imageName.name} container: $component_instance_name. Response: $content\")\n          containerCount -= 1\n\n        case httpresponse(_, _) => YARNRESTUtil.handleYARNRESTError(logging)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/yarn/YARNContainerFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.yarn\n\nimport org.apache.pekko.actor.{ActorRef, ActorSystem, Props}\nimport org.apache.pekko.http.scaladsl.model.{HttpMethods, StatusCodes}\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.util.Timeout\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.core.entity.{ByteSize, ExecManifest, InvokerInstanceId}\nimport org.apache.openwhisk.core.yarn.YARNComponentActor.CreateContainerAsync\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\n\nimport scala.collection.immutable.HashMap\nimport scala.concurrent.{blocking, ExecutionContext, Future}\nimport scala.concurrent.duration._\nimport YARNJsonProtocol._\n\ncase class YARNConfig(masterUrl: String,\n                      yarnLinkLogMessage: Boolean,\n                      serviceName: String,\n                      authType: String,\n                      kerberosPrincipal: String,\n                      kerberosKeytab: String,\n                      queue: String,\n                      memory: String,\n                      cpus: Int)\n\nobject YARNContainerFactoryProvider extends ContainerFactoryProvider {\n  override def instance(actorSystem: ActorSystem,\n                        logging: Logging,\n                        config: WhiskConfig,\n                        instance: InvokerInstanceId,\n                        parameters: Map[String, Set[String]]): ContainerFactory =\n    new YARNContainerFactory(actorSystem, logging, config, instance, parameters)\n}\n\nclass YARNContainerFactory(actorSystem: ActorSystem,\n                           logging: Logging,\n                           config: WhiskConfig,\n                           instance: InvokerInstanceId,\n                           parameters: Map[String, Set[String]],\n                           containerArgs: ContainerArgsConfig =\n                             loadConfigOrThrow[ContainerArgsConfig](ConfigKeys.containerArgs),\n                           yarnConfig: YARNConfig = loadConfigOrThrow[YARNConfig](ConfigKeys.yarn))\n    extends ContainerFactory {\n\n  val images: Set[ImageName] = ExecManifest.runtimesManifest.runtimes.flatMap(a => a.versions.map(b => b.image))\n\n  //One actor of each type per image for parallelism\n  private var yarnComponentActors: Map[ImageName, ActorRef] = HashMap[ImageName, ActorRef]()\n  private var YARNContainerInfoActors: Map[ImageName, ActorRef] = HashMap[ImageName, ActorRef]()\n\n  val serviceStartTimeoutMS = 60000\n  val retryWaitMS = 1000\n  val runCommand = \"\"\n  val version = \"1.0.0\"\n  val description = \"OpenWhisk Action Service\"\n\n  //Allows for invoker HA\n  val serviceName: String = yarnConfig.serviceName + \"-\" + instance.toInt\n\n  val containerStartTimeoutMS = 60000\n\n  implicit val as: ActorSystem = actorSystem\n  implicit val ec: ExecutionContext = actorSystem.dispatcher\n\n  override def init(): Unit = {\n    yarnComponentActors = images\n      .map(\n        i =>\n          (\n            i,\n            actorSystem.actorOf(\n              Props(new YARNComponentActor(actorSystem, logging, yarnConfig, serviceName, i)),\n              name = s\"YARNComponentActor-${i.name}\")))\n      .toMap\n    YARNContainerInfoActors = images\n      .map(\n        i =>\n          (\n            i,\n            actorSystem.actorOf(\n              Props(new YARNContainerInfoActor(actorSystem, logging, yarnConfig, serviceName, i)),\n              name = s\"YARNComponentInfoActor-${i.name}\")))\n      .toMap\n    blocking {\n      implicit val timeout: Timeout = Timeout(serviceStartTimeoutMS.milliseconds)\n\n      //Remove service if it already exists\n      val serviceDef =\n        YARNRESTUtil.downloadServiceDefinition(yarnConfig.authType, serviceName, yarnConfig.masterUrl)(logging)\n\n      if (serviceDef != null)\n        removeService()\n\n      createService()\n    }\n  }\n  override def createContainer(\n    unusedtid: TransactionId,\n    unusedname: String,\n    actionImage: ExecManifest.ImageName,\n    unuseduserProvidedImage: Boolean,\n    unusedmemory: ByteSize,\n    unusedcpuShares: Int,\n    unusedcpuLimit: Option[Double])(implicit config: WhiskConfig, logging: Logging): Future[Container] = {\n    implicit val timeout: Timeout = Timeout(containerStartTimeoutMS.milliseconds)\n\n    //First send the create command to YARN, then with a different actor, wait for the container to be ready\n    ask(yarnComponentActors(actionImage), CreateContainerAsync).flatMap(_ =>\n      ask(YARNContainerInfoActors(actionImage), GetContainerInfo(yarnComponentActors(actionImage))).mapTo[Container])\n  }\n  override def cleanup(): Unit = {\n    removeService()\n    yarnComponentActors foreach { case (k, v)     => actorSystem.stop(v) }\n    YARNContainerInfoActors foreach { case (k, v) => actorSystem.stop(v) }\n  }\n  def createService(): Unit = {\n    logging.info(this, \"Creating Service with images: \" + images.map(i => i.resolveImageName()).mkString(\", \"))\n\n    val componentList = images\n      .map(\n        i =>\n          ComponentDefinition(\n            i.name.replace('.', '-'), //name must be [a-z][a-z0-9-]*\n            Some(0), //start with zero containers\n            Some(runCommand),\n            Option.empty,\n            Some(ArtifactDefinition(i.resolveImageName(), \"DOCKER\")),\n            Some(ResourceDefinition(yarnConfig.cpus, yarnConfig.memory)),\n            Some(ConfigurationDefinition(Map((\"YARN_CONTAINER_RUNTIME_DOCKER_RUN_OVERRIDE_DISABLE\", \"true\")))),\n            List[String]()))\n      .toList\n\n    //Add kerberos def if necessary\n    var kerberosDef: Option[KerberosPrincipalDefinition] = None\n    if (yarnConfig.authType.equals(YARNRESTUtil.KERBEROSAUTH))\n      kerberosDef = Some(\n        KerberosPrincipalDefinition(Some(yarnConfig.kerberosPrincipal), Some(yarnConfig.kerberosKeytab)))\n\n    val service = ServiceDefinition(\n      Some(serviceName),\n      Some(version),\n      Some(description),\n      Some(\"STABLE\"),\n      Some(yarnConfig.queue),\n      componentList,\n      kerberosDef)\n\n    //Submit service\n    val response =\n      YARNRESTUtil.submitRequestWithAuth(\n        yarnConfig.authType,\n        HttpMethods.POST,\n        s\"${yarnConfig.masterUrl}/app/v1/services\",\n        service.toJson.compactPrint)\n\n    //Handle response\n    response match {\n      case httpresponse(StatusCodes.OK, content) =>\n        logging.info(this, s\"Service submitted. Response: $content\")\n\n      case httpresponse(StatusCodes.Accepted, content) =>\n        logging.info(this, s\"Service submitted. Response: $content\")\n\n      case httpresponse(_, _) => YARNRESTUtil.handleYARNRESTError(logging)\n    }\n\n    //Wait for service start (up to serviceStartTimeoutMS milliseconds)\n    var started = false\n    var retryCount = 0\n    val maxRetryCount = serviceStartTimeoutMS / retryWaitMS\n    while (!started && retryCount < maxRetryCount) {\n      val serviceDef =\n        YARNRESTUtil.downloadServiceDefinition(yarnConfig.authType, serviceName, yarnConfig.masterUrl)(logging)\n\n      if (serviceDef == null) {\n        logging.info(this, \"Service not found yet\")\n        Thread.sleep(retryWaitMS)\n      } else {\n        serviceDef.state.getOrElse(None) match {\n          case \"STABLE\" | \"STARTED\" =>\n            logging.info(this, \"YARN service achieved stable state\")\n            started = true\n\n          case state =>\n            logging.info(\n              this,\n              s\"YARN service is not in stable state yet ($retryCount/$maxRetryCount). Current state: $state\")\n            Thread.sleep(retryWaitMS)\n        }\n      }\n      retryCount += 1\n    }\n    if (!started)\n      throw new Exception(s\"After ${serviceStartTimeoutMS}ms YARN service did not achieve stable state\")\n  }\n  def removeService(): Unit = {\n    val response: httpresponse =\n      YARNRESTUtil.submitRequestWithAuth(\n        yarnConfig.authType,\n        HttpMethods.DELETE,\n        s\"${yarnConfig.masterUrl}/app/v1/services/$serviceName\",\n        \"\")\n\n    response match {\n      case httpresponse(StatusCodes.OK, _) =>\n        logging.info(this, \"YARN service Removed\")\n\n      case httpresponse(StatusCodes.NotFound, _) =>\n        logging.warn(this, \"YARN service did not exist\")\n\n      case httpresponse(StatusCodes.BadRequest, _) =>\n        logging.warn(this, \"YARN service did not exist\")\n\n      case httpresponse(_, _) =>\n        YARNRESTUtil.handleYARNRESTError(logging)\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/yarn/YARNContainerInfoActor.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.yarn\n\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorSystem}\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.containerpool.{ContainerAddress, ContainerId}\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\n\nimport scala.collection.immutable.HashMap\nimport scala.concurrent.ExecutionContext\n\ncase class GetContainerInfo(yarnComponentActorRef: ActorRef)\n\n//This actor is separate from the YARNComponentActor so that container create commands can be issued in parallel\nclass YARNContainerInfoActor(actorSystem: ActorSystem,\n                             logging: Logging,\n                             yarnConfig: YARNConfig,\n                             serviceName: String,\n                             imageName: ImageName)\n    extends Actor {\n\n  implicit val as: ActorSystem = actorSystem\n  implicit val ec: ExecutionContext = actorSystem.dispatcher\n\n  val containerStartTimeoutMS = 60000\n  val retryWaitMS = 1000\n\n  //Map with the definition of all active containers\n  var containerDefMap: Map[String, ContainerDefinition] = new HashMap[String, ContainerDefinition]\n\n  //Map that keeps track of which containers have been returned to the main invoker for use\n  val containersAllocated = new scala.collection.mutable.HashMap[String, Boolean]\n\n  def receive: PartialFunction[Any, Unit] = {\n\n    case GetContainerInfo(yarnComponentActorRef) =>\n      //Check if there are any left over containers from the last check\n      var firstNewContainerName = containersAllocated.find { case (k, v) => !v }\n\n      //If no containers are ready, wait for one to come up (up to containerStartTimeoutMS milliseconds)\n      var retryCount = 0\n      val maxRetryCount = containerStartTimeoutMS / retryWaitMS\n      while (firstNewContainerName.isEmpty && retryCount < maxRetryCount) {\n        //Get updated service def\n        val serviceDef =\n          YARNRESTUtil.downloadServiceDefinition(yarnConfig.authType, serviceName, yarnConfig.masterUrl)(logging)\n\n        //Update container list with new container details\n        if (serviceDef == null) {\n          retryCount += 1\n          Thread.sleep(retryWaitMS)\n          logging.info(this, s\"Waiting for ${imageName.name} YARN container ($retryCount/$maxRetryCount)\")\n        } else {\n          containerDefMap = serviceDef.components\n            .filter(c => c.name.equals(imageName.name))\n            .flatMap(c => c.containers.getOrElse(List[ContainerDefinition]()))\n            .filter(containerDef => containerDef.state.equals(\"READY\"))\n            .map(containerDef => (containerDef.component_instance_name, containerDef))\n            .toMap\n\n          //Filter map to only contain active containers\n          containersAllocated.retain((k, v) => containerDefMap.contains(k))\n          for (containerDef <- containerDefMap) {\n            if (!containersAllocated.contains(containerDef._1))\n              containersAllocated.put(containerDef._1, false)\n          }\n\n          firstNewContainerName = containersAllocated.find { case (k, v) => !v }\n\n          //keep waiting\n          if (firstNewContainerName.isEmpty) {\n            retryCount += 1\n            Thread.sleep(retryWaitMS)\n            logging.info(this, s\"Waiting for ${imageName.name} YARN container ($retryCount/$maxRetryCount)\")\n          }\n        }\n      }\n      if (firstNewContainerName.isEmpty) {\n        throw new Exception(s\"After ${containerStartTimeoutMS}ms ${imageName.name} YARN container was not available\")\n      }\n\n      //Return container\n      val newContainerDef = containerDefMap(firstNewContainerName.get._1)\n      containersAllocated(firstNewContainerName.get._1) = true\n\n      val containerAddress = ContainerAddress(newContainerDef.ip.getOrElse(\"127.0.0.1\")) //default port is 8080\n      val containerId = ContainerId(newContainerDef.id)\n\n      logging.info(this, s\"New ${imageName.name} YARN Container: ${newContainerDef.id}, $containerAddress\")\n      sender ! new YARNTask(\n        containerId,\n        containerAddress,\n        ec,\n        logging,\n        as,\n        newContainerDef.component_instance_name,\n        imageName,\n        yarnConfig,\n        yarnComponentActorRef)\n    case input =>\n      throw new IllegalArgumentException(\"Unknown input: \" + input)\n      sender ! None\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/yarn/YARNRESTUtil.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.yarn\n\nimport java.nio.charset.StandardCharsets\nimport java.security.Principal\n\nimport org.apache.pekko.http.scaladsl.model.{HttpMethod, HttpMethods, StatusCode, StatusCodes}\nimport javax.security.sasl.AuthenticationException\nimport org.apache.commons.io.IOUtils\nimport org.apache.http.auth.{AuthSchemeProvider, AuthScope, Credentials}\nimport org.apache.http.client.CredentialsProvider\nimport org.apache.http.client.config.AuthSchemes\nimport org.apache.http.client.methods._\nimport org.apache.http.config.RegistryBuilder\nimport org.apache.http.entity.StringEntity\nimport org.apache.http.impl.auth.SPNegoSchemeFactory\nimport org.apache.http.impl.client.{CloseableHttpClient, HttpClientBuilder, HttpClients}\nimport org.apache.openwhisk.common.Logging\nimport spray.json._\n\ncase class KerberosPrincipalDefinition(principal_name: Option[String], keytab: Option[String])\ncase class YARNResponseDefinition(diagnostics: String)\ncase class ConfigurationDefinition(env: Map[String, String])\ncase class ArtifactDefinition(id: String, `type`: String)\ncase class ResourceDefinition(cpus: Int, memory: String)\ncase class ContainerDefinition(ip: Option[String],\n                               bare_host: Option[String],\n                               component_instance_name: String,\n                               hostname: Option[String],\n                               id: String,\n                               launch_time: Long,\n                               state: String)\ncase class ComponentDefinition(name: String,\n                               number_of_containers: Option[Int],\n                               launch_command: Option[String],\n                               containers: Option[List[ContainerDefinition]],\n                               artifact: Option[ArtifactDefinition],\n                               resource: Option[ResourceDefinition],\n                               configuration: Option[ConfigurationDefinition],\n                               decommissioned_instances: List[String])\n\ncase class ServiceDefinition(name: Option[String],\n                             version: Option[String],\n                             description: Option[String],\n                             state: Option[String],\n                             queue: Option[String],\n                             components: List[ComponentDefinition],\n                             kerberos_principal: Option[KerberosPrincipalDefinition])\n\nobject YARNJsonProtocol extends DefaultJsonProtocol {\n  implicit val KerberosPrincipalDefinitionFormat: RootJsonFormat[KerberosPrincipalDefinition] = jsonFormat2(\n    KerberosPrincipalDefinition)\n  implicit val YARNResponseDefinitionFormat: RootJsonFormat[YARNResponseDefinition] = jsonFormat1(\n    YARNResponseDefinition)\n  implicit val configurationDefinitionFormat: RootJsonFormat[ConfigurationDefinition] = jsonFormat1(\n    ConfigurationDefinition)\n  implicit val artifactDefinitionFormat: RootJsonFormat[ArtifactDefinition] = jsonFormat2(ArtifactDefinition)\n  implicit val resourceDefinitionFormat: RootJsonFormat[ResourceDefinition] = jsonFormat2(ResourceDefinition)\n  implicit val containerDefinitionFormat: RootJsonFormat[ContainerDefinition] = jsonFormat7(ContainerDefinition)\n  implicit val componentDefinitionFormat: RootJsonFormat[ComponentDefinition] = jsonFormat8(ComponentDefinition)\n  implicit val serviceDefinitionFormat: RootJsonFormat[ServiceDefinition] = jsonFormat7(ServiceDefinition)\n}\nimport YARNJsonProtocol._\n\ncase class httpresponse(statusCode: StatusCode, content: String)\n\nobject YARNRESTUtil {\n  val SIMPLEAUTH = \"simple\"\n  val KERBEROSAUTH = \"kerberos\"\n  def downloadServiceDefinition(authType: String, serviceName: String, masterUrl: String)(\n    implicit logging: Logging): ServiceDefinition = {\n    val response: httpresponse =\n      YARNRESTUtil.submitRequestWithAuth(authType, HttpMethods.GET, s\"$masterUrl/app/v1/services/$serviceName\", \"\")\n\n    response match {\n      case httpresponse(StatusCodes.OK, content) =>\n        content.parseJson.convertTo[ServiceDefinition]\n\n      case httpresponse(StatusCodes.NotFound, content) =>\n        logging.info(this, s\"Service not found. Response: $content\")\n        null\n\n      case httpresponse(_, _) =>\n        handleYARNRESTError(logging)\n        null\n    }\n  }\n\n  def submitRequestWithAuth(authType: String, httpMethod: HttpMethod, URL: String, body: String): httpresponse = {\n\n    var client: CloseableHttpClient = null\n    var updatedURL = URL\n\n    authType match {\n      case SIMPLEAUTH =>\n        if (URL.contains(\"?\"))\n          updatedURL = URL + \"&user.name=\" + System.getProperty(\"user.name\")\n        else\n          updatedURL = URL + \"?user.name=\" + System.getProperty(\"user.name\")\n\n        client = HttpClientBuilder.create.build\n\n      case KERBEROSAUTH =>\n        //System.setProperty(\"sun.security.krb5.debug\", \"true\")\n        //System.setProperty(\"sun.security.spnego.debug\", \"true\")\n        System.setProperty(\"javax.security.auth.useSubjectCredsOnly\", \"false\")\n\n        //Null credentials result in jaas login\n        val useJaaS = new CredentialsProvider {\n          override def setCredentials(authscope: AuthScope, credentials: Credentials): Unit = {}\n\n          override def getCredentials(authscope: AuthScope): Credentials = new Credentials() {\n            def getPassword: String = null\n            def getUserPrincipal: Principal = null\n          }\n          override def clear(): Unit = {}\n        }\n\n        val authSchemeRegistry = RegistryBuilder\n          .create[AuthSchemeProvider]()\n          .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory())\n          .build()\n\n        client = HttpClients\n          .custom()\n          .setDefaultAuthSchemeRegistry(authSchemeRegistry)\n          .setDefaultCredentialsProvider(useJaaS)\n          .build()\n    }\n\n    var request: HttpRequestBase = null\n    httpMethod match {\n      case HttpMethods.GET =>\n        request = new HttpGet(updatedURL)\n\n      case HttpMethods.POST =>\n        request = new HttpPost(updatedURL)\n        request.asInstanceOf[HttpPost].setEntity(new StringEntity(body.toString, StandardCharsets.UTF_8.toString))\n\n      case HttpMethods.PUT =>\n        request = new HttpPut(updatedURL)\n        request.asInstanceOf[HttpPut].setEntity(new StringEntity(body.toString, StandardCharsets.UTF_8.toString))\n\n      case HttpMethods.DELETE =>\n        request = new HttpDelete(updatedURL)\n\n      case _ =>\n        throw new IllegalArgumentException(s\"Unsupported HTTP method: $httpMethod\")\n    }\n    request.addHeader(\"content-type\", \"application/json\")\n\n    try {\n      val response = client.execute(request)\n      val responseBody = IOUtils.toString(response.getEntity.getContent, StandardCharsets.UTF_8)\n      val statusCode: Int = response.getStatusLine.getStatusCode\n\n      httpresponse(statusCode, responseBody)\n\n    } catch {\n      case e: Exception =>\n        e.printStackTrace()\n\n        httpresponse(500, e.getMessage)\n    }\n  }\n  def handleYARNRESTError(implicit logging: Logging): PartialFunction[httpresponse, Unit] = {\n\n    case httpresponse(StatusCodes.Unauthorized, content) =>\n      logging.error(\n        this,\n        s\"Received 401 (Authentication Required) response code from YARN. Check authentication. Response: $content\")\n      throw new AuthenticationException(\n        s\"Received 401 (Authentication Required) response code from YARN. Check authentication. Response: $content\")\n\n    case httpresponse(StatusCodes.Forbidden, content) =>\n      logging.error(this, s\"Received 403 (Forbidden) response code from YARN. Check authentication. Response: $content\")\n      throw new AuthenticationException(\n        s\"Received 403 (Forbidden) response code from YARN. Check authentication. Response: $content\")\n\n    case httpresponse(code, content) =>\n      logging.error(this, \"Unknown response from YARN\")\n      throw new Exception(s\"Unknown response from YARN. Code: ${code.intValue}, content: $content\")\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/core/yarn/YARNTask.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.yarn\n\nimport java.time.Instant\n\nimport org.apache.pekko.actor.{ActorRef, ActorSystem}\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.util.{ByteString, Timeout}\nimport spray.json._\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.containerpool.{Container, ContainerAddress, ContainerId}\nimport org.apache.openwhisk.core.containerpool.logging.LogLine\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.core.yarn.YARNComponentActor.RemoveContainer\nimport org.apache.pekko.pattern.ask\nimport scala.concurrent.duration._\n\nimport scala.concurrent.{ExecutionContext, Future}\n\n/**\n * YARNTask implementation of Container.\n * Differences from DockerContainer include:\n * - does not launch container using docker cli, but rather the YARN framework\n * - does not support pause/resume\n * - does not support log collection (currently), but does provide a message indicating logs can be viewed via YARN UI\n * (external log collection and retrieval must be enabled via LogStore SPI to expose logs to wsk cli)\n */\nclass YARNTask(override protected val id: ContainerId,\n               override protected[core] val addr: ContainerAddress,\n               override protected val ec: ExecutionContext,\n               override protected val logging: Logging,\n               override protected val as: ActorSystem,\n               val component_instance_name: String,\n               imageName: ImageName,\n               yarnConfig: YARNConfig,\n               yarnComponentActor: ActorRef)\n    extends Container {\n\n  val containerRemoveTimeoutMS = 60000\n\n  /** Stops the container from consuming CPU cycles. */\n  override def suspend()(implicit transid: TransactionId): Future[Unit] = {\n    // suspend not supported (just return result from super)\n    super.suspend()\n  }\n\n  /** Dual of halt. */\n  override def resume()(implicit transid: TransactionId): Future[Unit] = {\n    // resume not supported\n    Future.successful(())\n  }\n\n  /** Completely destroys this instance of the container. */\n  override def destroy()(implicit transid: TransactionId): Future[Unit] = {\n\n    implicit val timeout: Timeout = Timeout(containerRemoveTimeoutMS.milliseconds)\n    ask(yarnComponentActor, RemoveContainer(component_instance_name)).mapTo[Unit]\n  }\n\n  /**\n   * Obtains logs up to a given threshold from the container. Optionally waits for a sentinel to appear.\n   * For YARN, this log message is static per container, just indicating that YARN logs can be found via the YARN UI.\n   * To disable this message, and just store an static log message per activation, set\n   *     whisk.yarn.yarnLinkLogMessage=false\n   */\n  private val linkedLogMsg =\n    s\"Logs are not collected from YARN containers currently. \" +\n      s\"You can browse the logs for YARN Service ${yarnConfig.serviceName} using the yarn UI at ${yarnConfig.masterUrl}\"\n  private val noLinkLogMsg = \"Log collection is not configured correctly, check with your service administrator.\"\n  private val logMsg = if (yarnConfig.yarnLinkLogMessage) linkedLogMsg else noLinkLogMsg\n  override def logs(limit: ByteSize, waitForSentinel: Boolean)(\n    implicit transid: TransactionId): Source[ByteString, Any] =\n    Source.single(ByteString(LogLine(logMsg, \"stdout\", Instant.now.toString).toJson.compactPrint))\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/http/BasicHttpService.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.http\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging\nimport org.apache.pekko.http.scaladsl.{Http, ServerBuilder}\nimport org.apache.pekko.http.scaladsl.model.{HttpRequest, _}\nimport org.apache.pekko.http.scaladsl.server.RouteResult.Rejected\nimport org.apache.pekko.http.scaladsl.server._\nimport org.apache.pekko.http.scaladsl.server.directives._\n\nimport kamon.metric.MeasurementUnit\nimport spray.json._\nimport org.apache.openwhisk.common.Https.HttpsConfig\nimport org.apache.openwhisk.common._\n\nimport scala.collection.immutable.Seq\nimport scala.concurrent.duration.DurationInt\nimport scala.concurrent.{Await, Future}\n\n/**\n * This trait extends the Pekko Directives and Actor with logging and transaction counting\n * facilities common to all OpenWhisk REST services.\n */\ntrait BasicHttpService extends Directives {\n\n  val OW_EXTRA_LOGGING_HEADER = \"X-OW-EXTRA-LOGGING\"\n\n  /**\n   * Gets the routes implemented by the HTTP service.\n   *\n   * @param transid the id for the transaction (every request is assigned an id)\n   */\n  def routes(implicit transid: TransactionId): Route\n\n  /**\n   * Gets the log level for a given route. The default is\n   * InfoLevel so override as needed.\n   *\n   * @param route the route to determine the loglevel for\n   * @return a log level for the route\n   */\n  def loglevelForRoute(route: String): Logging.LogLevel = Logging.InfoLevel\n\n  /** Prioritized rejections based on relevance. */\n  val prioritizeRejections = recoverRejections { rejections =>\n    val priorityRejection = rejections.find {\n      case rejection: UnacceptedResponseContentTypeRejection => true\n      case rejection: ValidationRejection                    => true\n      case _                                                 => false\n    }\n\n    priorityRejection.map(rejection => Rejected(Seq(rejection))).getOrElse(Rejected(rejections))\n  }\n\n  /**\n   * Receives a message and runs the router.\n   */\n  def route: Route = {\n    assignId { implicit transid =>\n      respondWithHeader(transid.toHeader) {\n        DebuggingDirectives.logRequest(logRequestInfo _) {\n          DebuggingDirectives.logRequestResult(logResponseInfo _) {\n            BasicDirectives.mapRequest(_.removeHeader(OW_EXTRA_LOGGING_HEADER)) {\n              handleRejections(BasicHttpService.customRejectionHandler) {\n                prioritizeRejections {\n                  toStrictEntity(30.seconds) {\n                    routes\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  /** Assigns transaction id to every request. */\n  protected def assignId = HeaderDirectives.optionalHeaderValueByName(OW_EXTRA_LOGGING_HEADER) flatMap { headerValue =>\n    val extraLogging = headerValue match {\n      // extract headers from HTTP request that indicates if additional logging should be enabled for this transaction.\n      // Passing \"on\" as header content will enable additional logging for this transaction,\n      // passing any other value will leave it as configured in the logging configuration\n      case Some(value) => value.toLowerCase == \"on\"\n      case None        => false\n    }\n    extract { req =>\n      val tid =\n        req.request.headers\n          .find(_.is(TransactionId.generatorConfig.lowerCaseHeader))\n          .map(_.value)\n          .filterNot(_.startsWith(TransactionId.systemPrefix))\n          .getOrElse {\n            // As this is only a fallback, because the tid should be generated by nginx, this shouldn't be used.\n            TransactionId.generateTid()\n          }\n\n      TransactionId(tid, extraLogging)\n    }\n  }\n\n  /** Generates log entry for every request. */\n  protected def logRequestInfo(req: HttpRequest)(implicit tid: TransactionId): LogEntry = {\n    val m = req.method.name\n    val p = req.uri.path.toString\n\n    val q: String = {\n      try {\n        req.uri.query().toString\n      } catch {\n        case _: IllegalUriException => s\"Bad query parameters:${req.uri.toString()}\"\n        case e: Exception           => s\"Query parsing error: ${e.getMessage}\"\n      }\n    }\n    val l = loglevelForRoute(p)\n    LogEntry(s\"[$tid] $m $p $q\", l)\n  }\n\n  protected def logResponseInfo(req: HttpRequest)(implicit tid: TransactionId): RouteResult => Option[LogEntry] = {\n    case RouteResult.Complete(res: HttpResponse) =>\n      val m = req.method.name\n      val p = req.uri.path.toString\n      val l = loglevelForRoute(p)\n\n      val name = \"BasicHttpService\"\n\n      val token =\n        LogMarkerToken(\n          \"http\",\n          m.toLowerCase,\n          LoggingMarkers.counter,\n          Some(res.status.intValue.toString),\n          Map(\"statusCode\" -> res.status.intValue.toString))(MeasurementUnit.time.milliseconds)\n      val marker = LogMarker(token, tid.deltaToStart, Some(tid.deltaToStart))\n\n      MetricEmitter.emitHistogramMetric(token, tid.deltaToStart)\n      MetricEmitter.emitCounterMetric(token)\n\n      if (TransactionId.metricsLog) {\n        Some(LogEntry(s\"[$tid] [$name] $marker\", l))\n      } else {\n        None\n      }\n\n    case _ => None // other kind of responses\n  }\n}\n\nobject BasicHttpService {\n\n  /**\n   * Starts an HTTP(S) route handler on given port and registers a shutdown hook.\n   */\n  def startHttpService(route: Route, port: Int, config: Option[HttpsConfig] = None, interface: String = \"0.0.0.0\")(\n    implicit\n    actorSystem: ActorSystem): Unit = {\n    val httpsContext = config.map(Https.connectionContextServer(_))\n    var httpBindingBuilder: ServerBuilder = Http().newServerAt(interface, port)\n    if (httpsContext.isDefined) {\n      httpBindingBuilder = httpBindingBuilder.enableHttps(httpsContext.get)\n    }\n    val httpBinding = httpBindingBuilder.bindFlow(route)\n    addShutdownHook(httpBinding)\n  }\n\n  def addShutdownHook(binding: Future[Http.ServerBinding])(implicit actorSystem: ActorSystem): Unit = {\n    implicit val executionContext = actorSystem.dispatcher\n    sys.addShutdownHook {\n      Await.result(binding.map(_.unbind()), 30.seconds)\n      Await.result(actorSystem.whenTerminated, 30.seconds)\n    }\n  }\n\n  /** Rejection handler to terminate connection on a bad request. Delegates to Pekko handler. */\n  def customRejectionHandler(implicit transid: TransactionId) = {\n    RejectionHandler.default.mapRejectionResponse {\n      case res @ HttpResponse(_, _, ent: HttpEntity.Strict, _) =>\n        val error = ErrorResponse(ent.data.utf8String, transid).toJson\n        res.withEntity(HttpEntity(ContentTypes.`application/json`, error.compactPrint))\n      case x => x\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/http/BasicRasService.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.http\n\nimport org.apache.pekko.event.Logging\nimport org.apache.openwhisk.common.{MetricsRoute, TransactionId}\n\n/**\n * This trait extends the BasicHttpService with a standard \"ping\" endpoint which\n * responds to health queries, intended for monitoring.\n */\ntrait BasicRasService extends BasicHttpService {\n\n  override def routes(implicit transid: TransactionId) = ping ~ MetricsRoute()\n\n  override def loglevelForRoute(route: String): Logging.LogLevel = {\n    if (route == \"/ping\" || route == \"/metrics\") {\n      Logging.DebugLevel\n    } else {\n      super.loglevelForRoute(route)\n    }\n  }\n\n  val ping = path(\"ping\") {\n    get { complete(\"pong\") }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/http/CorsSettings.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.http\n\nimport org.apache.pekko.http.scaladsl.model.HttpMethods._\nimport org.apache.pekko.http.scaladsl.model.headers.{\n  `Access-Control-Allow-Headers`,\n  `Access-Control-Allow-Methods`,\n  `Access-Control-Allow-Origin`\n}\nimport org.apache.pekko.http.scaladsl.server.Directives\n\n/**\n * Defines the CORS settings for the REST APIs and Web Actions.\n */\nobject CorsSettings {\n\n  trait RestAPIs {\n    val allowOrigin = Defaults.allowOrigin\n    val allowHeaders = Defaults.allowHeaders\n    val allowMethods =\n      `Access-Control-Allow-Methods`(GET, DELETE, POST, PUT, HEAD)\n  }\n\n  trait WebActions {\n    val allowOrigin = Defaults.allowOrigin\n    val allowHeaders = Defaults.allowHeaders\n    val allowMethods = `Access-Control-Allow-Methods`(OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH)\n  }\n\n  object ServerAPIs {\n    val allowOrigin = Defaults.allowOrigin\n    val allowHeaders = Defaults.allowHeaders\n    val allowMethods = `Access-Control-Allow-Methods`(OPTIONS, GET, POST)\n  }\n\n  trait RespondWithServerCorsHeaders extends Directives {\n    val sendCorsHeaders = respondWithHeaders(ServerAPIs.allowOrigin, ServerAPIs.allowHeaders, ServerAPIs.allowMethods)\n  }\n\n  object Defaults {\n    val allowOrigin = `Access-Control-Allow-Origin`.*\n\n    val allowHeaders = `Access-Control-Allow-Headers`(\n      \"Authorization\",\n      \"Origin\",\n      \"X-Requested-With\",\n      \"Content-Type\",\n      \"Accept\",\n      \"User-Agent\")\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/http/ErrorResponse.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.http\n\nimport scala.concurrent.duration.Duration\nimport scala.concurrent.duration.FiniteDuration\nimport scala.util.Try\nimport org.apache.pekko.http.scaladsl.model.StatusCode\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Forbidden\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport org.apache.pekko.http.scaladsl.model.MediaType\nimport org.apache.pekko.http.scaladsl.server.Directives\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller\nimport org.apache.pekko.http.scaladsl.server.StandardRoute\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.SizeError\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.Exec\nimport org.apache.openwhisk.core.entity.ExecMetaDataBase\nimport org.apache.openwhisk.core.entity.ActivationId\n\nobject Messages {\n\n  /** Standard message for reporting resource conflicts. */\n  val conflictMessage = \"Concurrent modification to resource detected.\"\n\n  /**\n   * Standard message for reporting resource conformance error when trying to access\n   * a resource from a different collection.\n   */\n  val conformanceMessage = \"Resource by this name exists but is not in this collection.\"\n  val corruptedEntity = \"Resource is corrupted and cannot be read.\"\n\n  /**\n   * Standard message for reporting deprecated runtimes.\n   */\n  def runtimeDeprecated(e: Exec) =\n    s\"The '${e.kind}' runtime is no longer supported. You may read and delete but not update or invoke this action.\"\n\n  /**\n   * Standard message for reporting deprecated runtimes.\n   */\n  def runtimeDeprecated(e: ExecMetaDataBase) =\n    s\"The '${e.kind}' runtime is no longer supported. You may read and delete but not update or invoke this action.\"\n\n  /** Standard message for resource not found. */\n  val resourceDoesNotExist = \"The requested resource does not exist.\"\n  def resourceDoesntExist(value: String) = s\"The requested resource '$value' does not exist.\"\n\n  /** Standard message for too many activation requests within a rolling time window. */\n  def tooManyRequests(count: Int, allowed: Int) =\n    s\"Too many requests in the last minute (count: $count, allowed: $allowed).\"\n\n  /** Standard message for too many concurrent activation requests within a time window. */\n  val tooManyConcurrentRequests = s\"Too many concurrent requests in flight.\"\n  def tooManyConcurrentRequests(count: Int, allowed: Int) =\n    s\"Too many concurrent requests in flight (count: $count, allowed: $allowed).\"\n\n  def maxActionInstanceConcurrencyExceedsNamespace(namespaceConcurrencyLimit: Int) =\n    s\"Max action instance concurrency must not exceed your namespace concurrency of $namespaceConcurrencyLimit.\"\n\n  def belowMinAllowedActionInstanceConcurrency(minThreshold: Int) =\n    s\"Action container concurrency must be greater than or equal to $minThreshold.\"\n\n  /** System overload message. */\n  val systemOverloaded = \"System is overloaded, try again later.\"\n\n  /** Standard message when supplied authkey is not authorized for an operation. */\n  val notAuthorizedtoOperateOnResource = \"The supplied authentication is not authorized to access this resource.\"\n  def notAuthorizedtoAccessResource(value: String) =\n    s\"The supplied authentication is not authorized to access '$value'.\"\n  def notAuthorizedtoActionKind(value: String) =\n    s\"The supplied authentication is not authorized to access actions of kind '$value'.\"\n\n  /** Standard error message for malformed fully qualified entity names. */\n  val malformedFullyQualifiedEntityName =\n    \"The fully qualified name of the entity must contain at least the namespace and the name of the entity.\"\n  def entityNameTooLong(error: SizeError) = {\n    s\"${error.field} longer than allowed: ${error.is.toBytes} > ${error.allowed.toBytes}.\"\n  }\n  val entityNameIllegal = \"The name of the entity contains illegal characters.\"\n  val namespaceIllegal = \"The namespace contains illegal characters.\"\n\n  /** Standard error for malformed activation id. */\n  val activationIdIllegal = \"The activation id is not valid.\"\n  def activationIdLengthError(error: SizeError) = {\n    s\"${error.field} length is ${error.is.toBytes} but must be ${error.allowed.toBytes}.\"\n  }\n\n  /** Standard error for malformed creation id. */\n  val creationIdIllegal = \"The creation id is not valid.\"\n  def creationIdLengthError(error: SizeError) = {\n    s\"${error.field} length is ${error.is.toBytes} but must be ${error.allowed.toBytes}.\"\n  }\n\n  /** Error messages for sequence actions. */\n  val sequenceIsTooLong = \"Too many actions in the sequence.\"\n  val sequenceNoComponent = \"No component specified for the sequence.\"\n  val sequenceIsCyclic = \"Sequence may not refer to itself.\"\n  val sequenceComponentNotFound = \"Sequence component does not exist.\"\n\n  /** Error message for packages. */\n  val bindingDoesNotExist = \"Binding references a package that does not exist.\"\n  val packageCannotBecomeBinding = \"Resource is a package and cannot be converted into a binding.\"\n  val bindingCannotReferenceBinding = \"Cannot bind to another package binding.\"\n  val requestedBindingIsNotValid = \"Cannot bind to a resource that is not a package.\"\n  val notAllowedOnBinding = \"Operation not permitted on package binding.\"\n  def packageNameIsReserved(name: String) = s\"Package name '$name' is reserved.\"\n  def packageBindingCircularReference(name: String) = s\"Package binding '$name' contains a circular reference.\"\n\n  /** Error messages for triggers */\n  def triggerWithInactiveRule(rule: String, action: String) = {\n    s\"Rule '$rule' is inactive, action '$action' was not activated.\"\n  }\n\n  /** Error messages for sequence activations. */\n  def sequenceRetrieveActivationTimeout(id: ActivationId) =\n    s\"Timeout reached when retrieving activation $id for sequence component.\"\n  val sequenceActivationFailure = \"Sequence failed.\"\n\n  /** Error messages for compositions. */\n  val compositionIsTooLong = \"Too many actions in the composition.\"\n  val compositionActivationFailure = \"Activation failure during composition.\"\n  def compositionActivationTimeout(id: ActivationId) =\n    s\"Timeout reached when retrieving activation $id during composition.\"\n  def compositionComponentInvalid(value: JsValue) =\n    s\"Failed to parse action name from json value $value during composition.\"\n  def compositionComponentNotFound(name: String) =\n    s\"Failed to resolve action with name '$name' during composition.\"\n  def compositionComponentNotAccessible(name: String) =\n    s\"Failed entitlement check for action with name '$name' during composition.\"\n\n  /** Error messages for bad requests where parameters do not conform. */\n  val parametersNotAllowed = \"Request defines parameters that are not allowed (e.g., reserved properties).\"\n  def invalidTimeout(max: FiniteDuration) = s\"Timeout must be number of milliseconds up to ${max.toMillis}.\"\n\n  /** Error messages for activations. */\n  val abnormalInitialization = \"The action did not initialize and exited unexpectedly.\"\n  val abnormalRun = \"The action did not produce a valid response and exited unexpectedly.\"\n  val memoryExhausted = \"The action exhausted its memory and was aborted.\"\n  val docsNotAllowedWithCount = \"The parameter 'docs' is not permitted with 'count'.\"\n  def badNameFilter(value: String) = s\"Parameter may be a 'simple' name or 'package-name/simple' name: $value\"\n  def badEpoch(value: String) = s\"Parameter is not a valid value for epoch seconds: $value\"\n\n  /** Error message for size conformance. */\n  def entityTooBig(error: SizeError) = {\n    s\"${error.field} larger than allowed: ${error.is.toBytes} > ${error.allowed.toBytes} bytes.\"\n  }\n\n  def sizeExceedsAllowedThreshold(field: String, is: Int, allowed: Int) = {\n    s\"${field} size ${is} MB exceeds allowed threshold of ${allowed} MB\"\n  }\n  def sizeBelowAllowedThreshold(field: String, is: Int, allowed: Int) = {\n    s\"${field} size ${is} MB below allowed threshold of ${allowed} MB\"\n  }\n  def durationBelowAllowedThreshold(field: String, is: FiniteDuration, allowed: FiniteDuration) = {\n    s\"${field} ${is.toMillis} milliseconds below allowed threshold of ${allowed.toMillis} milliseconds\"\n  }\n  def durationExceedsAllowedThreshold(field: String, is: FiniteDuration, allowed: FiniteDuration) = {\n    s\"${field} ${is.toMillis} milliseconds exceeds allowed threshold of ${allowed.toMillis} milliseconds\"\n  }\n  def concurrencyExceedsAllowedThreshold(is: Int, allowed: Int) = {\n    s\"concurrency $is exceeds allowed threshold of $allowed\"\n  }\n  def concurrencyBelowAllowedThreshold(is: Int, allowed: Int) = {\n    s\"concurrency $is below allowed threshold of $allowed\"\n  }\n\n  def listLimitOutOfRange(collection: String, value: Int, max: Int) = {\n    s\"The value '$value' is not in the range of 0 to $max for $collection.\"\n  }\n\n  def invalidRuntimeError(kind: String, runtimes: Set[String]) = {\n    s\"The specified runtime '$kind' is not supported by this platform. Valid values are: ${runtimes.mkString(\"'\", \"', '\", \"'\")}.\"\n  }\n\n  def listSkipOutOfRange(collection: String, value: Int) = {\n    s\"The value '$value' is not greater than or equal to 0 for $collection.\"\n  }\n  def argumentNotInteger(collection: String, value: String) = s\"The value '$value' is not an integer for $collection.\"\n\n  def truncateLogs(limit: ByteSize) = {\n    s\"Logs were truncated because the total bytes size exceeds the limit of ${limit.toBytes} bytes.\"\n  }\n  val logFailure = \"There was an issue while collecting your logs. Data might be missing.\"\n\n  val logWarningDeveloperError = \"The action did not initialize or run as expected. Log data might be missing.\"\n\n  /** Error for meta api. */\n  val propertyNotFound = \"Response does not include requested property.\"\n  def invalidMedia(m: MediaType) = s\"Response is not valid '${m.value}'.\"\n  def contentTypeExtensionNotSupported(extensions: Set[String]) = {\n    s\"\"\"Extension must be specified and one of ${extensions.mkString(\"[\", \", \", \"]\")}.\"\"\"\n  }\n  val unsupportedContentType = \"\"\"Content type is not supported.\"\"\"\n  def unsupportedContentType(m: MediaType) = s\"\"\"Content type '${m.value}' is not supported.\"\"\"\n  val errorExtractingRequestBody = \"Failed extracting request body.\"\n\n  val responseNotReady = \"Response not yet ready.\"\n  val httpUnknownContentType = \"Response did not specify a known content-type.\"\n  val httpContentTypeError = \"Response type in header did not match generated content type.\"\n  val errorProcessingRequest = \"There was an error processing your request.\"\n\n  def invalidInitResponse(actualResponse: String) = {\n    \"The action failed during initialization\" + {\n      Option(actualResponse) filter { _.nonEmpty } map { s =>\n        s\": $s\"\n      } getOrElse \".\"\n    }\n  }\n\n  def invalidRunResponse(actualResponse: String) = {\n    \"The action did not produce a valid JSON or JSON Array response\" + {\n      Option(actualResponse) filter { _.nonEmpty } map { s =>\n        s\": $s\"\n      } getOrElse \".\"\n    }\n  }\n\n  def truncatedResponse(length: ByteSize, maxLength: ByteSize): String = {\n    s\"The action produced a response that exceeded the allowed length: ${length.toBytes} > ${maxLength.toBytes} bytes.\"\n  }\n\n  def truncatedResponse(trunk: String, length: ByteSize, maxLength: ByteSize): String = {\n    s\"${truncatedResponse(length, maxLength)} The truncated response was: $trunk\"\n  }\n\n  def timedoutActivation(timeout: Duration, init: Boolean) = {\n    s\"The action exceeded its time limits of ${timeout.toMillis} milliseconds\" + {\n      if (!init) \".\" else \" during initialization.\"\n    }\n  }\n\n  val namespacesBlacklisted = \"The action was not invoked due to a blacklisted namespace.\"\n  val namespaceLimitUnderZero = \"The namespace limit is less than or equal to 0.\"\n\n  val actionRemovedWhileInvoking = \"Action could not be found or may have been deleted.\"\n  val actionMismatchWhileInvoking = \"Action version is not compatible and cannot be invoked.\"\n  val actionFetchErrorWhileInvoking = \"Action could not be fetched.\"\n  val actionLimitExceededSystemLimit = \"Action limit exceeded the system limit.\"\n\n  /** Indicates that the image could not be pulled. */\n  def imagePullError(image: String) = s\"Failed to pull container image '$image'.\"\n\n  def commandNotFoundError = \"executable file not found\"\n\n  /** Indicates that the container for the action could not be started. */\n  val resourceProvisionError = \"Failed to provision resources to run the action.\"\n\n  def forbiddenGetActionBinding(entityDocId: String) =\n    s\"GET not permitted for '$entityDocId'. Resource does not exist or is an action in a shared package binding.\"\n  def forbiddenGetAction(entityPath: String) =\n    s\"GET not permitted for '$entityPath' since it's an action in a shared package\"\n  def forbiddenGetPackageBinding(packageName: String) =\n    s\"GET not permitted since $packageName is a binding of a shared package\"\n  def forbiddenGetPackage(packageName: String) =\n    s\"GET not permitted for '$packageName' since it's a shared package\"\n}\n\n/** Replaces rejections with Json object containing cause and transaction id. */\ncase class ErrorResponse(error: String, code: TransactionId)\n\nobject ErrorResponse extends Directives with DefaultJsonProtocol {\n\n  def terminate(status: StatusCode, error: String)(implicit transid: TransactionId,\n                                                   jsonPrinter: JsonPrinter): StandardRoute = {\n    terminate(status, Option(error) filter { _.trim.nonEmpty } map { e =>\n      Some(ErrorResponse(e.trim, transid))\n    } getOrElse None)\n  }\n\n  def terminate(status: StatusCode, error: Option[ErrorResponse] = None, asJson: Boolean = true)(\n    implicit transid: TransactionId,\n    jsonPrinter: JsonPrinter): StandardRoute = {\n    val errorResponse = error getOrElse response(status)\n    if (asJson) {\n      complete(status, errorResponse)\n    } else {\n      complete(status, s\"${errorResponse.error} (code: ${errorResponse.code})\")\n    }\n  }\n\n  def response(status: StatusCode)(implicit transid: TransactionId): ErrorResponse = status match {\n    case NotFound  => ErrorResponse(Messages.resourceDoesNotExist, transid)\n    case Forbidden => ErrorResponse(Messages.notAuthorizedtoOperateOnResource, transid)\n    case _         => ErrorResponse(status.defaultMessage, transid)\n  }\n\n  implicit val serializer: RootJsonFormat[ErrorResponse] = new RootJsonFormat[ErrorResponse] {\n    def write(er: ErrorResponse) = JsObject(\"error\" -> er.error.toJson, \"code\" -> er.code.meta.id.toJson)\n\n    def read(v: JsValue) =\n      Try {\n        v.asJsObject.getFields(\"error\", \"code\") match {\n          case Seq(JsString(error), JsString(code)) =>\n            ErrorResponse(error, TransactionId(code))\n          case Seq(JsString(error)) =>\n            ErrorResponse(error, TransactionId.unknown)\n        }\n      } getOrElse deserializationError(\"error response malformed\")\n  }\n\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/http/LenientSprayJsonSupport.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.http\n\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport\nimport org.apache.pekko.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}\nimport spray.json.JsValue\n\nobject LenientSprayJsonSupport extends SprayJsonSupport {\n  // Removes the mandatory application/json content-type for unmarshalling\n  override implicit def sprayJsValueUnmarshaller: FromEntityUnmarshaller[JsValue] =\n    Unmarshaller.byteStringUnmarshaller\n      .andThen(sprayJsValueByteStringUnmarshaller)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/http/PoolingRestClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.http\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.marshalling._\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.settings.ConnectionPoolSettings\nimport org.apache.pekko.http.scaladsl.unmarshalling._\nimport org.apache.pekko.stream.scaladsl.{Flow, _}\nimport org.apache.pekko.stream.{KillSwitches, QueueOfferResult}\nimport org.apache.openwhisk.common.PekkoLogging\nimport spray.json._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future, Promise}\nimport scala.util.{Failure, Success, Try}\n\n/**\n * Http client to talk to a known host.\n *\n * This class only handles the basic communication to the proper endpoints. It is up to its clients to interpret the\n * results. It is built on pekko-http host-level connection pools; compared to single requests, it saves some time\n * on each request because it doesn't need to look up the pool corresponding to the host. It is also easier to add an\n * extra queueing mechanism.\n */\nclass PoolingRestClient(\n  protocol: String,\n  host: String,\n  port: Int,\n  queueSize: Int,\n  httpFlow: Option[Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), Any]] = None,\n  timeout: Option[FiniteDuration] = None)(implicit system: ActorSystem, ec: ExecutionContext) {\n  require(protocol == \"http\" || protocol == \"https\", \"Protocol must be one of { http, https }.\")\n\n  private val logging = new PekkoLogging(system.log)\n\n  //if specified, override the ClientConnection idle-timeout and keepalive socket option value\n  private val timeoutSettings = {\n    val cps = ConnectionPoolSettings(system.settings.config)\n    timeout\n      .map { t =>\n        cps\n          .withMaxConnectionBackoff(cps.maxConnectionBackoff.min(t))\n          .withUpdatedConnectionSettings(_.withIdleTimeout(t))\n      }\n      .getOrElse(cps)\n  }\n\n  // Creates or retrieves a connection pool for the host.\n  private val pool = if (protocol == \"http\") {\n    Http().cachedHostConnectionPool[Promise[HttpResponse]](host = host, port = port, settings = timeoutSettings)\n  } else {\n    Http().cachedHostConnectionPoolHttps[Promise[HttpResponse]](host = host, port = port, settings = timeoutSettings)\n  }\n\n  // Additional queue in case all connections are busy. Should hardly ever be\n  // filled in practice but can be useful, e.g., in tests starting many\n  // asynchronous requests in a very short period of time.\n  private val ((requestQueue, killSwitch), sinkCompletion) = Source\n    .queue(queueSize)\n    .via(httpFlow.getOrElse(pool))\n    .viaMat(KillSwitches.single)(Keep.both)\n    .toMat(Sink.foreach({\n      case (Success(response), p) =>\n        p.success(response)\n      case (Failure(error), p) =>\n        p.failure(error)\n    }))(Keep.both)\n    .run()\n\n  sinkCompletion.onComplete(_ => shutdown())\n\n  /**\n   * Execute an HttpRequest on the underlying connection pool.\n   *\n   * WARNING: It is **very** important that the resulting entity is either drained or discarded fully, so the connection\n   * can be reused. Otherwise, the pool will dry up.\n   *\n   * @return a future holding the response from the server.\n   */\n  def request(futureRequest: Future[HttpRequest]): Future[HttpResponse] = futureRequest.flatMap { request =>\n    val promise = Promise[HttpResponse]\n\n    // When the future completes, we know whether the request made it\n    // through the queue.\n    requestQueue.offer(request -> promise) match {\n      case QueueOfferResult.Enqueued    => promise.future\n      case QueueOfferResult.Dropped     => Future.failed(new Exception(\"Request queue is full.\"))\n      case QueueOfferResult.QueueClosed => Future.failed(new Exception(\"Request queue was closed.\"))\n      case QueueOfferResult.Failure(f)  => Future.failed(f)\n    }\n  }\n\n  /**\n   * Execute an HttpRequest on the underlying connection pool and return an unmarshalled result.\n   *\n   * @return either the unmarshalled result or a status code, if the status code is not a success (2xx class)\n   */\n  def requestJson[T: RootJsonReader](futureRequest: Future[HttpRequest]): Future[Either[StatusCode, T]] =\n    request(futureRequest).flatMap { response =>\n      if (response.status.isSuccess) {\n        Unmarshal(response.entity.withoutSizeLimit).to[T].map(Right.apply)\n      } else {\n        Unmarshal(response.entity).to[String].flatMap { body =>\n          val statusCode = response.status\n          val reason =\n            if (body.nonEmpty) s\"${statusCode.reason} (details: $body)\" else statusCode.reason\n          val customStatusCode = StatusCodes\n            .custom(intValue = statusCode.intValue, reason = reason, defaultMessage = statusCode.defaultMessage)\n          // This is important, as it drains the entity stream.\n          // Otherwise the connection stays open and the pool dries up.\n          response.discardEntityBytes().future.map(_ => Left(customStatusCode))\n        }\n      }\n    }\n\n  def shutdown(): Future[Unit] = {\n    killSwitch.shutdown()\n    Try(requestQueue.complete()).recover {\n      case t: IllegalStateException => logging.warn(this, t.getMessage)\n    }\n    Future.unit\n  }\n}\n\nobject PoolingRestClient {\n\n  def mkRequest(method: HttpMethod,\n                uri: Uri,\n                body: Future[MessageEntity] = Future.successful(HttpEntity.Empty),\n                headers: List[HttpHeader] = List.empty)(implicit ec: ExecutionContext): Future[HttpRequest] = {\n    body.map { b =>\n      HttpRequest(method, uri, headers, b)\n    }\n  }\n\n  def mkJsonRequest(method: HttpMethod, uri: Uri, body: JsValue, headers: List[HttpHeader] = List.empty)(\n    implicit ec: ExecutionContext): Future[HttpRequest] = {\n    val b = Marshal(body).to[MessageEntity]\n    mkRequest(method, uri, b, headers)\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/spi/SpiLoader.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.spi\n\nimport com.typesafe.config.ConfigFactory\n\n/** Marker trait to mark an Spi */\ntrait Spi\n\ntrait SpiClassResolver {\n\n  /** Resolves the implementation for a given type */\n  def getClassNameForType[T: Manifest]: String\n}\n\nobject SpiLoader {\n\n  /**\n   * Instantiates an object of the given type.\n   *\n   * The ClassName to load is resolved via the SpiClassResolver in scode, which defaults to\n   * a TypesafeConfig based resolver.\n   */\n  def get[A <: Spi: Manifest](implicit resolver: SpiClassResolver = TypesafeConfigClassResolver): A = {\n    val clazz = Class.forName(resolver.getClassNameForType[A] + \"$\")\n    clazz.getField(\"MODULE$\").get(clazz).asInstanceOf[A]\n  }\n}\n\n/** Lookup the classname for the SPI impl based on a key in the provided Config */\nobject TypesafeConfigClassResolver extends SpiClassResolver {\n  private val config = ConfigFactory.load()\n\n  override def getClassNameForType[T: Manifest]: String =\n    config.getString(\"whisk.spi.\" + manifest[T].runtimeClass.getSimpleName)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/utils/Exceptions.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.utils\n\nimport org.apache.openwhisk.common.Logging\n\nimport scala.util.control.NonFatal\n\ntrait Exceptions {\n\n  /**\n   * Executes the block, catches and logs a NonFatal exception and swallows it.\n   *\n   * @param task description of the task that's being executed\n   * @param block the block to execute\n   */\n  def tryAndSwallow(task: String)(block: => Any)(implicit logging: Logging): Unit = {\n    try block\n    catch {\n      case NonFatal(t) => logging.error(this, s\"$task failed: $t\")\n    }\n  }\n\n  /**\n   * Executes the block, catches and logs a NonFatal exception and rethrows it.\n   *\n   * @param task description of the task that's being executed\n   * @param block the block to execute\n   */\n  def tryAndThrow[T](task: String)(block: => T)(implicit logging: Logging): T = {\n    try block\n    catch {\n      case NonFatal(t) =>\n        logging.error(this, s\"$task failed: $t\")\n        throw t\n    }\n  }\n\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/utils/ExecutionContextFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.utils\n\nimport java.util.concurrent.Executors\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.concurrent.Promise\nimport scala.concurrent.duration.FiniteDuration\nimport scala.util.control.NonFatal\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.actor.Cancellable\nimport org.apache.pekko.actor.Scheduler\n\nobject ExecutionContextFactory {\n\n  private type CancellableFuture[T] = (Cancellable, Future[T])\n\n  /**\n   * org.apache.pekko.pattern.after has a memory drag issue: it opaquely\n   * schedules an actor which consequently results in drag for the\n   * timeout duration\n   *\n   */\n  def expire[T](duration: FiniteDuration, using: Scheduler)(value: => Future[T])(\n    implicit ec: ExecutionContext): CancellableFuture[T] = {\n    val p = Promise[T]()\n    val cancellable = using.scheduleOnce(duration) {\n      p completeWith {\n        try value\n        catch { case NonFatal(t) => Future.failed(t) }\n      }\n    }\n    (cancellable, p.future)\n  }\n\n  /**\n   * Return the first of the two given futures to complete; if f1\n   * finishes first, we will cancel f2\n   *\n   */\n  def firstCompletedOf2[T](f1: Future[T], f2Cancellable: CancellableFuture[T])(\n    implicit executor: ExecutionContext): Future[T] = {\n    val p = Promise[T]()\n    val (f2Killswitch, f2) = f2Cancellable\n\n    f1.onComplete { result =>\n      p.tryComplete(result)\n      f2Killswitch.cancel()\n    }\n    f2.onComplete(p.tryComplete)\n\n    p.future\n  }\n\n  implicit class FutureExtensions[T](f: Future[T]) {\n    def withTimeout(timeout: FiniteDuration, msg: => Throwable)(implicit system: ActorSystem): Future[T] = {\n      implicit val ec = system.dispatcher\n      firstCompletedOf2(f, expire(timeout, system.scheduler)(Future.failed(msg)))\n    }\n\n    def withAlternativeAfterTimeout(timeout: FiniteDuration, alt: => Future[T])(\n      implicit system: ActorSystem): Future[T] = {\n      implicit val ec = system.dispatcher\n      firstCompletedOf2(f, expire(timeout, system.scheduler)(alt))\n    }\n  }\n\n  /**\n   * Makes an execution context for Futures using Executors.newCachedThreadPool. From the javadoc:\n   *\n   * Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads\n   * when they are available. These pools will typically improve the performance of programs that execute many\n   * short-lived asynchronous tasks. Calls to execute will reuse previously constructed threads if available.\n   * If no existing thread is available, a new thread will be created and added to the pool. Threads that have\n   * not been used for sixty seconds are terminated and removed from the cache. Thus, a pool that remains idle\n   * for long enough will not consume any resources. Note that pools with similar properties but different details\n   * (for example, timeout parameters) may be created using ThreadPoolExecutor constructors.\n   */\n  def makeCachedThreadPoolExecutionContext(): ExecutionContext = {\n    ExecutionContext.fromExecutor(Executors.newCachedThreadPool())\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/utils/JsHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.utils\n\nimport spray.json.JsObject\nimport spray.json.JsValue\n\nobject JsHelpers {\n  def getFieldPath(js: JsObject, path: List[String]): Option[JsValue] = {\n    path match {\n      case Nil      => Option(js)\n      case p :: Nil => js.fields.get(p)\n      case p :: tail =>\n        js.fields.get(p) match {\n          case Some(o: JsObject) => getFieldPath(o, tail)\n          case Some(_)           => None // head exists but value is not an object so cannot project further\n          case None              => None // head doesn't exist, cannot project further\n        }\n    }\n  }\n\n  def getFieldPath(js: JsObject, path: String*): Option[JsValue] = {\n    getFieldPath(js, path.toList)\n  }\n\n  def fieldPathExists(js: JsObject, path: List[String]): Boolean = getFieldPath(js, path).isDefined\n  def fieldPathExists(js: JsObject, path: String*): Boolean = fieldPathExists(js, path.toList)\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/utils/Retry.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.utils\n\nimport scala.concurrent.duration._\n\nobject retry {\n\n  /**\n   * Retries a method which returns a value or throws an exception on failure, up to N times,\n   * and optionally sleeping up to specified duration between retries.\n   *\n   * @param fn the function to retry; fn is expected to throw an exception if it fails, else should return a value of type T\n   * @param N the maximum number of times to apply fn, must be >= 1\n   * @param waitBeforeRetry an option specifying duration to wait before retrying method, will not wait if none given\n   * @param retryMessage an optional message to emit before retrying function\n   * @return the result of fn iff it is successful\n   * @throws Throwable exception from fn (or an illegal argument exception if N is < 1)\n   */\n  def apply[T](fn: => T,\n               N: Int = 3,\n               waitBeforeRetry: Option[Duration] = Some(50.milliseconds),\n               retryMessage: Option[String] = None): T = {\n    require(N >= 1, \"maximum number of fn applications must be greater than 1\")\n\n    try fn\n    catch {\n      case _ if N > 1 =>\n        retryMessage.foreach(println)\n        waitBeforeRetry.foreach(t => Thread.sleep(t.toMillis))\n        retry(fn, N - 1, waitBeforeRetry, retryMessage)\n    }\n  }\n}\n"
  },
  {
    "path": "common/scala/src/main/scala/org/apache/openwhisk/utils/TimeHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.utils\n\nimport java.time.{Duration => JDuration}\n\nimport scala.concurrent.duration.{Duration => SDuration}\nimport scala.language.implicitConversions\n\nobject TimeHelpers {\n  implicit def toJavaDuration(d: SDuration): JDuration = JDuration.ofNanos(d.toNanos)\n  implicit def toScalaDuration(d: JDuration): SDuration = SDuration.fromNanos(d.toNanos)\n}\n"
  },
  {
    "path": "common/scala/transformEnvironment.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# Transforms environment variables starting with `prefix` to kebab-cased JVM system properties\n#\n# \"_\"           becomes \".\"\n# \"camelCased\"  becomes \"camel-cased\"\n# \"PascalCased\" stays   \"PascalCased\" -> classnames stay untouched\n#\n# Examples:\n# CONFIG_whisk_loadbalancer_invokerBusyThreshold -> -Dwhisk.loadbalancer.invoker-busy-threshold\n# CONFIG_pekko_remote_netty_tcp_bindPort          -> -Dpekko.remote.netty.tcp.bind-port\n# CONFIG_whisk_spi_LogStoreProvider              -> -Dwhisk.spi.LogStoreProvider\n#\n\nprefix=\"CONFIG_\"\nconfigVariables=$(compgen -v | grep $prefix)\n\nprops=()\n\nif [ -n \"$OPENWHISK_CONFIG\" ]; then\n    config=\"$OPENWHISK_CONFIG\"\nelif [ -n \"$OPENWHISK_ENCODED_CONFIG\" ]; then\n    config=$(echo \"$OPENWHISK_ENCODED_CONFIG\" | base64 -d)\nfi\n\nif [ -n \"$config\" ]\nthen\n    location=\"$HOME/config.conf\"\n    printf \"%s\" \"$config\" > \"$location\"\n    props+=(\"-Dconfig.file='$location'\")\nfi\n\nfor var in $configVariables\ndo\n    value=$(printenv \"$var\")\n    #allow us to dereference environment variables, e.g. CONFIG_some_key=$SOME_ENV_VAR\n    if [[ $value == \\$* ]] # iff the value starts with $\n    then\n        varname=${value:1} # drop the starting '$'\n        value2=${!varname} # '!' dereferences the variable\n        if [ ! -z \"$value2\" ]\n        then\n            value=$value2 # replace $value with $value2 (the dereferenced value)\n        fi\n    fi\n\n\n    if [ ! -z \"$value\" ]\n    then\n        sansConfig=${var#$prefix} # remove the CONFIG_ prefix\n        parts=${sansConfig//_/ } # \"split\" the name by replacing '_' with ' '\n\n        transformedParts=()\n        for part in $parts\n        do\n            if [[ $part =~ ^[A-Z] ]] # if the current part starts with an uppercase letter (is PascalCased)\n            then\n                transformedParts+=($part) # leave it alone\n            else\n                transformedParts+=($(echo \"$part\" | sed -r 's/([a-z0-9])([A-Z])/\\1-\\L\\2/g')) # rewrite camelCased to kebab-cased\n            fi\n        done\n\n        key=$(IFS=.; echo \"${transformedParts[*]}\") # reassemble the parts delimited by a '.'\n        props+=(\"-D$key='$value'\") # assemble a JVM system property\n    fi\ndone\n\necho \"${props[@]}\"\n"
  },
  {
    "path": "core/controller/.dockerignore",
    "content": "*\n!init.sh\n!build/distributions\n!build/tmp/docker-coverage\n"
  },
  {
    "path": "core/controller/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nARG BASE=scala\nFROM ${BASE}\n\nENV UID=1001 \\\n    NOT_ROOT_USER=owuser\nENV SWAGGER_UI_DOWNLOAD_SHA256=3d7ef5ddc59e10f132fe99771498f0f1ba7a2cbfb9585f9863d4191a574c96e7 \\\n    SWAGGER_UI_VERSION=3.6.0\n\n###################################################################################################\n# It's needed for lean mode where the controller is also an invoker\n###################################################################################################\n# If you change the docker version here, it has implications on invoker runc support.\n# Docker server version and the invoker docker version must be the same to enable runc usage.\n# If this cannot be guaranteed, set `invoker_use_runc: false` in the ansible env.\nENV DOCKER_VERSION=23.0.6\n\n# Uncomment to fetch latest version of docker instead: RUN wget -qO- https://get.docker.com | sh\n# Install docker client\nRUN curl -sSL -o docker-${DOCKER_VERSION}.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz && \\\n    tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/docker && \\\n    tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/runc && \\\n    rm -f docker-${DOCKER_VERSION}.tgz && \\\n    chmod +x /usr/bin/docker && \\\n    chmod +x /usr/bin/runc\n##################################################################################################\n\n# Install swagger-ui\nRUN curl -sSL -o swagger-ui-v${SWAGGER_UI_VERSION}.tar.gz --no-verbose https://github.com/swagger-api/swagger-ui/archive/v${SWAGGER_UI_VERSION}.tar.gz && \\\n    echo \"${SWAGGER_UI_DOWNLOAD_SHA256}  swagger-ui-v${SWAGGER_UI_VERSION}.tar.gz\" | sha256sum -c - && \\\n    mkdir swagger-ui && \\\n    tar zxf swagger-ui-v${SWAGGER_UI_VERSION}.tar.gz -C /swagger-ui --strip-components=2 swagger-ui-${SWAGGER_UI_VERSION}/dist && \\\n    rm swagger-ui-v${SWAGGER_UI_VERSION}.tar.gz && \\\n    sed -i s#http://petstore.swagger.io/v2/swagger.json#/api/v1/api-docs#g /swagger-ui/index.html\n\n# Copy app jars\nADD build/distributions/controller.tar /\n\nCOPY init.sh /\nRUN chmod +x init.sh\n\nRUN useradd -m -u 1001 -d /home/${NOT_ROOT_USER} -s /bin/bash ${NOT_ROOT_USER}\n\n# It is possible to run as non root if you dont need invoker capabilities out of the controller today\n# When running it as a non-root user this has implications on the standard directory where runc stores its data.\n# The non-root user should have access on the directory and corresponding permission to make changes on it.\n#USER ${NOT_ROOT_USER}\n\nEXPOSE 8080\nCMD [\"./init.sh\", \"0\"]\n"
  },
  {
    "path": "core/controller/Dockerfile-debian",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM scala\n\nENV UID=1001 \\\n    NOT_ROOT_USER=owuser\nENV SWAGGER_UI_DOWNLOAD_SHA256=3d7ef5ddc59e10f132fe99771498f0f1ba7a2cbfb9585f9863d4191a574c96e7 \\\n    SWAGGER_UI_VERSION=3.6.0\n\n###################################################################################################\n# It's needed for lean mode where the controller is also an invoker\n###################################################################################################\n# If you change the docker version here, it has implications on invoker runc support.\n# Docker server version and the invoker docker version must be the same to enable runc usage.\n# If this cannot be guaranteed, set `invoker_use_runc: false` in the ansible env.\nENV DOCKER_VERSION=23.0.6\n\nRUN apt-get -y install openssl\n\n# Uncomment to fetch latest version of docker instead: RUN wget -qO- https://get.docker.com | sh\n# Install docker client\nRUN curl -sSL -o docker-${DOCKER_VERSION}.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz && \\\n    tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/docker && \\\n    tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/runc && \\\n    rm -f docker-${DOCKER_VERSION}.tgz && \\\n    chmod +x /usr/bin/docker && \\\n    chmod +x /usr/bin/runc\n##################################################################################################\n\n# Install swagger-ui\nRUN curl -sSL -o swagger-ui-v${SWAGGER_UI_VERSION}.tar.gz --no-verbose https://github.com/swagger-api/swagger-ui/archive/v${SWAGGER_UI_VERSION}.tar.gz && \\\n    echo \"${SWAGGER_UI_DOWNLOAD_SHA256}  swagger-ui-v${SWAGGER_UI_VERSION}.tar.gz\" | sha256sum -c - && \\\n    mkdir swagger-ui && \\\n    tar zxf swagger-ui-v${SWAGGER_UI_VERSION}.tar.gz -C /swagger-ui --strip-components=2 swagger-ui-${SWAGGER_UI_VERSION}/dist && \\\n    rm swagger-ui-v${SWAGGER_UI_VERSION}.tar.gz && \\\n    sed -i s#http://petstore.swagger.io/v2/swagger.json#/api/v1/api-docs#g /swagger-ui/index.html\n\n# Copy app jars\nADD build/distributions/controller.tar /\n\nCOPY init.sh /\nRUN chmod +x init.sh\n\nRUN adduser --disabled-password --disabled-login --gecos '' --uid ${UID} --home /home/${NOT_ROOT_USER} ${NOT_ROOT_USER}\n\n# It is possible to run as non root if you dont need invoker capabilities out of the controller today\n# When running it as a non-root user this has implications on the standard directory where runc stores its data.\n# The non-root user should have access on the directory and corresponding permission to make changes on it.\n#USER ${NOT_ROOT_USER}\n\nEXPOSE 8080\nCMD [\"./init.sh\", \"0\"]\n"
  },
  {
    "path": "core/controller/Dockerfile.cov",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM controller\n\nARG OW_ROOT_DIR\n\nUSER root\nRUN mkdir -p /coverage/common && \\\n    mkdir -p /coverage/controller && \\\n    mkdir -p \"${OW_ROOT_DIR}/common/scala/build\" && \\\n    mkdir -p \"${OW_ROOT_DIR}/core/controller/build\" && \\\n    ln -s /coverage/common \"${OW_ROOT_DIR}/common/scala/build/scoverage\" && \\\n    ln -s /coverage/controller \"${OW_ROOT_DIR}/core/controller/build/scoverage\"\n\nCOPY build/tmp/docker-coverage /controller/\n"
  },
  {
    "path": "core/controller/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'application'\n    id 'eclipse'\n    id 'maven-publish'\n    id 'org.scoverage'\n    id 'scala'\n}\n\next.dockerImageName = 'controller'\napply from: '../../gradle/docker.gradle'\ndistDocker.dependsOn ':common:scala:distDocker', 'distTar'\n\nproject.archivesBaseName = \"openwhisk-controller\"\n\nscoverage {\n    scoverageVersion.set(\"${gradle.scala.scoverageVersion}\")\n    scoverageScalaVersion.set(\"${gradle.scala.scoverageScalaVersion}\")\n}\n\next.coverageDirs = [\n    \"${buildDir}/classes/scala/scoverage\",\n    \"${project(':common:scala').buildDir.absolutePath}/classes/scala/scoverage\"\n]\ndistDockerCoverage.dependsOn ':common:scala:scoverageClasses', 'scoverageClasses'\n\ndependencies {\n    implementation \"org.scala-lang:scala-library:${gradle.scala.version}\"\n    implementation project(':common:scala')\n    implementation project(':core:invoker')\n    implementation project(':core:scheduler')\n\n    implementation \"org.apache.pekko:pekko-management-cluster-bootstrap_${gradle.scala.depVersion}:${gradle.pekko_management.version}\"\n    implementation \"org.apache.pekko:pekko-discovery-kubernetes-api_${gradle.scala.depVersion}:${gradle.pekko_management.version}\"\n    implementation \"org.apache.pekko:pekko-discovery-marathon-api_${gradle.scala.depVersion}:${gradle.pekko_management.version}\"\n}\n\nmainClassName = \"org.apache.openwhisk.core.controller.Controller\"\napplicationDefaultJvmArgs = [\n    \"-Djava.security.egd=file:/dev/./urandom\"\n]\n"
  },
  {
    "path": "core/controller/init.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n./copyJMXFiles.sh\n\nexport CONTROLLER_OPTS\nCONTROLLER_OPTS=\"$CONTROLLER_OPTS -Dpekko.remote.artery.bind.hostname=$(hostname -i) $(./transformEnvironment.sh)\"\n\nexec controller/bin/controller \"$@\"\n"
  },
  {
    "path": "core/controller/src/main/resources/apiv1swagger.json",
    "content": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"OpenWhisk REST API\",\n    \"description\": \"API for OpenWhisk\",\n    \"version\": \"0.1.0\"\n  },\n  \"produces\": [\n    \"application/json\"\n  ],\n  \"basePath\": \"/api/v1\",\n  \"securityDefinitions\": {\n    \"basicAuth\": {\n      \"type\": \"basic\"\n    }\n  },\n  \"security\": [\n    {\n      \"basicAuth\": []\n    }\n  ],\n  \"tags\": [\n    {\n      \"name\": \"Actions\"\n    },\n    {\n      \"name\": \"Rules\"\n    },\n    {\n      \"name\": \"Triggers\"\n    },\n    {\n      \"name\": \"Activations\"\n    },\n    {\n      \"name\": \"Packages\"\n    },\n    {\n      \"name\": \"Namespaces\"\n    },\n    {\n      \"name\": \"Limits\"\n    }\n  ],\n  \"paths\": {\n    \"/namespaces\": {\n      \"get\": {\n        \"tags\": [\n          \"Namespaces\"\n        ],\n        \"description\": \"Get all namespaces for authenticated user\",\n        \"summary\": \"Get all namespaces for authenticated user\",\n        \"operationId\": \"getAllNamespaces\",\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Array of namespaces\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/actions\": {\n      \"get\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"description\": \"Get all actions\",\n        \"summary\": \"Get all actions\",\n        \"operationId\": \"getAllActions\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to include in the result (0-200). The default limit is 30. A value of 0 sets the limit to the maximum.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"skip\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to skip in the result.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Actions response\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/ActionMeta\"\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/actions/{actionName}\": {\n      \"parameters\": [\n        {\n          \"name\": \"namespace\",\n          \"in\": \"path\",\n          \"description\": \"The entity namespace\",\n          \"required\": true,\n          \"type\": \"string\"\n        },\n        {\n          \"name\": \"actionName\",\n          \"in\": \"path\",\n          \"description\": \"Name of action to fetch\",\n          \"required\": true,\n          \"type\": \"string\"\n        }\n      ],\n      \"get\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"summary\": \"Get action information\",\n        \"description\": \"Get action information.\",\n        \"operationId\": \"getActionByName\",\n        \"parameters\": [\n          {\n            \"name\": \"code\",\n            \"in\": \"query\",\n            \"description\": \"Include action code in the result\",\n            \"required\": false,\n            \"type\": \"boolean\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Returned action\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Action\"\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"description\": \"Create or update an action\",\n        \"summary\": \"Create or update an action\",\n        \"operationId\": \"updateAction\",\n        \"parameters\": [\n          {\n            \"name\": \"overwrite\",\n            \"in\": \"query\",\n            \"description\": \"Overwrite item if it exists. Default is false.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          },\n          {\n            \"name\": \"action\",\n            \"in\": \"body\",\n            \"description\": \"The action being updated\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/definitions/ActionPut\"\n            }\n          }\n        ],\n        \"consumes\": [\n          \"application/json\"\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Updated Action\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Action\"\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"409\": {\n            \"$ref\": \"#/responses/Conflict\"\n          },\n          \"413\": {\n            \"$ref\": \"#/responses/RequestEntityTooLarge\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"description\": \"Delete an action\",\n        \"summary\": \"Delete an action\",\n        \"operationId\": \"deleteAction\",\n        \"responses\": {\n          \"200\": {\n            \"$ref\": \"#/responses/DeletedItem\"\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"409\": {\n            \"$ref\": \"#/responses/Conflict\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"description\": \"Invoke an action\",\n        \"summary\": \"Invoke an action\",\n        \"operationId\": \"invokeAction\",\n        \"parameters\": [\n          {\n            \"name\": \"blocking\",\n            \"in\": \"query\",\n            \"description\": \"Blocking or non-blocking invocation. Default is non-blocking.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          },\n          {\n            \"name\": \"result\",\n            \"in\": \"query\",\n            \"description\": \"Return only the result of a blocking activation. Default is false.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          },\n          {\n            \"name\": \"timeout\",\n            \"in\": \"query\",\n            \"description\": \"Wait no more than specified duration in milliseconds for a blocking response. Default value and max allowed timeout are 60000.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"payload\",\n            \"in\": \"body\",\n            \"description\": \"The parameters for the action being invoked\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"object\"\n            }\n          }\n        ],\n        \"consumes\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful activation\"\n          },\n          \"202\": {\n            \"$ref\": \"#/responses/AcceptedActivation\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"408\": {\n            \"$ref\": \"#/responses/Timeout\"\n          },\n          \"429\": {\n            \"$ref\": \"#/responses/TooManyRequests\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          },\n          \"502\": {\n            \"description\": \"Activation produced an application error\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/actions/{packageName}/{actionName}\": {\n      \"parameters\": [\n        {\n          \"name\": \"namespace\",\n          \"in\": \"path\",\n          \"description\": \"The entity namespace\",\n          \"required\": true,\n          \"type\": \"string\"\n        },\n        {\n          \"name\": \"packageName\",\n          \"in\": \"path\",\n          \"description\": \"Name of package that contains action\",\n          \"required\": true,\n          \"type\": \"string\"\n        },\n        {\n          \"name\": \"actionName\",\n          \"in\": \"path\",\n          \"description\": \"Name of action to fetch\",\n          \"required\": true,\n          \"type\": \"string\"\n        }\n      ],\n      \"get\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"summary\": \"Get action information\",\n        \"description\": \"Get action information.\",\n        \"operationId\": \"getActionInPackageByName\",\n        \"parameters\": [\n          {\n            \"name\": \"code\",\n            \"in\": \"query\",\n            \"description\": \"Include action code in the result\",\n            \"required\": false,\n            \"type\": \"boolean\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Returned action\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Action\"\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"description\": \"Create or update an action\",\n        \"summary\": \"Create or update an action\",\n        \"operationId\": \"updateActionInPackage\",\n        \"parameters\": [\n          {\n            \"name\": \"overwrite\",\n            \"in\": \"query\",\n            \"description\": \"Overwrite item if it exists. Default is false.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          },\n          {\n            \"name\": \"action\",\n            \"in\": \"body\",\n            \"description\": \"The action being updated\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/definitions/ActionPut\"\n            }\n          }\n        ],\n        \"consumes\": [\n          \"application/json\"\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Updated Action\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Action\"\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"409\": {\n            \"$ref\": \"#/responses/Conflict\"\n          },\n          \"413\": {\n            \"$ref\": \"#/responses/RequestEntityTooLarge\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"description\": \"Delete an action\",\n        \"summary\": \"Delete an action\",\n        \"operationId\": \"deleteActionInPackage\",\n        \"responses\": {\n          \"200\": {\n            \"$ref\": \"#/responses/DeletedItem\"\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"409\": {\n            \"$ref\": \"#/responses/Conflict\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"description\": \"Invoke an action\",\n        \"summary\": \"Invoke an action\",\n        \"operationId\": \"invokeActionInPackage\",\n        \"parameters\": [\n          {\n            \"name\": \"blocking\",\n            \"in\": \"query\",\n            \"description\": \"Blocking or non-blocking invocation. Default is non-blocking.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          },\n          {\n            \"name\": \"result\",\n            \"in\": \"query\",\n            \"description\": \"Return only the result of a blocking activation. Default is false.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          },\n          {\n            \"name\": \"timeout\",\n            \"in\": \"query\",\n            \"description\": \"Wait no more than specified duration in milliseconds for a blocking response. Default value and max allowed timeout are 60000.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"payload\",\n            \"in\": \"body\",\n            \"description\": \"The parameters for the action being invoked\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"object\"\n            }\n          }\n        ],\n        \"consumes\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful activation\"\n          },\n          \"202\": {\n            \"$ref\": \"#/responses/AcceptedActivation\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"408\": {\n            \"$ref\": \"#/responses/Timeout\"\n          },\n          \"429\": {\n            \"$ref\": \"#/responses/TooManyRequests\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          },\n          \"502\": {\n            \"description\": \"Activation produced an application error\"\n          }\n        }\n      }\n    },\n    \"/web/{namespace}/{packageName}/{actionName}.{extension}\": {\n      \"parameters\": [\n        {\n          \"name\": \"namespace\",\n          \"type\": \"string\",\n          \"in\": \"path\",\n          \"required\": true\n        },\n        {\n          \"name\": \"packageName\",\n          \"type\": \"string\",\n          \"in\": \"path\",\n          \"required\": true\n        },\n        {\n          \"name\": \"actionName\",\n          \"type\": \"string\",\n          \"in\": \"path\",\n          \"required\": true\n        },\n        {\n          \"name\": \"extension\",\n          \"type\": \"string\",\n          \"in\": \"path\",\n          \"required\": true\n        }\n      ],\n      \"get\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"any response\",\n            \"schema\": {}\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"any response\",\n            \"schema\": {}\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"any response\",\n            \"schema\": {}\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\n          \"Actions\"\n        ],\n        \"parameters\": [\n          {\n            \"name\": \"payload\",\n            \"in\": \"body\",\n            \"description\": \"The parameters for the action being invoked\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"object\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"any response\",\n            \"schema\": {}\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/rules\": {\n      \"get\": {\n        \"tags\": [\n          \"Rules\"\n        ],\n        \"description\": \"Get all rules\",\n        \"summary\": \"Get all rules\",\n        \"operationId\": \"getAllRules\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to include in the result (0-200). The default limit is 30. A value of 0 sets the limit to the maximum.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"skip\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to skip in the result.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Rules response\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/RuleMeta\"\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/rules/{ruleName}\": {\n      \"get\": {\n        \"tags\": [\n          \"Rules\"\n        ],\n        \"description\": \"Get rule information\",\n        \"summary\": \"Get rule information\",\n        \"operationId\": \"getRuleByName\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"ruleName\",\n            \"in\": \"path\",\n            \"description\": \"Name of rule to fetch\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Returned rule\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Rule\"\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\n          \"Rules\"\n        ],\n        \"description\": \"Create or update a rule\",\n        \"summary\": \"Create or update a rule\",\n        \"operationId\": \"updateRule\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"ruleName\",\n            \"in\": \"path\",\n            \"description\": \"Name of rule to update\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"overwrite\",\n            \"in\": \"query\",\n            \"description\": \"Overwrite item if it exists. Default is false.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          },\n          {\n            \"name\": \"rule\",\n            \"in\": \"body\",\n            \"description\": \"The rule being updated\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/definitions/RulePut\"\n            }\n          }\n        ],\n        \"consumes\": [\n          \"application/json\"\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Updated rule\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Rule\"\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"409\": {\n            \"$ref\": \"#/responses/Conflict\"\n          },\n          \"413\": {\n            \"$ref\": \"#/responses/RequestEntityTooLarge\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Rules\"\n        ],\n        \"description\": \"Delete a rule\",\n        \"summary\": \"Delete a rule\",\n        \"operationId\": \"deleteRule\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"ruleName\",\n            \"in\": \"path\",\n            \"description\": \"Name of rule to delete\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"$ref\": \"#/responses/DeletedItem\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\n          \"Rules\"\n        ],\n        \"description\": \"Enable or disable a rule\",\n        \"summary\": \"Enable or disable a rule\",\n        \"operationId\": \"setState\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"ruleName\",\n            \"in\": \"path\",\n            \"description\": \"Name of rule to update\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"status\",\n            \"in\": \"body\",\n            \"description\": \"Set status to active or inactive\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"object\",\n              \"required\": [\n                \"status\"\n              ],\n              \"properties\": {\n                \"status\": {\n                  \"type\": \"string\",\n                  \"enum\": [\n                    \"inactive\",\n                    \"active\"\n                  ]\n                }\n              }\n            }\n          }\n        ],\n        \"produces\": [\n          \"application/json\",\n          \"text/plain\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"$ref\": \"#/responses/AcceptedRuleStateChange\"\n          },\n          \"202\": {\n            \"$ref\": \"#/responses/AcceptedRuleStateChange\"\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/triggers\": {\n      \"get\": {\n        \"tags\": [\n          \"Triggers\"\n        ],\n        \"description\": \"Get all triggers\",\n        \"summary\": \"Get all triggers\",\n        \"operationId\": \"getAllTriggers\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to include in the result (0-200). The default limit is 30. A value of 0 sets the limit to the maximum.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"skip\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to skip in the result.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Triggers response\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/TriggerMeta\"\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/triggers/{triggerName}\": {\n      \"get\": {\n        \"tags\": [\n          \"Triggers\"\n        ],\n        \"description\": \"Get trigger information\",\n        \"summary\": \"Get trigger information\",\n        \"operationId\": \"getTriggerByName\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"triggerName\",\n            \"in\": \"path\",\n            \"description\": \"Name of trigger to fetch\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Returned trigger\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Trigger\"\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\n          \"Triggers\"\n        ],\n        \"description\": \"Create or update a trigger\",\n        \"summary\": \"Create or update a trigger\",\n        \"operationId\": \"updateTrigger\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"triggerName\",\n            \"in\": \"path\",\n            \"description\": \"Name of trigger to update\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"overwrite\",\n            \"in\": \"query\",\n            \"description\": \"Overwrite item if it exists. Default is false.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          },\n          {\n            \"name\": \"trigger\",\n            \"in\": \"body\",\n            \"description\": \"The trigger being updated\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/definitions/TriggerPut\"\n            }\n          }\n        ],\n        \"consumes\": [\n          \"application/json\"\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Updated trigger\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Trigger\"\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"409\": {\n            \"$ref\": \"#/responses/Conflict\"\n          },\n          \"413\": {\n            \"$ref\": \"#/responses/RequestEntityTooLarge\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Triggers\"\n        ],\n        \"description\": \"Delete a trigger\",\n        \"summary\": \"Delete a trigger\",\n        \"operationId\": \"deleteTrigger\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"triggerName\",\n            \"in\": \"path\",\n            \"description\": \"Name of trigger to delete\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"$ref\": \"#/responses/DeletedItem\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"post\": {\n        \"tags\": [\n          \"Triggers\"\n        ],\n        \"description\": \"Fire a trigger\",\n        \"summary\": \"Fire a trigger\",\n        \"operationId\": \"fireTrigger\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"triggerName\",\n            \"in\": \"path\",\n            \"description\": \"Name of trigger being fired\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"payload\",\n            \"in\": \"body\",\n            \"description\": \"The trigger payload\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"object\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"202\": {\n            \"description\": \"Activation id\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/ActivationId\"\n            }\n          },\n          \"204\": {\n            \"$ref\": \"#/responses/NoActiveRules\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"408\": {\n            \"$ref\": \"#/responses/Timeout\"\n          },\n          \"429\": {\n            \"$ref\": \"#/responses/TooManyRequests\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/packages\": {\n      \"get\": {\n        \"tags\": [\n          \"Packages\"\n        ],\n        \"description\": \"Get all packages\",\n        \"summary\": \"Get all packages\",\n        \"operationId\": \"getAllPackages\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"public\",\n            \"in\": \"query\",\n            \"description\": \"Include publicly shared entitles in the result.\",\n            \"required\": false,\n            \"type\": \"boolean\"\n          },\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to include in the result (0-200). The default limit is 30. A value of 0 sets the limit to the maximum.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"skip\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to skip in the result.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Packages response\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/Package\"\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/packages/{packageName}\": {\n      \"get\": {\n        \"tags\": [\n          \"Packages\"\n        ],\n        \"summary\": \"Get package information\",\n        \"description\": \"Get package information.\",\n        \"operationId\": \"getPackageByName\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"packageName\",\n            \"in\": \"path\",\n            \"description\": \"Name of package to fetch\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Returned package\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Package\"\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"409\": {\n            \"$ref\": \"#/responses/Conflict\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\n          \"Packages\"\n        ],\n        \"description\": \"Create or update a package\",\n        \"summary\": \"Create or update a package\",\n        \"operationId\": \"updatePackage\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"packageName\",\n            \"in\": \"path\",\n            \"description\": \"Name of package\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"overwrite\",\n            \"in\": \"query\",\n            \"description\": \"Overwrite item if it exists. Default is false.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          },\n          {\n            \"name\": \"package\",\n            \"in\": \"body\",\n            \"description\": \"The package being updated\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/definitions/PackagePut\"\n            }\n          }\n        ],\n        \"consumes\": [\n          \"application/json\"\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Updated Package\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Package\"\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"403\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"409\": {\n            \"$ref\": \"#/responses/Conflict\"\n          },\n          \"413\": {\n            \"$ref\": \"#/responses/RequestEntityTooLarge\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Packages\"\n        ],\n        \"description\": \"Delete a package\",\n        \"summary\": \"Delete a package\",\n        \"operationId\": \"deletePackage\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"packageName\",\n            \"in\": \"path\",\n            \"description\": \"Name of package\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"force\",\n            \"in\": \"query\",\n            \"description\": \"Force delete a package if it contains entities removing everything in it. Default is false.\",\n            \"required\": false,\n            \"type\": \"string\",\n            \"enum\": [\n              \"true\",\n              \"false\"\n            ]\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"$ref\": \"#/responses/DeletedItem\"\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"409\": {\n            \"$ref\": \"#/responses/Conflict\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/activations\": {\n      \"get\": {\n        \"tags\": [\n          \"Activations\"\n        ],\n        \"summary\": \"Get activation summary\",\n        \"description\": \"Get activation summary.\",\n        \"operationId\": \"getActivations\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"name\",\n            \"in\": \"query\",\n            \"description\": \"Name of item\",\n            \"required\": false,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to include in the result (0-200). The default limit is 30. A value of 0 sets the limit to the maximum.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"skip\",\n            \"in\": \"query\",\n            \"description\": \"Number of entities to skip in the result.\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"since\",\n            \"in\": \"query\",\n            \"description\": \"Only include entities later than this timestamp (measured in milliseconds since Thu, 01 Jan 1970)\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"upto\",\n            \"in\": \"query\",\n            \"description\": \"Only include entities earlier than this timestamp (measured in milliseconds since Thu, 01 Jan 1970)\",\n            \"required\": false,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"docs\",\n            \"in\": \"query\",\n            \"description\": \"Whether to include full entity description.\",\n            \"required\": false,\n            \"type\": \"boolean\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Activations response\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/ActivationBrief\"\n              }\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/activations/{activationid}\": {\n      \"get\": {\n        \"tags\": [\n          \"Activations\"\n        ],\n        \"summary\": \"Get activation information\",\n        \"description\": \"Get activation information.\",\n        \"operationId\": \"getActivationById\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"activationid\",\n            \"in\": \"path\",\n            \"description\": \"Name of activation to fetch\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Return output\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Activation\"\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/activations/{activationid}/logs\": {\n      \"get\": {\n        \"tags\": [\n          \"Activations\"\n        ],\n        \"summary\": \"Get the logs for an activation\",\n        \"description\": \"Get activation logs information.\",\n        \"operationId\": \"getActivationLogs\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"activationid\",\n            \"in\": \"path\",\n            \"description\": \"Name of activation to fetch\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Return output\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/ActivationLogs\"\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/activations/{activationid}/result\": {\n      \"get\": {\n        \"tags\": [\n          \"Activations\"\n        ],\n        \"summary\": \"Get the result of an activation\",\n        \"description\": \"Get activation result.\",\n        \"operationId\": \"getActivationResult\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"activationid\",\n            \"in\": \"path\",\n            \"description\": \"Name of activation to fetch\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Return output\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/ActivationResult\"\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"404\": {\n            \"$ref\": \"#/responses/ItemNotFound\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    },\n    \"/namespaces/{namespace}/limits\": {\n      \"get\": {\n        \"tags\": [\n          \"Limits\"\n        ],\n        \"summary\": \"Get the limits for a namespace\",\n        \"description\": \"Get limits.\",\n        \"operationId\": \"getLimits\",\n        \"parameters\": [\n          {\n            \"name\": \"namespace\",\n            \"in\": \"path\",\n            \"description\": \"The entity namespace\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Return output\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/UserLimits\"\n            }\n          },\n          \"401\": {\n            \"$ref\": \"#/responses/UnauthorizedRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/ServerError\"\n          }\n        }\n      }\n    }\n  },\n  \"definitions\": {\n    \"KeyValue\": {\n      \"properties\": {\n        \"key\": {\n          \"type\": \"string\"\n        },\n        \"value\": {\n          \"description\": \"Any JSON value\"\n        }\n      }\n    },\n    \"ItemId\": {\n      \"required\": [\n        \"id\"\n      ],\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"PathName\": {\n      \"required\": [\n        \"path\",\n        \"name\"\n      ],\n      \"properties\": {\n        \"path\": {\n          \"type\": \"string\"\n        },\n        \"name\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"ErrorMessage\": {\n      \"required\": [\n        \"error\"\n      ],\n      \"properties\": {\n        \"error\": {\n          \"type\": \"string\"\n        },\n        \"code\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"ActionLimits\": {\n      \"description\": \"Limits on a specific action\",\n      \"properties\": {\n        \"timeout\": {\n          \"type\": \"integer\",\n          \"format\": \"int32\",\n          \"description\": \"timeout in milliseconds\",\n          \"default\": 60000\n        },\n        \"memory\": {\n          \"type\": \"integer\",\n          \"format\": \"int32\",\n          \"description\": \"memory in megabytes\",\n          \"default\": 256\n        },\n        \"logs\": {\n          \"type\": \"integer\",\n          \"format\": \"int32\",\n          \"description\": \"log size in megabytes\",\n          \"default\": 10\n        },\n        \"concurrency\": {\n          \"type\": \"integer\",\n          \"format\": \"int32\",\n          \"description\": \"number of concurrent activations allowed within an instance\",\n          \"default\": 1\n        },\n        \"instances\": {\n          \"type\": \"integer\",\n          \"format\": \"int32\",\n          \"description\": \"Max number of instances allowed for an action. Must be less than or equal to namespace concurrency limit. Default is the namespace concurrency limit.\"\n        }\n      }\n    },\n    \"EntityBrief\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        }\n      }\n    },\n    \"Action\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\",\n        \"exec\",\n        \"limits\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"exec\": {\n          \"$ref\": \"#/definitions/ActionExec\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"parameters\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"parameter bindings included in the context passed to the action\"\n        },\n        \"limits\": {\n          \"$ref\": \"#/definitions/ActionLimits\"\n        },\n        \"updated\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the action was updated\"\n        }\n      }\n    },\n    \"ActionMeta\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\",\n        \"exec\",\n        \"limits\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"exec\": {\n          \"$ref\": \"#/definitions/ActionExecMeta\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"limits\": {\n          \"$ref\": \"#/definitions/ActionLimits\"\n        },\n        \"updated\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the action was updated\"\n        }\n      }\n    },\n    \"ActionPut\": {\n      \"description\": \"A restricted Action view used when updating an Action\",\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"exec\": {\n          \"$ref\": \"#/definitions/ActionExec\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"delAnnotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"description\": \"The list of annotations to be deleted from the item\"\n        },\n        \"parameters\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"parameter bindings included in the context passed to the action\"\n        },\n        \"limits\": {\n          \"$ref\": \"#/definitions/ActionLimits\"\n        }\n      }\n    },\n    \"ActionExec\": {\n      \"properties\": {\n        \"kind\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"blackbox\",\n            \"java:8\",\n            \"java:default\",\n            \"nodejs:16\",\n            \"nodejs:18\",\n            \"nodejs:20\",\n            \"nodejs:default\",\n            \"php:8.1\",\n            \"php:default\",\n            \"python:3.10\",\n            \"python:3.11\",\n            \"python:default\",\n            \"ruby:2.5\",\n            \"ruby:default\",\n            \"go:1.20\",\n            \"go:default\",\n            \"sequence\",\n            \"swift:5.3\",\n            \"swift:5.7\",\n            \"swift:default\",\n            \"dotnet:3.1\",\n            \"dotnet:6.0\",\n            \"dotnet:default\",\n            \"rust:1.34\",\n            \"rust:default\"\n          ],\n          \"description\": \"the type of action\"\n        },\n        \"code\": {\n          \"type\": \"string\",\n          \"description\": \"The code to execute when kind is not 'blackbox'\"\n        },\n        \"image\": {\n          \"type\": \"string\",\n          \"description\": \"container image name when kind is 'blackbox'\"\n        },\n        \"main\": {\n          \"type\": \"string\",\n          \"description\": \"main entrypoint of the action code\"\n        },\n        \"binary\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether the action has a binary attachment or not. This attribute is ignored when creating or updating an action.\"\n        },\n        \"components\": {\n          \"type\": \"array\",\n          \"description\": \"For sequence actions, the individual action components\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"description\": \"definition of the action, such as javascript code or the name of a container\"\n    },\n    \"ActionExecMeta\": {\n      \"properties\": {\n        \"binary\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether the action has a binary attachment or not. This attribute is ignored when creating or updating an action.\"\n        }\n      },\n      \"description\": \"definition of the action, such as javascript code or the name of a container\"\n    },\n    \"ActionPayload\": {\n      \"required\": [\n        \"payload\"\n      ],\n      \"properties\": {\n        \"payload\": {\n          \"type\": \"string\",\n          \"description\": \"The payload to pass to the action.\"\n        }\n      }\n    },\n    \"Rule\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\",\n        \"trigger\",\n        \"action\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"Status of a rule\",\n          \"enum\": [\n            \"active\",\n            \"inactive\",\n            \"activating\",\n            \"deactivating\"\n          ]\n        },\n        \"updated\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the rule was updated\"\n        },\n        \"trigger\": {\n          \"$ref\": \"#/definitions/PathName\"\n        },\n        \"action\": {\n          \"$ref\": \"#/definitions/PathName\"\n        }\n      }\n    },\n    \"RuleMeta\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\",\n        \"trigger\",\n        \"action\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"updated\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the rule was updated\"\n        },\n        \"trigger\": {\n          \"$ref\": \"#/definitions/PathName\"\n        },\n        \"action\": {\n          \"$ref\": \"#/definitions/PathName\"\n        }\n      }\n    },\n    \"RulePut\": {\n      \"description\": \"A restricted Rule view used when updating a Rule\",\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"Status of a rule\",\n          \"enum\": [\n            \"active\",\n            \"inactive\",\n            \"\"\n          ]\n        },\n        \"trigger\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the trigger\",\n          \"minLength\": 1\n        },\n        \"action\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the action\",\n          \"minLength\": 1\n        }\n      }\n    },\n    \"Trigger\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"parameters\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"parameter bindings for the trigger\"\n        },\n        \"limits\": {\n          \"$ref\": \"#/definitions/TriggerLimits\"\n        },\n        \"rules\": {\n          \"type\": \"object\",\n          \"description\": \"rules associated with the trigger\"\n        },\n        \"updated\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the trigger was updated\"\n        }\n      }\n    },\n    \"TriggerMeta\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"updated\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the trigger was updated\"\n        }\n      }\n    },\n    \"TriggerPut\": {\n      \"description\": \"A restricted Trigger view used when updating the Trigger\",\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"parameters\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"parameter bindings included in the context passed to the trigger\"\n        },\n        \"limits\": {\n          \"$ref\": \"#/definitions/TriggerLimits\"\n        }\n      }\n    },\n    \"TriggerPayload\": {\n      \"required\": [\n        \"payload\"\n      ],\n      \"properties\": {\n        \"payload\": {\n          \"type\": \"string\",\n          \"description\": \"The payload of the trigger event.\"\n        }\n      }\n    },\n    \"TriggerLimits\": {\n      \"description\": \"Limits on a specific trigger\",\n      \"type\": \"object\"\n    },\n    \"Package\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"parameters\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"parameter for the package\"\n        },\n        \"binding\": {\n          \"$ref\": \"#/definitions/PackageBinding\"\n        },\n        \"actions\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/PackageAction\"\n          },\n          \"description\": \"Actions contained in this package\"\n        },\n        \"feeds\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\"\n          },\n          \"description\": \"Feeds contained in this package\"\n        },\n        \"updated\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the package was updated\"\n        }\n      }\n    },\n    \"PackagePut\": {\n      \"description\": \"A restricted Package view used when updating a Package\",\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\",\n          \"minLength\": 1\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"parameters\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"parameter for the package\"\n        },\n        \"binding\": {\n          \"$ref\": \"#/definitions/PackageBinding\"\n        }\n      }\n    },\n    \"PackageBinding\": {\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the item\"\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\"\n        }\n      }\n    },\n    \"PackageAction\": {\n      \"description\": \"A restricted Action view used when listing actions in a package\",\n      \"required\": [\n        \"name\",\n        \"version\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\",\n          \"minLength\": 1\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\",\n          \"minLength\": 1\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"parameters\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"parameter bindings included in the context passed to the action\"\n        }\n      }\n    },\n    \"ActivationBrief\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\",\n        \"activationId\",\n        \"start\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the associated item\"\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\"\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\"\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"activationId\": {\n          \"type\": \"string\",\n          \"description\": \"Id of the activation\"\n        },\n        \"start\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the activation began\"\n        },\n        \"end\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the activation completed\"\n        },\n        \"duration\": {\n          \"type\": \"integer\",\n          \"description\": \"How long the invocation took, in millisecnods\"\n        },\n        \"cause\": {\n          \"type\": \"string\",\n          \"description\": \"the activation id that caused this activation\"\n        },\n        \"statusCode\": {\n          \"type\": \"integer\",\n          \"format\": \"int32\",\n          \"description\": \"The status code\",\n          \"enum\": [\n            0,\n            1,\n            2\n          ]\n        }\n      }\n    },\n    \"Activation\": {\n      \"required\": [\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\",\n        \"subject\",\n        \"activationId\",\n        \"start\",\n        \"response\",\n        \"logs\"\n      ],\n      \"properties\": {\n        \"namespace\": {\n          \"type\": \"string\",\n          \"description\": \"Namespace of the associated item\"\n        },\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the item\"\n        },\n        \"version\": {\n          \"type\": \"string\",\n          \"description\": \"Semantic version of the item\"\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the item or not\"\n        },\n        \"annotations\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"annotations on the item\"\n        },\n        \"subject\": {\n          \"type\": \"string\",\n          \"description\": \"The subject that activated the item\"\n        },\n        \"activationId\": {\n          \"type\": \"string\",\n          \"description\": \"Id of the activation\"\n        },\n        \"start\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the activation began\"\n        },\n        \"end\": {\n          \"type\": \"integer\",\n          \"description\": \"Time when the activation completed\"\n        },\n        \"duration\": {\n          \"type\": \"integer\",\n          \"description\": \"How long the invocation took, in millisecnods\"\n        },\n        \"response\": {\n          \"$ref\": \"#/definitions/ActivationResult\"\n        },\n        \"logs\": {\n          \"type\": \"array\",\n          \"description\": \"Logs generated by the activation\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"cause\": {\n          \"type\": \"string\",\n          \"description\": \"the activation id that caused this activation\"\n        },\n        \"statusCode\": {\n          \"type\": \"integer\",\n          \"format\": \"int32\",\n          \"description\": \"The status code\",\n          \"enum\": [\n            0,\n            1,\n            2\n          ]\n        }\n      }\n    },\n    \"ActivationId\": {\n      \"required\": [\n        \"activationId\"\n      ],\n      \"properties\": {\n        \"activationId\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"ActivationIds\": {\n      \"properties\": {\n        \"ids\": {\n          \"type\": \"array\",\n          \"description\": \"Array of activation ids\",\n          \"items\": {\n            \"$ref\": \"#/definitions/ActivationId\"\n          }\n        }\n      }\n    },\n    \"ActivationInfo\": {\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\",\n          \"description\": \"Activation id\",\n          \"minLength\": 1\n        },\n        \"result\": {\n          \"type\": \"object\",\n          \"description\": \"Activation result\",\n          \"required\": [\n            \"status\"\n          ],\n          \"properties\": {\n            \"status\": {\n              \"type\": \"string\"\n            }\n          }\n        },\n        \"stdout\": {\n          \"type\": \"string\",\n          \"description\": \"Standard output from activation\"\n        },\n        \"stderr\": {\n          \"type\": \"string\",\n          \"description\": \"Standard error from activation\"\n        }\n      }\n    },\n    \"ActivationLogs\": {\n      \"properties\": {\n        \"logs\": {\n          \"type\": \"array\",\n          \"description\": \"Interleaved standard output and error of an activation\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"ActivationStderr\": {\n      \"properties\": {\n        \"stderr\": {\n          \"type\": \"string\",\n          \"description\": \"Standard error of an activation\"\n        }\n      }\n    },\n    \"ActivationResult\": {\n      \"properties\": {\n        \"status\": {\n          \"type\": \"string\",\n          \"description\": \"Exit status of the activation\"\n        },\n        \"result\": {\n          \"description\": \"The return value from the activation\"\n        },\n        \"success\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether the activation was successful or not\"\n        },\n        \"size\" : {\n          \"type\": \"integer\",\n          \"description\": \"Size of response\"\n        }\n      }\n    },\n    \"ProviderTrigger\": {\n      \"required\": [\n        \"name\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the trigger\",\n          \"minLength\": 1\n        }\n      }\n    },\n    \"ProviderAction\": {\n      \"required\": [\n        \"name\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the action\",\n          \"minLength\": 1\n        }\n      }\n    },\n    \"ProviderBinding\": {\n      \"required\": [\n        \"name\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the binding\",\n          \"minLength\": 1\n        }\n      }\n    },\n    \"Provider\": {\n      \"required\": [\n        \"name\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\",\n          \"description\": \"Name of the provider\",\n          \"minLength\": 1\n        },\n        \"publish\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether to publish the provider or not\"\n        },\n        \"parameters\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/KeyValue\"\n          },\n          \"description\": \"parameter bindings included in the context passed to the provider\"\n        }\n      }\n    },\n    \"UserLimits\": {\n      \"properties\": {\n        \"invocationsPerMinute\": {\n          \"type\": \"integer\",\n          \"description\": \"Max allowed invocations per minute for namespace\"\n        },\n        \"concurrentInvocations\": {\n          \"type\": \"integer\",\n          \"description\": \"Max allowed concurrent in flight invocations for namespace\"\n        },\n        \"firesPerMinute\": {\n          \"type\": \"integer\",\n          \"description\": \"Max allowed trigger fires per minute for namespace\"\n        },\n        \"allowedKinds\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"description\": \"List of runtimes whitelisted to be used by namespace (all if none returned)\"\n        },\n        \"storeActivations\": {\n          \"type\": \"boolean\",\n          \"description\": \"Whether storing activation is turned on for namespace (default is true)\"\n        },\n        \"maxParameterSize\": {\n          \"type\": \"string\",\n          \"description\": \"Max parameter size for namespace\"\n        },\n        \"minActionMemory\": {\n          \"type\": \"integer\",\n          \"description\": \"Min allowed action memory size in megabytes for namespace\"\n        },\n        \"maxActionMemory\": {\n          \"type\": \"integer\",\n          \"description\": \"Max allowed action memory size in megabytes for namespace\"\n        },\n        \"minActionTimeout\": {\n          \"type\": \"integer\",\n          \"description\": \"Min allowed action timeout in milliseconds for namespace\"\n        },\n        \"maxActionTimeout\": {\n          \"type\": \"integer\",\n          \"description\": \"Max allowed action timeout in milliseconds for namespace\"\n        },\n        \"minActionLogs\": {\n          \"type\": \"integer\",\n          \"description\": \"Min allowed action log size in megabytes for namespace\"\n        },\n        \"maxActionLogs\": {\n          \"type\": \"integer\",\n          \"description\": \"Max allowed action log size in megabytes for namespace\"\n        },\n        \"minActionConcurrency\": {\n          \"type\": \"integer\",\n          \"description\": \"Min number of concurrent activations within an instance allowed\"\n        },\n        \"maxActionConcurrency\": {\n          \"type\": \"integer\",\n          \"description\": \"Max number of concurrent activations within an instance allowed\"\n        },\n        \"maxActionInstances\": {\n          \"type\": \"integer\",\n          \"description\": \"Max number of concurrent instances allowed for an action\"\n        }\n      }\n    }\n  },\n  \"responses\": {\n    \"BadRequest\": {\n      \"description\": \"Bad request\",\n      \"schema\": {\n        \"$ref\": \"#/definitions/ErrorMessage\"\n      }\n    },\n    \"UnauthorizedRequest\": {\n      \"description\": \"Unauthorized request\",\n      \"schema\": {\n        \"$ref\": \"#/definitions/ErrorMessage\"\n      }\n    },\n    \"ServerError\": {\n      \"description\": \"Server error\",\n      \"schema\": {\n        \"$ref\": \"#/definitions/ErrorMessage\"\n      }\n    },\n    \"AddedItem\": {\n      \"description\": \"Added Item\",\n      \"schema\": {\n        \"$ref\": \"#/definitions/ItemId\"\n      }\n    },\n    \"UpdatedItem\": {\n      \"description\": \"Updated Item\",\n      \"schema\": {\n        \"$ref\": \"#/definitions/ItemId\"\n      }\n    },\n    \"DeletedItem\": {\n      \"description\": \"Deleted Item\"\n    },\n    \"ItemNotFound\": {\n      \"description\": \"Item not found\",\n      \"schema\": {\n        \"$ref\": \"#/definitions/ErrorMessage\"\n      }\n    },\n    \"Timeout\": {\n      \"description\": \"Request timed out\"\n    },\n    \"Conflict\": {\n      \"description\": \"Conflicting item already exists\",\n      \"schema\": {\n        \"$ref\": \"#/definitions/ErrorMessage\"\n      }\n    },\n    \"AcceptedActivation\": {\n      \"description\": \"Accepted activation request\",\n      \"schema\": {\n        \"$ref\": \"#/definitions/ActivationId\"\n      }\n    },\n    \"AcceptedRuleStateChange\": {\n      \"description\": \"Rule has been enabled or disabled\"\n    },\n    \"RequestEntityTooLarge\": {\n      \"description\": \"Request entity too large\",\n      \"schema\": {\n        \"$ref\": \"#/definitions/ErrorMessage\"\n      }\n    },\n    \"NoActiveRules\": {\n      \"description\": \"Trigger has no active rules\"\n    },\n    \"TooManyRequests\": {\n      \"description\": \"Too many requests in a given time period\"\n    }\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/resources/application.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# common logging configuration see common scala\ninclude \"logging\"\ninclude \"pekko-http-version\"\n\n# https://pekko.apache.org/docs/pekko-http/current/configuration.html\n# descriptions inlined below for convenience\npekko.http {\n  server {\n    # Bind to all interfaces in containerized environments\n    bind-host = \"0.0.0.0\"\n    default-http-port = 10001\n\n    # Description:\n    # If a request hasn't been responded to after the time period set here\n    # a `org.apache.pekko.http.Timedout` message will be sent to the timeout handler.\n    # Set to `infinite` to completely disable request timeouts.\n    #\n    # Explaining the set value:\n    # The controller holds connections up to 60s for blocking invokes, and\n    # all other operations are expected to complete quickly. We allow a grace\n    # period in addition to the blocking invoke timeout.\n    request-timeout = 65s\n\n    # The maximum number of concurrently accepted connections when using the\n    # `Http().bindAndHandle` methods.\n    #\n    # This setting doesn't apply to the `Http().bind` method which will still\n    # deliver an unlimited backpressured stream of incoming connections.\n    #\n    # Note, that this setting limits the number of the connections on a best-effort basis.\n    # It does *not* strictly guarantee that the number of established TCP connections will never\n    # exceed the limit (but it will be approximately correct) because connection termination happens\n    # asynchronously. It also does *not* guarantee that the number of concurrently active handler\n    # flow materializations will never exceed the limit for the reason that it is impossible to reliably\n    # detect when a materialization has ended.\n    max-connections = 8192\n\n    # Description:\n    # Enables/disables support for statistics collection and querying.\n    # Even though stats keeping overhead is small,\n    # for maximum performance switch off when not needed.\n    stats-support = off\n\n    # Description:\n    # The time after which an idle connection will be automatically closed.\n    # Set to `infinite` to completely disable idle connection timeouts.\n    #\n    # Explaining the set value:\n    # This must be greater than the request timeout.\n    idle-timeout = 70s\n\n    # Description:\n    # Enables/disables automatic handling of HEAD requests.\n    # If this setting is enabled the server dispatches HEAD requests as GET\n    # requests to the application and automatically strips off all message\n    # bodies from outgoing responses.\n    # Note that, even when this setting is off the server will never send\n    # out message bodies on responses to HEAD requests.\n    # Default value today is on, hence need to explicitly set this to off\n    transparent-head-requests = off\n\n    parsing {\n      # This indirectly puts a bound on the name of entities\n      # 8k matches nginx default\n      max-uri-length = 8k\n\n      # This is 50MB to allow action attachments\n      max-content-length = 50m\n    }\n  }\n}\n\n# Check out all pekko-remote options here:\n# https://pekko.apache.org/docs/pekko/current/general/configuration-reference.html\npekko {\n  java-flight-recorder.enabled = false\n  remote {\n    artery {\n      enabled = on\n      transport = tcp\n    }\n    log-remote-lifecycle-events = DEBUG\n    log-received-messages = on\n    log-sent-messages = on\n  }\n  cluster {\n    # Disable legacy metrics in pekko-cluster.\n    metrics.enabled=off\n  }\n}\n\nssl-config {\n  enabledCipherSuites = [\n    \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\",\n    \"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\",\n    \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n  ]\n  enabledProtocols = [\n    \"TLSv1.2\"\n  ]\n}\n\nwhisk{\n  # tracing configuration\n  tracing {\n    component = \"Controller\"\n  }\n  swagger-ui {\n    file-system : true\n    dir-path : \"/swagger-ui/\"\n  }\n  controller {\n    username: \"controller.user\"\n    password: \"controller.pass\"\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/resources/infoswagger.json",
    "content": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"title\": \"OpenWhisk\",\n    \"description\": \"API for OpenWhisk Configuration\",\n    \"version\": \"0.1.0\"\n  },\n  \"produces\": [\n    \"application/json\"\n  ],\n  \"basePath\": \"/\",\n  \"tags\": [],\n  \"paths\": {\n    \"/\": {\n      \"get\": {\n        \"description\": \"Get configuration parameters for deployment including API paths, runtimes supported, and thorttle limits.\",\n        \"summary\": \"Get configuration parameters for deployment.\",\n        \"produces\": [\n          \"application/json\"\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"API paths, runtimes, limits.\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/Info\"\n            }\n          }\n        }\n      }\n    }\n  },\n  \"definitions\": {\n    \"Info\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"description\": {\n          \"type\": \"string\",\n          \"description\": \"A description of the configuration.\"\n        },\n        \"support\": {\n          \"type\": \"object\",\n          \"description\": \"Where to file defects, feature requests, reach the community for support, etc.\",\n          \"properties\": {\n            \"github\": {\n               \"type\": \"string\"\n            },\n            \"slack\": {\n               \"type\": \"string\"\n            }\n          }\n        },\n        \"api_paths\": {\n          \"type\": \"array\",\n          \"description\": \"Available API paths.\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"limits\": {\n          \"type\": \"object\",\n          \"description\": \"Throttle limits on action and trigger activations.\",\n          \"properties\": {\n            \"actions_per_minute\": {\n              \"type\": \"integer\",\n              \"description\": \"Number of allowed action activations per minute per namespace.\"\n            },\n            \"triggers_per_minute\": {\n              \"type\": \"integer\",\n              \"description\": \"Number of allowed trigger activations per minute per namespace.\"\n            },\n            \"concurrent_actions\": {\n              \"type\": \"integer\",\n              \"description\": \"Number of concurrent activations allowed per namespace at any time.\"\n            }\n          },\n          \"required\": [\n            \"actions_per_minute\",\n            \"triggers_per_minute\",\n            \"concurrent_actions\"\n          ]\n        },\n        \"runtimes\": {\n          \"$ref\": \"#/definitions/RuntimesManifest\"\n        }\n      },\n      \"required\": [\n        \"description\",\n        \"support\",\n        \"api_paths\",\n        \"limits\",\n        \"runtimes\"\n      ]\n    },\n    \"RuntimesManifest\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"family\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"kind\": {\n                \"type\": \"string\",\n                \"description\": \"The action kind, implies a particular image for the action runtime.\"\n              },\n              \"image\": {\n                \"type\": \"string\",\n                \"description\": \"The public container image name.\"\n              },\n              \"default\": {\n                \"type\": \"boolean\",\n                \"description\": \"Indicates if this is the default action 'kind' for a runtime family.\"\n              },\n              \"requireMain\": {\n                \"type\": \"boolean\",\n                \"description\": \"Indicates if a 'main' entry point for the action is required.\"\n              },\n              \"attached\": {\n                \"type\": \"boolean\",\n                \"description\": \"Indicates if the action 'code' is an attachment.\"\n              },\n              \"deprecated\": {\n                \"type\": \"boolean\",\n                \"description\": \"Indicates if runtime is deprecated: may get/delete action but not invoke/create/update.\"\n              }\n            },\n            \"required\": [\n              \"kind\",\n              \"image\",\n              \"default\",\n              \"requireMain\",\n              \"attached\",\n              \"deprecated\"\n            ]\n          }\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "core/controller/src/main/resources/reference.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nwhisk {\n  cluster {\n    use-cluster-bootstrap: false\n  }\n  loadbalancer {\n    managed-fraction: 90%\n    blackbox-fraction: 10%\n    # factor to increase the timeout for forced active acks\n    # timeout = time-limit.std * timeout-factor + timeout-addon\n    # default is 2 because init and run can both use the configured timeout fully\n    timeout-factor = 2\n    # extra time to increase the timeout for forced active acks\n    # default is 1.minute\n    timeout-addon = 1m\n\n    fpc {\n      use-per-min-throttles = true\n    }\n  }\n  controller {\n    protocol: http\n    interface: \"0.0.0.0\"\n    readiness-fraction: 100%\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Actions.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.util.{Failure, Success, Try}\nimport org.apache.kafka.common.errors.RecordTooLargeException\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.server.RequestContext\nimport org.apache.pekko.http.scaladsl.server.RouteResult\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.unmarshalling._\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.{FeatureFlags, WhiskConfig}\nimport org.apache.openwhisk.core.controller.RestApiCommons.{ListLimit, ListSkip}\nimport org.apache.openwhisk.core.controller.actions.PostActionActivation\nimport org.apache.openwhisk.core.database.{ActivationStore, CacheChangeNotification, NoDocumentException}\nimport org.apache.openwhisk.core.entitlement._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types.EntityStore\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.http.Messages._\nimport org.apache.openwhisk.core.entitlement.Resource\nimport org.apache.openwhisk.core.entitlement.Collection\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancerException\nimport pureconfig._\nimport org.apache.openwhisk.core.ConfigKeys\n\n/**\n * A singleton object which defines the properties that must be present in a configuration\n * in order to implement the actions API.\n */\nobject WhiskActionsApi {\n  def requiredProperties = Map(WhiskConfig.actionSequenceMaxLimit -> null)\n\n  /**\n   * Amends annotations on an action create/update with system defined values.\n   * This method currently adds the following annotations:\n   * 1. [[Annotations.ProvideApiKeyAnnotationName]] with the value false iff the annotation is not already defined in the action annotations\n   * 2. An [[execAnnotation]] consistent with the action kind; this annotation is always added and overrides a pre-existing value\n   */\n  protected[core] def amendAnnotations(annotations: Parameters, exec: Exec, create: Boolean = true): Parameters = {\n    val newAnnotations = if (create && FeatureFlags.requireApiKeyAnnotation) {\n      // these annotations are only added on newly created actions\n      // since they can break existing actions created before the\n      // annotation was created\n      annotations\n        .get(Annotations.ProvideApiKeyAnnotationName)\n        .map(_ => annotations)\n        .getOrElse {\n          annotations ++ Parameters(Annotations.ProvideApiKeyAnnotationName, JsFalse)\n        }\n    } else annotations\n    newAnnotations ++ execAnnotation(exec)\n  }\n\n  /**\n   * Constructs an \"exec\" annotation. This is redundant with the exec kind\n   * information available in WhiskAction but necessary for some clients which\n   * fetch action lists but cannot determine action kinds without fetching them.\n   * An alternative is to include the exec in the action list \"view\" but this\n   * will require an API change. So using an annotation instead.\n   */\n  private def execAnnotation(exec: Exec): Parameters = {\n    Parameters(WhiskAction.execFieldName, exec.kind)\n  }\n}\n\n/** A trait implementing the actions API. */\ntrait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with ReferencedEntities {\n  services: WhiskServices =>\n\n  protected override val collection = Collection(Collection.ACTIONS)\n\n  /** An actor system for timed based futures. */\n  protected implicit val actorSystem: ActorSystem\n\n  /** Database service to CRUD actions. */\n  protected val entityStore: EntityStore\n\n  /** Notification service for cache invalidation. */\n  protected implicit val cacheChangeNotification: Some[CacheChangeNotification]\n\n  /** Database service to get activations. */\n  protected val activationStore: ActivationStore\n\n  /** Config flag for Execute Only for Actions in Shared Packages */\n  protected def executeOnly =\n    loadConfigOrThrow[Boolean](ConfigKeys.sharedPackageExecuteOnly)\n\n  /** Entity normalizer to JSON object. */\n  import RestApiCommons.emptyEntityToJsObject\n\n  /** JSON response formatter. */\n  import RestApiCommons.jsonDefaultResponsePrinter\n\n  /**\n   * Handles operations on action resources, which encompass these cases:\n   *\n   * 1. ns/foo     -> subject must be authorized for one of { action(ns, *), action(ns, foo) },\n   *                  resource resolves to { action(ns, foo) }\n   *\n   * 2. ns/bar/foo -> where bar is a package\n   *                  subject must be authorized for one of { package(ns, *), package(ns, bar), action(ns.bar, foo) }\n   *                  resource resolves to { action(ns.bar, foo) }\n   *\n   * 3. ns/baz/foo -> where baz is a binding to ns'.bar\n   *                  subject must be authorized for one of { package(ns, *), package(ns, baz) }\n   *                  *and* one of { package(ns', *), package(ns', bar), action(ns'.bar, foo) }\n   *                  resource resolves to { action(ns'.bar, foo) }\n   *\n   * Note that package(ns, xyz) == action(ns.xyz, *) and if subject has rights to package(ns, xyz)\n   * then they also have rights to action(ns.xyz, *) since sharing is done at the package level and\n   * is not more granular; hence a check on action(ns.xyz, abc) is eschewed.\n   *\n   * Only list is supported for these resources:\n   *\n   * 4. ns/bar/    -> where bar is a package\n   *                  subject must be authorized for one of { package(ns, *), package(ns, bar) }\n   *                  resource resolves to { action(ns.bar, *) }\n   *\n   * 5. ns/baz/    -> where baz is a binding to ns'.bar\n   *                  subject must be authorized for one of { package(ns, *), package(ns, baz) }\n   *                  *and* one of { package(ns', *), package(ns', bar) }\n   *                  resource resolves to { action(ns.bar, *) }\n   */\n  protected override def innerRoutes(user: Identity, ns: EntityPath)(implicit transid: TransactionId) = {\n    (entityPrefix & entityOps & requestMethod) { (segment, m) =>\n      entityname(segment) { outername =>\n        pathEnd {\n          // matched /namespace/collection/name\n          // this is an action in default package, authorize and dispatch\n          authorizeAndDispatch(m, user, Resource(ns, collection, Some(outername)))\n        } ~ (get & pathSingleSlash) {\n          // matched GET /namespace/collection/package-name/\n          // list all actions in package iff subject is entitled to READ package\n          val resource = Resource(ns, Collection(Collection.PACKAGES), Some(outername))\n          onComplete(entitlementProvider.check(user, Privilege.READ, resource)) {\n            case Success(_) => listPackageActions(user, FullyQualifiedEntityName(ns, EntityName(outername)))\n            case Failure(f) => super.handleEntitlementFailure(f)\n          }\n        } ~ (entityPrefix & pathEnd) { segment =>\n          entityname(segment) { innername =>\n            // matched /namespace/collection/package-name/action-name\n            // this is an action in a named package\n            val packageDocId = FullyQualifiedEntityName(ns, EntityName(outername)).toDocId\n            val packageResource = Resource(ns.addPath(EntityName(outername)), collection, Some(innername))\n\n            val right = collection.determineRight(m, Some(innername))\n            onComplete(entitlementProvider.check(user, right, packageResource)) {\n              case Success(_) =>\n                getEntity(WhiskPackage.get(entityStore, packageDocId), Some {\n                  if (right == Privilege.READ || right == Privilege.ACTIVATE) { wp: WhiskPackage =>\n                    val actionResource = Resource(wp.fullPath, collection, Some(innername))\n                    dispatchOp(user, right, actionResource)\n\n                  } else {\n                    // these packaged action operations do not need merging with the package,\n                    // but may not be permitted if this is a binding, or if the subject does\n                    // not have PUT and DELETE rights to the package itself\n                    (wp: WhiskPackage) =>\n                      wp.binding map { _ =>\n                        terminate(BadRequest, Messages.notAllowedOnBinding)\n                      } getOrElse {\n                        val actionResource = Resource(wp.fullPath, collection, Some(innername))\n                        dispatchOp(user, right, actionResource)\n                      }\n                  }\n                })\n              case Failure(f) => super.handleEntitlementFailure(f)\n            }\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * Creates or updates action if it already exists. The PUT content is deserialized into a WhiskActionPut\n   * which is a subset of WhiskAction (it eschews the namespace and entity name since the former is derived\n   * from the authenticated user and the latter is derived from the URI). The WhiskActionPut is merged with\n   * the existing WhiskAction in the datastore, overriding old values with new values that are defined.\n   * Any values not defined in the PUT content are replaced with old values.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskAction as JSON\n   * - 400 Bad Request\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n    parameter('overwrite ? false) { overwrite =>\n      entity(as[WhiskActionPut]) { content =>\n        val request = content.resolve(user.namespace)\n        val check = for {\n          checkLimits <- checkActionLimits(user, content)\n          checkAdditionalPrivileges <- entitleReferencedEntities(user, Privilege.READ, request.exec).flatMap(_ =>\n            entitlementProvider.check(user, content.exec))\n        } yield (checkAdditionalPrivileges, checkLimits)\n\n        onComplete(check) {\n          case Success(_) =>\n            putEntity(WhiskAction, entityStore, entityName.toDocId, overwrite, update(user, request), () => {\n              make(user, entityName, request)\n            })\n          case Failure(f) =>\n            super.handleEntitlementFailure(f)\n        }\n      }\n    }\n  }\n\n  /**\n   * Invokes action if it exists. The POST content is deserialized into a Payload and posted\n   * to the loadbalancer.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 Activation as JSON if blocking or just the result JSON iff '&result=true'\n   * - 202 ActivationId as JSON (this is issued on non-blocking activation or blocking activation that times out)\n   * - 404 Not Found\n   * - 502 Bad Gateway\n   * - 500 Internal Server Error\n   */\n  override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId) = {\n    parameter(\n      'blocking ? false,\n      'result ? false,\n      'timeout.as[FiniteDuration] ? controllerActivationConfig.maxWaitForBlockingActivation) {\n      (blocking, result, waitOverride) =>\n        entity(as[Option[JsObject]]) { payload =>\n          getEntity(WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, entityName), Some {\n            act: WhiskActionMetaData =>\n              // resolve the action --- special case for sequences that may contain components with '_' as default package\n              val action = act.resolve(user.namespace)\n              onComplete(entitleReferencedEntitiesMetaData(user, Privilege.ACTIVATE, Some(action.exec))) {\n                case Success(_) =>\n                  val actionWithMergedParams = env.map(action.inherit(_)) getOrElse action\n\n                  // incoming parameters may not override final parameters (i.e., parameters with already defined values)\n                  // on an action once its parameters are resolved across package and binding\n                  val allowInvoke = payload\n                    .map(_.fields.keySet.forall(key => !actionWithMergedParams.immutableParameters.contains(key)))\n                    .getOrElse(true)\n\n                  if (allowInvoke) {\n                    doInvoke(user, actionWithMergedParams, payload, blocking, waitOverride, result)\n                  } else {\n                    terminate(BadRequest, Messages.parametersNotAllowed)\n                  }\n\n                case Failure(f) =>\n                  super.handleEntitlementFailure(f)\n              }\n          })\n        }\n    }\n  }\n\n  private def doInvoke(user: Identity,\n                       actionWithMergedParams: WhiskActionMetaData,\n                       payload: Option[JsObject],\n                       blocking: Boolean,\n                       waitOverride: FiniteDuration,\n                       result: Boolean)(implicit transid: TransactionId): RequestContext => Future[RouteResult] = {\n    val waitForResponse = if (blocking) Some(waitOverride) else None\n    onComplete(invokeAction(user, actionWithMergedParams, payload, waitForResponse, cause = None)) {\n      case Success(Left(activationId)) =>\n        // non-blocking invoke or blocking invoke which got queued instead\n        respondWithActivationIdHeader(activationId) {\n          complete(Accepted, activationId.toJsObject)\n        }\n      case Success(Right(activation)) =>\n        val response = activation.response.result match {\n          case Some(JsArray(elements)) =>\n            JsArray(elements)\n          case _ =>\n            if (result) activation.resultAsJson else activation.toExtendedJson()\n        }\n        respondWithActivationIdHeader(activation.activationId) {\n          if (activation.response.isSuccess) {\n            complete(OK, response)\n          } else if (activation.response.isApplicationError) {\n            // actions that result is ApplicationError status are considered a 'success'\n            // and will have an 'error' property in the result - the HTTP status is OK\n            // and clients must check the response status if it exists\n            // NOTE: response status will not exist in the JSON object if ?result == true\n            // and instead clients must check if 'error' is in the JSON\n            // PRESERVING OLD BEHAVIOR and will address defect in separate change\n            complete(BadGateway, response)\n          } else if (activation.response.isContainerError) {\n            complete(BadGateway, response)\n          } else {\n            complete(InternalServerError, response)\n          }\n        }\n      case Failure(t: RecordTooLargeException) =>\n        logging.debug(this, s\"[POST] action payload was too large\")\n        terminate(ContentTooLarge)\n      case Failure(RejectRequest(code, message)) =>\n        logging.debug(this, s\"[POST] action rejected with code $code: $message\")\n        terminate(code, message)\n      case Failure(t: LoadBalancerException) =>\n        logging.error(this, s\"[POST] failed in loadbalancer: ${t.getMessage}\")\n        terminate(ServiceUnavailable)\n      case Failure(t: Throwable) =>\n        logging.error(this, s\"[POST] action activation failed: ${t.getMessage}\")\n        terminate(InternalServerError)\n    }\n  }\n\n  /**\n   * Deletes action.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskAction as JSON\n   * - 404 Not Found\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n    deleteEntity(WhiskAction, entityStore, entityName.toDocId, (a: WhiskAction) => Future.successful({}))\n  }\n\n  /** Checks for package binding case. we don't want to allow get for a package binding in shared package */\n  private def fetchEntity(entityName: FullyQualifiedEntityName, env: Option[Parameters], code: Boolean)(\n    implicit transid: TransactionId) = {\n    val resolvedPkg: Future[Either[String, FullyQualifiedEntityName]] = if (entityName.path.defaultPackage) {\n      Future.successful(Right(entityName))\n    } else {\n      WhiskPackage.resolveBinding(entityStore, entityName.path.toDocId, mergeParameters = true).map { pkg =>\n        val originalPackageLocation = pkg.fullyQualifiedName(withVersion = false).namespace\n        if (executeOnly && originalPackageLocation != entityName.namespace) {\n          Left(forbiddenGetActionBinding(entityName.toDocId.asString))\n        } else {\n          Right(entityName)\n        }\n      }\n    }\n    onComplete(resolvedPkg) {\n      case Success(pkgFuture) =>\n        pkgFuture match {\n          case Left(f) => terminate(Forbidden, f)\n          case Right(_) =>\n            if (code) {\n              getEntity(WhiskAction.resolveActionAndMergeParameters(entityStore, entityName), Some {\n                action: WhiskAction =>\n                  val mergedAction = env map {\n                    action inherit _\n                  } getOrElse action\n                  complete(OK, mergedAction)\n              })\n            } else {\n              getEntity(WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, entityName), Some {\n                action: WhiskActionMetaData =>\n                  val mergedAction = env map {\n                    action inherit _\n                  } getOrElse action\n                  complete(OK, mergedAction)\n              })\n            }\n        }\n      case Failure(t: Throwable) =>\n        logging.error(this, s\"[GET] package ${entityName.path.toDocId} failed: ${t.getMessage}\")\n        terminate(InternalServerError)\n    }\n  }\n\n  /**\n   * Gets action. The action name is prefixed with the namespace to create the primary index key.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskAction has JSON\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId) = {\n    parameter('code ? true) { code =>\n      //check if execute only is enabled, and if there is a discrepancy between the current user's namespace\n      //and that of the entity we are trying to fetch\n      if (executeOnly && user.namespace.name != entityName.namespace) {\n        terminate(Forbidden, forbiddenGetAction(entityName.path.asString))\n      } else {\n        fetchEntity(entityName, env, code)\n      }\n    }\n  }\n\n  /**\n   * Gets all actions in a path.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 [] or [WhiskAction as JSON]\n   * - 500 Internal Server Error\n   */\n  override def list(user: Identity, namespace: EntityPath)(implicit transid: TransactionId) = {\n    parameter(\n      'skip.as[ListSkip] ? ListSkip(collection.defaultListSkip),\n      'limit.as[ListLimit] ? ListLimit(collection.defaultListLimit),\n      'count ? false) { (skip, limit, count) =>\n      if (!count) {\n        listEntities {\n          WhiskAction.listCollectionInNamespace(entityStore, namespace, skip.n, limit.n, includeDocs = false) map {\n            list =>\n              list.fold((js) => js, (as) => as.map(WhiskAction.serdes.write(_)))\n          }\n        }\n      } else {\n        countEntities {\n          WhiskAction.countCollectionInNamespace(entityStore, namespace, skip.n)\n        }\n      }\n    }\n  }\n\n  /** Replaces default namespaces in a vector of components from a sequence with appropriate namespace. */\n  private def resolveDefaultNamespace(components: Vector[FullyQualifiedEntityName],\n                                      user: Identity): Vector[FullyQualifiedEntityName] = {\n    // if components are part of the default namespace, they contain `_`; replace it!\n    val resolvedComponents = components map { c =>\n      FullyQualifiedEntityName(c.path.resolveNamespace(user.namespace), c.name)\n    }\n    resolvedComponents\n  }\n\n  /**\n   * Creates a WhiskAction instance from the PUT request.\n   */\n  private def makeWhiskAction(content: WhiskActionPut, entityName: FullyQualifiedEntityName)(\n    implicit transid: TransactionId) = {\n    val exec = content.exec.get\n    val limits = content.limits map { l =>\n      ActionLimits(\n        l.timeout getOrElse TimeLimit(),\n        l.memory getOrElse MemoryLimit(),\n        l.logs getOrElse LogLimit(),\n        l.concurrency getOrElse IntraConcurrencyLimit(),\n        l.instances)\n    } getOrElse ActionLimits()\n    // This is temporary while we are making sequencing directly supported in the controller.\n    // The parameter override allows this to work with Pipecode.code. Any parameters other\n    // than the action sequence itself are discarded and have no effect.\n    // Note: While changing the implementation of sequences, components now store the fully qualified entity names\n    // (which loses the leading \"/\"). Adding it back while both versions of the code are in place.\n    val parameters = exec match {\n      case seq: SequenceExec =>\n        Parameters(\"_actions\", JsArray(seq.components map { _.qualifiedNameWithLeadingSlash.toJson }))\n      case _ => content.parameters getOrElse Parameters()\n    }\n\n    WhiskAction(\n      entityName.path,\n      entityName.name,\n      exec,\n      parameters,\n      limits,\n      content.version getOrElse SemVer(),\n      content.publish getOrElse false,\n      WhiskActionsApi.amendAnnotations(content.annotations getOrElse Parameters(), exec))\n  }\n\n  /** For a sequence action, gather referenced entities and authorize access. */\n  private def entitleReferencedEntities(user: Identity, right: Privilege, exec: Option[Exec])(\n    implicit transid: TransactionId) = {\n    exec match {\n      case Some(seq: SequenceExec) =>\n        logging.debug(this, \"checking if sequence components are accessible\")\n        entitlementProvider.check(user, right, referencedEntities(seq), noThrottle = true)\n      case _ => Future.successful(true)\n    }\n  }\n\n  private def entitleReferencedEntitiesMetaData(user: Identity, right: Privilege, exec: Option[ExecMetaDataBase])(\n    implicit transid: TransactionId) = {\n    exec match {\n      case Some(seq: SequenceExecMetaData) =>\n        logging.info(this, \"checking if sequence components are accessible\")\n        entitlementProvider.check(user, right, referencedEntities(seq), noThrottle = true)\n      case _ => Future.successful(true)\n    }\n  }\n\n  /** Creates a WhiskAction from PUT content, generating default values where necessary. */\n  private def make(user: Identity, entityName: FullyQualifiedEntityName, content: WhiskActionPut)(\n    implicit transid: TransactionId) = {\n    checkInstanceConcurrencyLessThanNamespaceConcurrency(user, content) flatMap { _ =>\n      content.exec map {\n        case seq: SequenceExec =>\n          // check that the sequence conforms to max length and no recursion rules\n          checkSequenceActionLimits(entityName, seq.components) map { _ =>\n            makeWhiskAction(content.replace(seq), entityName)\n          }\n        case supportedExec if !supportedExec.deprecated =>\n          Future successful makeWhiskAction(content, entityName)\n        case deprecatedExec =>\n          Future failed RejectRequest(BadRequest, runtimeDeprecated(deprecatedExec))\n\n      } getOrElse Future.failed(RejectRequest(BadRequest, \"exec undefined\"))\n    }\n  }\n\n  /** Updates a WhiskAction from PUT content, merging old action where necessary. */\n  private def update(user: Identity, content: WhiskActionPut)(action: WhiskAction)(implicit transid: TransactionId) = {\n    checkInstanceConcurrencyLessThanNamespaceConcurrency(user, content) flatMap { _ =>\n      content.exec map {\n        case seq: SequenceExec =>\n          // check that the sequence conforms to max length and no recursion rules\n          checkSequenceActionLimits(FullyQualifiedEntityName(action.namespace, action.name), seq.components) map { _ =>\n            updateWhiskAction(content.replace(seq), action)\n          }\n        case supportedExec if !supportedExec.deprecated =>\n          Future successful updateWhiskAction(content, action)\n        case deprecatedExec =>\n          Future failed RejectRequest(BadRequest, runtimeDeprecated(deprecatedExec))\n      } getOrElse {\n        if (!action.exec.deprecated) {\n          Future successful updateWhiskAction(content, action)\n        } else {\n          Future failed RejectRequest(BadRequest, runtimeDeprecated(action.exec))\n        }\n      }\n    }\n  }\n\n  /**\n   * Updates a WhiskAction instance from the PUT request.\n   */\n  private def updateWhiskAction(content: WhiskActionPut, action: WhiskAction)(implicit transid: TransactionId) = {\n    val limits = content.limits map { l =>\n      ActionLimits(\n        l.timeout getOrElse action.limits.timeout,\n        l.memory getOrElse action.limits.memory,\n        l.logs getOrElse action.limits.logs,\n        l.concurrency getOrElse action.limits.concurrency,\n        if (l.instances.isDefined) l.instances else action.limits.instances)\n    } getOrElse action.limits\n\n    // This is temporary while we are making sequencing directly supported in the controller.\n    // Actions that are updated with a sequence will have their parameter property overridden.\n    // Actions that are updated with non-sequence actions will either set the parameter property according to\n    // the content provided, or if that is not defined, and iff the previous version of the action was not a\n    // sequence, inherit previous parameters. This is because sequence parameters are special and should not\n    // leak to non-sequence actions.\n    // If updating an action but not specifying a new exec type, then preserve the previous parameters if the\n    // existing type of the action is a sequence (regardless of what parameters may be defined in the content)\n    // otherwise, parameters are inferred from the content or previous values.\n    // Note: While changing the implementation of sequences, components now store the fully qualified entity names\n    // (which loses the leading \"/\"). Adding it back while both versions of the code are in place. This will disappear completely\n    // once the version of sequences with \"pipe.js\" is removed.\n    val parameters = content.exec map {\n      case seq: SequenceExec =>\n        Parameters(\"_actions\", JsArray(seq.components map { c =>\n          JsString(\"/\" + c.toString)\n        }))\n      case _ =>\n        content.parameters getOrElse {\n          action.exec match {\n            case seq: SequenceExec => Parameters()\n            case _                 => action.parameters\n          }\n        }\n    } getOrElse {\n      action.exec match {\n        case seq: SequenceExec => action.parameters // discard content.parameters\n        case _                 => content.parameters getOrElse action.parameters\n      }\n    }\n\n    val exec = content.exec getOrElse action.exec\n\n    val newAnnotations = content.delAnnotations\n      .map { annotationArray =>\n        annotationArray.foldRight(action.annotations)((a: String, b: Parameters) => b - a)\n      }\n      .map(_ ++ content.annotations)\n      .getOrElse(action.annotations ++ content.annotations)\n\n    WhiskAction(\n      action.namespace,\n      action.name,\n      exec,\n      parameters,\n      limits,\n      content.version getOrElse action.version.upPatch,\n      content.publish getOrElse action.publish,\n      WhiskActionsApi.amendAnnotations(newAnnotations, exec, create = false))\n      .revision[WhiskAction](action.docinfo.rev)\n  }\n\n  /**\n   * Lists actions in package or binding. The router authorized the subject for the package\n   * (if binding, then authorized subject for both the binding and the references package)\n   * and iff authorized, this method is reached to lists actions.\n   *\n   * Note that when listing actions in a binding, the namespace on the actions will be that\n   * of the referenced packaged, not the binding.\n   */\n  private def listPackageActions(user: Identity, pkgName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n    // get the package to determine if it is a package or reference\n    // (this will set the appropriate namespace), and then list actions\n    // NOTE: these fetches are redundant with those from the authorization\n    // and should hit the cache to ameliorate the cost; this can be improved\n    // but requires communicating back from the authorization service the\n    // resolved namespace\n    getEntity(WhiskPackage.get(entityStore, pkgName.toDocId), Some { (wp: WhiskPackage) =>\n      val pkgns = wp.binding map { b =>\n        logging.debug(this, s\"list actions in package binding '${wp.name}' -> '$b'\")\n        b.namespace.addPath(b.name)\n      } getOrElse {\n        logging.debug(this, s\"list actions in package '${wp.name}'\")\n        pkgName.path.addPath(wp.name)\n      }\n      // list actions in resolved namespace\n      list(user, pkgns)\n    })\n  }\n\n  private def checkActionLimits(user: Identity, content: WhiskActionPut)(\n    implicit transid: TransactionId): Future[Unit] = {\n    logging.debug(this, \"checking the namespace and system limit for action\")\n    try {\n      // check namespace limits\n      content.limits foreach { limit =>\n        limit.memory foreach (_.checkNamespaceLimit(user))\n        limit.timeout foreach (_.checkNamespaceLimit(user))\n        limit.logs foreach (_.checkNamespaceLimit(user))\n        limit.concurrency foreach (_.checkNamespaceLimit(user))\n      }\n      Future.successful(())\n    } catch {\n      case e: ActionLimitsException => Future failed RejectRequest(BadRequest, e.getMessage)\n    }\n  }\n\n  /**\n   * Checks that the sequence is not cyclic and that the number of atomic actions in the \"inlined\" sequence is lower than max allowed.\n   *\n   * @param sequenceAction is the action sequence to check\n   * @param components the components of the sequence\n   */\n  private def checkSequenceActionLimits(\n    sequenceAction: FullyQualifiedEntityName,\n    components: Vector[FullyQualifiedEntityName])(implicit transid: TransactionId): Future[Unit] = {\n    // first checks that current sequence length is allowed\n    // then traverses all actions in the sequence, inlining any that are sequences\n    val future = if (components.size > actionSequenceLimit) {\n      Future.failed(TooManyActionsInSequence())\n    } else if (components.size == 0) {\n      Future.failed(NoComponentInSequence())\n    } else {\n      // resolve the action document id (if it's in a package/binding);\n      // this assumes that entityStore is the same for actions and packages\n      WhiskAction.resolveAction(entityStore, sequenceAction) flatMap { resolvedSeq =>\n        val atomicActionCnt = countAtomicActionsAndCheckCycle(resolvedSeq, components)\n        atomicActionCnt map { count =>\n          logging.debug(this, s\"sequence '$sequenceAction' atomic action count $count\")\n          if (count > actionSequenceLimit) {\n            throw TooManyActionsInSequence()\n          }\n        }\n      }\n    }\n\n    future recoverWith {\n      case _: TooManyActionsInSequence => Future failed RejectRequest(BadRequest, sequenceIsTooLong)\n      case _: NoComponentInSequence    => Future failed RejectRequest(BadRequest, sequenceNoComponent)\n      case _: SequenceWithCycle        => Future failed RejectRequest(BadRequest, sequenceIsCyclic)\n      case _: NoDocumentException      => Future failed RejectRequest(BadRequest, sequenceComponentNotFound)\n    }\n  }\n\n  private def checkInstanceConcurrencyLessThanNamespaceConcurrency(user: Identity, content: WhiskActionPut)(\n    implicit transid: TransactionId): Future[Unit] = {\n    val namespaceConcurrencyLimit =\n      user.limits.concurrentInvocations.getOrElse(whiskConfig.actionInvokeConcurrentLimit.toInt)\n    content.limits\n      .map(\n        l =>\n          l.instances\n            .map(\n              m =>\n                if (m.maxConcurrentInstances > namespaceConcurrencyLimit)\n                  Future failed RejectRequest(\n                    BadRequest,\n                    maxActionInstanceConcurrencyExceedsNamespace(namespaceConcurrencyLimit))\n                else Future.successful({}))\n            .getOrElse(Future.successful({})))\n      .getOrElse(Future.successful({}))\n  }\n\n  /**\n   * Counts the number of atomic actions in a sequence and checks for potential cycles. The latter is done\n   * by inlining any sequence components that are themselves sequences and checking if there if a reference to\n   * the given original sequence.\n   *\n   * @param origSequence the original sequence that is updated/created which generated the checks\n   * @param components the components of the a sequence to check if they reference the original sequence\n   * @return Future with the number of atomic actions in the current sequence or an appropriate error if there is a cycle or a non-existent action reference\n   */\n  private def countAtomicActionsAndCheckCycle(\n    origSequence: FullyQualifiedEntityName,\n    components: Vector[FullyQualifiedEntityName])(implicit transid: TransactionId): Future[Int] = {\n    if (components.size > actionSequenceLimit) {\n      Future.failed(TooManyActionsInSequence())\n    } else {\n      // resolve components wrt any package bindings\n      val resolvedComponentsFutures = components map { c =>\n        WhiskAction.resolveAction(entityStore, c)\n      }\n      // traverse the sequence structure by checking each of its components and do the following:\n      // 1. check whether any action (sequence or not) referred by the sequence (directly or indirectly)\n      //    is the same as the original sequence (aka origSequence)\n      // 2. count the atomic actions each component has (by \"inlining\" all sequences)\n      val actionCountsFutures = resolvedComponentsFutures map {\n        _ flatMap { resolvedComponent =>\n          // check whether this component is the same as origSequence\n          // this can happen when updating an atomic action to become a sequence\n          if (origSequence == resolvedComponent) {\n            Future failed SequenceWithCycle()\n          } else {\n            // check whether component is a sequence or an atomic action\n            // if the component does not exist, the future will fail with appropriate error\n            WhiskAction.get(entityStore, resolvedComponent.toDocId) flatMap { wskComponent =>\n              wskComponent.exec match {\n                case SequenceExec(seqComponents) =>\n                  // sequence action, count the number of atomic actions in this sequence\n                  countAtomicActionsAndCheckCycle(origSequence, seqComponents)\n                case _ => Future successful 1 // atomic action count is one\n              }\n            }\n          }\n        }\n      }\n      // collapse the futures in one future\n      val actionCountsFuture = Future.sequence(actionCountsFutures)\n      // sum up all individual action counts per component\n      val totalActionCount = actionCountsFuture map { actionCounts =>\n        actionCounts.foldLeft(0)(_ + _)\n      }\n      totalActionCount\n    }\n  }\n\n  /** Max atomic action count allowed for sequences. */\n  private lazy val actionSequenceLimit = whiskConfig.actionSequenceLimit.toInt\n\n  implicit val stringToFiniteDuration: Unmarshaller[String, FiniteDuration] = {\n    Unmarshaller.strict[String, FiniteDuration] { value =>\n      val max = controllerActivationConfig.maxWaitForBlockingActivation.toMillis\n\n      Try { value.toInt } match {\n        case Success(i) if i > 0 && i <= max => i.milliseconds\n        case _ =>\n          throw new IllegalArgumentException(\n            Messages.invalidTimeout(controllerActivationConfig.maxWaitForBlockingActivation))\n      }\n    }\n  }\n\n  /** Custom unmarshaller for query parameters \"limit\" for \"list\" operations. */\n  private implicit val stringToListLimit: Unmarshaller[String, ListLimit] = RestApiCommons.stringToListLimit(collection)\n\n  /** Custom unmarshaller for query parameters \"skip\" for \"list\" operations. */\n  private implicit val stringToListSkip: Unmarshaller[String, ListSkip] = RestApiCommons.stringToListSkip(collection)\n\n}\n\nprivate case class TooManyActionsInSequence() extends IllegalArgumentException\nprivate case class NoComponentInSequence() extends IllegalArgumentException\nprivate case class SequenceWithCycle() extends IllegalArgumentException\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Activations.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport java.time.Instant\n\nimport scala.concurrent.Future\nimport scala.language.postfixOps\nimport scala.util.{Failure, Success, Try}\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.BadRequest\nimport org.apache.pekko.http.scaladsl.server.Directives\nimport org.apache.pekko.http.scaladsl.unmarshalling._\nimport spray.json.DefaultJsonProtocol.RootJsObjectFormat\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.logging.LogStore\nimport org.apache.openwhisk.core.controller.RestApiCommons.{ListLimit, ListSkip}\nimport org.apache.openwhisk.core.database.ActivationStore\nimport org.apache.openwhisk.core.entitlement.Privilege.READ\nimport org.apache.openwhisk.core.entitlement.{Collection, Privilege, Resource}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.core.database.UserContext\nimport pureconfig.loadConfigOrThrow\n\nobject WhiskActivationsApi {\n\n  /** Custom unmarshaller for query parameters \"name\" into valid package/action name path. */\n  private implicit val stringToRestrictedEntityPath: Unmarshaller[String, Option[EntityPath]] =\n    Unmarshaller.strict[String, Option[EntityPath]] { value =>\n      Try { EntityPath(value) } match {\n        case Success(e) if e.segments <= 2 => Some(e)\n        case _ if value.trim.isEmpty       => None\n        case _                             => throw new IllegalArgumentException(Messages.badNameFilter(value))\n      }\n    }\n\n  /** Custom unmarshaller for query parameters \"since\" and \"upto\" into a valid Instant. */\n  private implicit val stringToInstantDeserializer: Unmarshaller[String, Instant] =\n    Unmarshaller.strict[String, Instant] { value =>\n      Try { Instant.ofEpochMilli(value.toLong) } match {\n        case Success(e) => e\n        case Failure(t) => throw new IllegalArgumentException(Messages.badEpoch(value))\n      }\n    }\n\n  /** Custom unmarshaller for query parameters \"limit\" for \"list\" operations. */\n  private implicit val stringToListLimit: Unmarshaller[String, ListLimit] =\n    RestApiCommons.stringToListLimit(Collection(Collection.ACTIVATIONS))\n\n  /** Custom unmarshaller for query parameters \"skip\" for \"list\" operations. */\n  private implicit val stringToListSkip: Unmarshaller[String, ListSkip] =\n    RestApiCommons.stringToListSkip(Collection(Collection.ACTIVATIONS))\n\n}\n\n/** A trait implementing the activations API. */\ntrait WhiskActivationsApi extends Directives with AuthenticatedRouteProvider with AuthorizedRouteProvider with ReadOps {\n\n  protected val disableStoreResultConfig = loadConfigOrThrow[Boolean](ConfigKeys.disableStoreResult)\n\n  protected override val collection = Collection(Collection.ACTIVATIONS)\n\n  /** JSON response formatter. */\n  import RestApiCommons.jsonDefaultResponsePrinter\n\n  /** Database service to GET activations. */\n  protected val activationStore: ActivationStore\n\n  /** LogStore for retrieving activation logs */\n  protected val logStore: LogStore\n\n  /** Path to Actions REST API. */\n  protected val activationsPath = \"activations\"\n\n  /** Path to activation result and logs. */\n  private val resultPath = \"result\"\n  private val logsPath = \"logs\"\n\n  /** Only GET is supported in this API. */\n  protected override lazy val entityOps = get\n\n  /** Validated entity name as an ActivationId from the matched path segment. */\n  protected override def entityname(n: String) = {\n    val activationId = ActivationId.parse(n)\n    validate(activationId.isSuccess, activationId match {\n      case Failure(t: IllegalArgumentException) => t.getMessage\n      case _                                    => Messages.activationIdIllegal\n    }) & extract(_ => n)\n  }\n\n  /**\n   * Overrides because API allows for GET on /activations and /activations/[result|log] which\n   * would be rejected in the superclass.\n   */\n  override protected def innerRoutes(user: Identity, ns: EntityPath)(implicit transid: TransactionId) = {\n    (entityPrefix & entityOps & requestMethod) { (segment, m) =>\n      entityname(segment) {\n        // defer rest of the path processing to the fetch operation, which is\n        // the only operation supported on activations that reach the inner route\n        name =>\n          authorizeAndDispatch(m, user, Resource(ns, collection, Some(name)))\n      }\n    }\n  }\n\n  /** Dispatches resource to the proper handler depending on context. */\n  protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(\n    implicit transid: TransactionId) = {\n    extractRequest { request =>\n      val context = UserContext(user, request)\n\n      resource.entity.flatMap(e => ActivationId.parse(e).toOption) match {\n        case Some(aid) =>\n          op match {\n            case READ => fetch(context, resource.namespace, aid)\n            case _    => reject // should not get here\n          }\n        case None =>\n          op match {\n            case READ => list(context, resource.namespace)\n            case _    => reject // should not get here\n          }\n      }\n    }\n  }\n\n  /**\n   * Gets all activations in namespace. Filters by action name if parameter is given.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 [] or [WhiskActivation as JSON]\n   * - 500 Internal Server Error\n   */\n  private def list(context: UserContext, namespace: EntityPath)(implicit transid: TransactionId) = {\n    import WhiskActivationsApi.stringToRestrictedEntityPath\n    import WhiskActivationsApi.stringToInstantDeserializer\n    import WhiskActivationsApi.stringToListLimit\n    import WhiskActivationsApi.stringToListSkip\n\n    parameter(\n      'skip.as[ListSkip] ? ListSkip(collection.defaultListSkip),\n      'limit.as[ListLimit] ? ListLimit(collection.defaultListLimit),\n      'count ? false,\n      'docs ? false,\n      'name.as[Option[EntityPath]] ?,\n      'since.as[Instant] ?,\n      'upto.as[Instant] ?) { (skip, limit, count, docs, name, since, upto) =>\n      if (count && !docs) {\n        countEntities {\n          activationStore.countActivationsInNamespace(namespace, name.flatten, skip.n, since, upto, context)\n        }\n      } else if (count && docs) {\n        terminate(BadRequest, Messages.docsNotAllowedWithCount)\n      } else {\n        val activations = name.flatten match {\n          case Some(action) =>\n            activationStore.listActivationsMatchingName(namespace, action, skip.n, limit.n, docs, since, upto, context)\n          case None =>\n            activationStore.listActivationsInNamespace(namespace, skip.n, limit.n, docs, since, upto, context)\n        }\n        listEntities(activations map (_.fold((js) => js, (wa) => wa.map(_.toExtendedJson()))))\n      }\n    }\n  }\n\n  /**\n   * Gets activation. The activation id is prefixed with the namespace to create the primary index key.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskActivation as JSON\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  private def fetch(context: UserContext, namespace: EntityPath, activationId: ActivationId)(\n    implicit transid: TransactionId) = {\n    val docid = DocId(WhiskEntity.qualifiedName(namespace, activationId))\n    pathEndOrSingleSlash {\n      getEntity(\n        activationStore.get(ActivationId(docid.asString), context),\n        postProcess = Some((activation: WhiskActivation) => complete(activation.toExtendedJson())))\n    } ~ (pathPrefix(resultPath) & pathEnd) { fetchResponse(context, docid) } ~\n      (pathPrefix(logsPath) & pathEnd) { fetchLogs(context, docid) }\n  }\n\n  /**\n   * Gets activation result. The activation id is prefixed with the namespace to create the primary index key.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 { result: ..., success: Boolean, statusMessage: String }\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  private def fetchResponse(context: UserContext, docid: DocId)(implicit transid: TransactionId) = {\n    getEntityAndProject(\n      activationStore.get(ActivationId(docid.asString), context),\n      (activation: WhiskActivation) => Future.successful(activation.response.toExtendedJson))\n  }\n\n  /**\n   * Gets activation logs. The activation id is prefixed with the namespace to create the primary index key.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 { logs: String }\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  private def fetchLogs(context: UserContext, docid: DocId)(implicit transid: TransactionId) = {\n    getEntityAndProjectLog(\n      activationStore.get(ActivationId(docid.asString), context),\n      docid,\n      disableStoreResultConfig,\n      (namespace: String,\n       activationId: ActivationId,\n       start: Option[Instant],\n       end: Option[Instant],\n       logs: Option[ActivationLogs]) =>\n        logStore.fetchLogs(namespace, activationId, start, end, logs, context).map(_.toJsonObject))\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/ApiUtils.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport java.time.Instant\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.util.Failure\nimport scala.util.Success\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCode\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Conflict\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.InternalServerError\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NoContent\nimport org.apache.pekko.http.scaladsl.server.{Directives, RequestContext, RouteResult}\nimport spray.json.DefaultJsonProtocol._\nimport spray.json.{JsObject, JsValue, RootJsonFormat}\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.FeatureFlags\nimport org.apache.openwhisk.core.controller.PostProcess.PostProcessEntity\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity.{ActivationId, ActivationLogs, DocId, WhiskActivation, WhiskDocument}\nimport org.apache.openwhisk.http.ErrorResponse\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport org.apache.openwhisk.http.Messages._\n\n/** An exception to throw inside a Predicate future. */\nprotected[core] case class RejectRequest(code: StatusCode, message: Option[ErrorResponse]) extends Throwable {\n  override def toString = s\"RejectRequest($code)\" + message.map(\" \" + _.error).getOrElse(\"\")\n}\n\nprotected[core] object RejectRequest {\n\n  /** Creates rejection with default message for status code. */\n  protected[core] def apply(code: StatusCode)(implicit transid: TransactionId): RejectRequest = {\n    RejectRequest(code, Some(ErrorResponse.response(code)(transid)))\n  }\n\n  /** Creates rejection with custom message for status code. */\n  protected[core] def apply(code: StatusCode, m: String)(implicit transid: TransactionId): RejectRequest = {\n    RejectRequest(code, Some(ErrorResponse(m, transid)))\n  }\n\n  /** Creates rejection with custom message for status code derived from reason for throwable. */\n  protected[core] def apply(code: StatusCode, t: Throwable)(implicit transid: TransactionId): RejectRequest = {\n    val reason = t.getMessage\n    RejectRequest(code, if (reason != null) reason else \"Rejected\")\n  }\n}\n\n/**\n * A convenient typedef for functions that post process an entity\n * on an operation and terminate the HTTP request.\n */\nobject PostProcess {\n  type PostProcessEntity[A] = A => RequestContext => Future[RouteResult]\n}\n\n/** A trait for REST APIs that read entities from a datastore */\ntrait ReadOps extends Directives {\n\n  /** An execution context for futures */\n  protected implicit val executionContext: ExecutionContext\n\n  protected implicit val logging: Logging\n\n  /** JSON response formatter. */\n  import RestApiCommons.jsonDefaultResponsePrinter\n\n  /**\n   * Terminates HTTP request for list requests.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 entity A [] as JSON []\n   * - 500 Internal Server Error\n   */\n  protected def listEntities(list: Future[List[JsValue]])(implicit transid: TransactionId) = {\n    onComplete(list) {\n      case Success(entities) =>\n        logging.debug(this, s\"[LIST] entity success\")\n        complete(OK, entities)\n      case Failure(t: Throwable) =>\n        logging.error(this, s\"[LIST] entity failed: ${t.getMessage}\")\n        terminate(InternalServerError)\n    }\n  }\n\n  /**\n   * Terminates HTTP request for list count requests.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 JSON object\n   * - 500 Internal Server Error\n   */\n  protected def countEntities(count: Future[JsValue])(implicit transid: TransactionId) = {\n    onComplete(count) {\n      case Success(c) =>\n        logging.info(this, s\"[COUNT] count success\")\n        complete(OK, c)\n      case Failure(t: Throwable) =>\n        logging.error(this, s\"[COUNT] count failed: ${t.getMessage}\")\n        terminate(InternalServerError)\n    }\n  }\n\n  /**\n   * Waits on specified Future that returns an entity of type A from datastore. Terminates HTTP request.\n   *\n   * @param entity future that returns an entity of type A fetched from datastore\n   * @param postProcess an optional continuation to post process the result of the\n   * get and terminate the HTTP request directly\n   *\n   * Responses are one of (Code, Message)\n   * - 200 entity A as JSON\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  protected def getEntity[A <: DocumentRevisionProvider, Au >: A](entity: Future[A],\n                                                                  postProcess: Option[PostProcessEntity[A]] = None)(\n    implicit transid: TransactionId,\n    format: RootJsonFormat[A],\n    ma: Manifest[A]) = {\n    onComplete(entity) {\n      case Success(entity) =>\n        logging.debug(this, s\"[GET] entity success\")\n        postProcess map { _(entity) } getOrElse complete(OK, entity)\n      case Failure(t: NoDocumentException) =>\n        logging.debug(this, s\"[GET] entity does not exist\")\n        terminate(NotFound)\n      case Failure(t: DocumentTypeMismatchException) =>\n        logging.debug(this, s\"[GET] entity conformance check failed: ${t.getMessage}\")\n        terminate(Conflict, conformanceMessage)\n      case Failure(t: ArtifactStoreException) =>\n        logging.debug(this, s\"[GET] entity unreadable\")\n        terminate(InternalServerError, t.getMessage)\n      case Failure(t: Throwable) =>\n        logging.error(this, s\"[GET] entity failed: ${t.getMessage}\")\n        terminate(InternalServerError)\n    }\n  }\n\n  /**\n   * Waits on specified Future that returns an entity of type A from datastore. Terminates HTTP request.\n   *\n   * @param entity future that returns an entity of type A fetched from datastore\n   * @param project a function A => JSON which projects fields form A\n   *\n   * Responses are one of (Code, Message)\n   * - 200 project(A) as JSON\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  protected def getEntityAndProject[A <: DocumentRevisionProvider, Au >: A](\n    entity: Future[A],\n    project: A => Future[JsObject])(implicit transid: TransactionId, format: RootJsonFormat[A], ma: Manifest[A]) = {\n    onComplete(entity) {\n      case Success(entity) =>\n        logging.debug(this, s\"[PROJECT] entity success\")\n\n        onComplete(project(entity)) {\n          case Success(response: JsObject) => complete(OK, response)\n          case Failure(t: Throwable) =>\n            logging.error(this, s\"[PROJECT] projection failed: ${t.getMessage}\")\n            terminate(InternalServerError, t.getMessage)\n        }\n      case Failure(t: NoDocumentException) =>\n        logging.debug(this, s\"[PROJECT] entity does not exist\")\n        terminate(NotFound)\n      case Failure(t: DocumentTypeMismatchException) =>\n        logging.debug(this, s\"[PROJECT] entity conformance check failed: ${t.getMessage}\")\n        terminate(Conflict, conformanceMessage)\n      case Failure(t: ArtifactStoreException) =>\n        logging.debug(this, s\"[PROJECT] entity unreadable\")\n        terminate(InternalServerError, t.getMessage)\n      case Failure(t: Throwable) =>\n        logging.error(this, s\"[PROJECT] entity failed: ${t.getMessage}\")\n        terminate(InternalServerError)\n    }\n  }\n\n  /**\n   * Waits on specified Future that returns an entity of type A from datastore.\n   * In case A entity is not stored, use the docId to search logstore\n   * Terminates HTTP request.\n   *\n   * @param entity future that returns an entity of type A fetched from datastore\n   * @param docId activation DocId\n   * @param disableStoreResultConfig configuration\n   * @param project a function A => JSON which projects fields form A\n   *\n   * Responses are one of (Code, Message)\n   * - 200 project(A) as JSON\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  protected def getEntityAndProjectLog[A <: DocumentRevisionProvider, Au >: A](\n    entity: Future[A],\n    docId: DocId,\n    disableStoreResultConfig: Boolean,\n    project: (String, ActivationId, Option[Instant], Option[Instant], Option[ActivationLogs]) => Future[JsObject])(\n    implicit transid: TransactionId,\n    format: RootJsonFormat[A],\n    ma: Manifest[A]) = {\n    onComplete(entity) {\n      case Success(entity) =>\n        logging.debug(this, s\"[PROJECT] entity success\")\n        val activation = entity.asInstanceOf[WhiskActivation]\n        onComplete(\n          project(\n            activation.namespace.asString,\n            activation.activationId,\n            Some(activation.start),\n            Some(activation.end),\n            Some(activation.logs))) {\n          case Success(response: JsObject) =>\n            complete(OK, response)\n          case Failure(t: Throwable) =>\n            logging.error(this, s\"[PROJECT] projection failed: ${t.getMessage}\")\n            terminate(InternalServerError, t.getMessage)\n        }\n      case Failure(t: NoDocumentException) =>\n        // In case disableStoreResult configuration is active, persevere\n        // log might still be available even if entity was not\n        if (disableStoreResultConfig) {\n          val namespace = docId.asString.split(\"/\")(0)\n          val id = docId.asString.split(\"/\")(1)\n          onComplete(project(namespace, ActivationId(id), None, None, None)) {\n            case Success(response: JsObject) =>\n              logging.debug(this, s\"[PROJECTLOG] entity success\")\n              complete(OK, response)\n            case Failure(t: Throwable) =>\n              logging.error(this, s\"[PROJECTLOG] projection failed: ${t.getMessage}\")\n              terminate(InternalServerError, t.getMessage)\n          }\n        } else {\n          terminate(NotFound)\n        }\n      case Failure(t: DocumentTypeMismatchException) =>\n        logging.debug(this, s\"[PROJECT] entity conformance check failed: ${t.getMessage}\")\n        terminate(Conflict, conformanceMessage)\n      case Failure(t: ArtifactStoreException) =>\n        logging.debug(this, s\"[PROJECT] entity unreadable\")\n        terminate(InternalServerError, t.getMessage)\n      case Failure(t: Throwable) =>\n        logging.error(this, s\"[PROJECT] entity failed: ${t.getMessage}\")\n        terminate(InternalServerError)\n    }\n  }\n}\n\n/** A trait for REST APIs that write entities to a datastore */\ntrait WriteOps extends Directives {\n\n  /** An execution context for futures */\n  protected implicit val executionContext: ExecutionContext\n\n  protected implicit val logging: Logging\n\n  /** JSON response formatter. */\n  import RestApiCommons.jsonDefaultResponsePrinter\n\n  /**\n   * A predicate future that completes with true iff the entity should be\n   * stored in the datastore. Future should fail otherwise with RejectPut.\n   */\n  protected type PutPredicate = Future[Boolean]\n\n  /**\n   * Creates or updates an entity of type A in the datastore. First, fetch the entity\n   * by id from the datastore (this is required to get the document revision for an update).\n   * If the entity does not exist, create it. If it does exist, and 'overwrite' is enabled,\n   * update the entity.\n   *\n   * @param factory the factory that can fetch entity of type A from datastore\n   * @param datastore the client to the database\n   * @param docid the document id to put\n   * @param overwrite updates an existing entity iff overwrite == true\n   * @param update a function (A) => Future[A] that updates the existing entity with PUT content\n   * @param create a function () => Future[A] that creates a new entity from PUT content\n   * @param treatExistsAsConflict if true and document exists but overwrite is not enabled, respond\n   * with Conflict else return OK and the existing document\n   *\n   * Responses are one of (Code, Message)\n   * - 200 entity A as JSON\n   * - 400 Bad Request\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  protected def putEntity[A <: DocumentRevisionProvider, Au >: A](factory: DocumentFactory[A],\n                                                                  datastore: ArtifactStore[Au],\n                                                                  docid: DocId,\n                                                                  overwrite: Boolean,\n                                                                  update: A => Future[A],\n                                                                  create: () => Future[A],\n                                                                  treatExistsAsConflict: Boolean = true,\n                                                                  postProcess: Option[PostProcessEntity[A]] = None)(\n    implicit transid: TransactionId,\n    format: RootJsonFormat[A],\n    notifier: Option[CacheChangeNotification],\n    ma: Manifest[A]) = {\n    // marker to return an existing doc with status OK rather than conflict if overwrite is false\n    case class IdentityPut(self: A) extends Throwable\n\n    onComplete(factory.get(datastore, docid, ignoreMissingAttachment = overwrite) flatMap { doc =>\n      if (overwrite) {\n        logging.debug(this, s\"[PUT] entity exists, will try to update '$doc'\")\n        update(doc).map(updatedDoc => (Some(doc), updatedDoc))\n      } else if (treatExistsAsConflict) {\n        logging.debug(this, s\"[PUT] entity exists, but overwrite is not enabled, aborting\")\n        Future failed RejectRequest(Conflict, \"resource already exists\")\n      } else {\n        Future failed IdentityPut(doc)\n      }\n    } recoverWith {\n      case _: NoDocumentException =>\n        logging.debug(this, s\"[PUT] entity does not exist, will try to create it\")\n        create().map(newDoc => (None, newDoc))\n    } flatMap {\n      case (old, a) =>\n        logging.debug(this, s\"[PUT] entity created/updated, writing back to datastore\")\n        factory.put(datastore, a, old) map { _ =>\n          a\n        }\n    }) {\n      case Success(entity) =>\n        logging.debug(this, s\"[PUT] entity success\")\n        if (FeatureFlags.requireResponsePayload) postProcess map { _(entity) } getOrElse complete(OK, entity)\n        else postProcess map { _(entity) } getOrElse complete(OK)\n      case Failure(IdentityPut(a)) =>\n        logging.debug(this, s\"[PUT] entity exists, not overwritten\")\n        complete(OK, a)\n      case Failure(t: DocumentConflictException) =>\n        logging.debug(this, s\"[PUT] entity conflict: ${t.getMessage}\")\n        terminate(Conflict, conflictMessage)\n      case Failure(RejectRequest(code, message)) =>\n        logging.debug(this, s\"[PUT] entity rejected with code $code: $message\")\n        terminate(code, message)\n      case Failure(t: DocumentTypeMismatchException) =>\n        logging.debug(this, s\"[PUT] entity conformance check failed: ${t.getMessage}\")\n        terminate(Conflict, conformanceMessage)\n      case Failure(t: ArtifactStoreException) =>\n        logging.debug(this, s\"[PUT] entity unreadable\")\n        terminate(InternalServerError, t.getMessage)\n      case Failure(t: Throwable) =>\n        logging.error(this, s\"[PUT] entity failed: ${t.getMessage}\")\n        terminate(InternalServerError)\n    }\n  }\n\n  /**\n   * Deletes an entity of type A from datastore.\n   * To delete an entity, first fetch the record to identify its revision and then delete it.\n   * Terminates HTTP request.\n   *\n   * @param factory the factory that can fetch entity of type A from datastore\n   * @param datastore the client to the database\n   * @param docid the document id to delete\n   * @param confirm a function (A => Future[Unit]) that confirms the entity is safe to delete (must fail future to abort)\n   * or fails the future with an appropriate message\n   *\n   * Responses are one of (Code, Message)\n   * - 200 entity A as JSON\n   * - 404 Not Found\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  protected def deleteEntity[A <: WhiskDocument, Au >: A](factory: DocumentFactory[A],\n                                                          datastore: ArtifactStore[Au],\n                                                          docid: DocId,\n                                                          confirm: A => Future[Unit],\n                                                          postProcess: Option[PostProcessEntity[A]] = None)(\n    implicit transid: TransactionId,\n    format: RootJsonFormat[A],\n    notifier: Option[CacheChangeNotification],\n    ma: Manifest[A]) = {\n    onComplete(factory.get(datastore, docid, ignoreMissingAttachment = true) flatMap { entity =>\n      confirm(entity) flatMap {\n        case _ =>\n          factory.del(datastore, entity.docinfo) map { _ =>\n            entity\n          }\n      }\n    }) {\n      case Success(entity) =>\n        logging.debug(this, s\"[DEL] entity success\")\n        if (FeatureFlags.requireResponsePayload) postProcess map { _(entity) } getOrElse complete(OK, entity)\n        else postProcess map { _(entity) } getOrElse complete(NoContent)\n      case Failure(t: NoDocumentException) =>\n        logging.debug(this, s\"[DEL] entity does not exist\")\n        terminate(NotFound)\n      case Failure(t: DocumentConflictException) =>\n        logging.debug(this, s\"[DEL] entity conflict: ${t.getMessage}\")\n        terminate(Conflict, conflictMessage)\n      case Failure(RejectRequest(code, message)) =>\n        logging.debug(this, s\"[DEL] entity rejected with code $code: $message\")\n        terminate(code, message)\n      case Failure(t: DocumentTypeMismatchException) =>\n        logging.debug(this, s\"[DEL] entity conformance check failed: ${t.getMessage}\")\n        terminate(Conflict, conformanceMessage)\n      case Failure(t: ArtifactStoreException) =>\n        logging.error(this, s\"[DEL] entity unreadable\")\n        terminate(InternalServerError, t.getMessage)\n      case Failure(t: Throwable) =>\n        logging.error(this, s\"[DEL] entity failed: ${t.getMessage}\")\n        terminate(InternalServerError)\n    }\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/AuthenticatedRoute.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport org.apache.pekko.http.scaladsl.server.Route\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.Identity\n\n/** A trait for authenticated routes. */\ntrait AuthenticatedRouteProvider {\n  def routes(user: Identity)(implicit transid: TransactionId): Route\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/AuthorizedRouteDispatcher.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport scala.concurrent.ExecutionContext\nimport scala.language.postfixOps\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport scala.concurrent.Future\n\nimport org.apache.pekko.http.scaladsl.server.Directives\nimport org.apache.pekko.http.scaladsl.model.HttpMethod\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Forbidden\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport org.apache.pekko.http.scaladsl.server.RequestContext\nimport org.apache.pekko.http.scaladsl.server.RouteResult\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.InternalServerError\nimport org.apache.pekko.http.scaladsl.server.Directive1\n\nimport org.apache.openwhisk.core.entitlement.Collection\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entitlement._\nimport org.apache.openwhisk.core.entitlement.Resource\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.ErrorResponse\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport org.apache.openwhisk.http.Messages\n\n/** A trait for routes that require entitlement checks. */\ntrait BasicAuthorizedRouteProvider extends Directives {\n\n  /** An execution context for futures */\n  protected implicit val executionContext: ExecutionContext\n\n  /** An entitlement service to check access rights. */\n  protected val entitlementProvider: EntitlementProvider\n\n  /** The collection type for this trait. */\n  protected val collection: Collection\n\n  /** Route directives for API. The methods that are supported on the collection. */\n  protected lazy val collectionOps = pathEndOrSingleSlash & get\n\n  /** Route directives for API. The path prefix that identifies entity handlers. */\n  protected lazy val entityPrefix = pathPrefix(Segment)\n\n  /** Route directives for API. The methods that are supported on entities. */\n  protected lazy val entityOps = get\n\n  /** JSON response formatter. */\n  import RestApiCommons.jsonDefaultResponsePrinter\n\n  /** Checks entitlement and dispatches to handler if authorized. */\n  protected def authorizeAndDispatch(method: HttpMethod, user: Identity, resource: Resource)(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult] = {\n    val right = collection.determineRight(method, resource.entity)\n\n    onComplete(entitlementProvider.check(user, right, resource)) {\n      case Success(_) => dispatchOp(user, right, resource)\n      case Failure(t) =>\n        t match {\n          case (r: RejectRequest) =>\n            r.code match {\n              case Forbidden =>\n                handleEntitlementFailure(\n                  RejectRequest(\n                    Forbidden,\n                    Some(ErrorResponse(Messages.notAuthorizedtoAccessResource(resource.fqname), transid))))\n              case NotFound =>\n                handleEntitlementFailure(\n                  RejectRequest(NotFound, Some(ErrorResponse(Messages.resourceDoesntExist(resource.fqname), transid))))\n              case _ => handleEntitlementFailure(t)\n            }\n        }\n    }\n  }\n\n  protected def handleEntitlementFailure(failure: Throwable)(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult] = {\n    failure match {\n      case (r: RejectRequest) => terminate(r.code, r.message)\n      case t                  => terminate(InternalServerError)\n    }\n  }\n\n  /** Dispatches resource to the proper handler depending on context. */\n  protected def dispatchOp(user: Identity, op: Privilege, resource: Resource)(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult]\n\n  /** Extracts namespace for user from the matched path segment. */\n  protected def namespace(user: Identity, ns: String) = {\n    validate(\n      isNamespace(ns), {\n        if (ns.length > EntityName.ENTITY_NAME_MAX_LENGTH) {\n          Messages.entityNameTooLong(\n            SizeError(namespaceDescriptionForSizeError, ns.length.B, EntityName.ENTITY_NAME_MAX_LENGTH.B))\n        } else {\n          Messages.namespaceIllegal\n        }\n      }) & extract(_ => EntityPath(if (EntityPath(ns) == EntityPath.DEFAULT) user.namespace.name.asString else ns))\n  }\n\n  /** Validates entity name from the matched path segment. */\n  protected val namespaceDescriptionForSizeError = \"Namespace\"\n\n  /** Extracts the HTTP method which is used to determine privilege for resource. */\n  protected val requestMethod = extract(_.request.method)\n\n  /** Confirms that a path segment is a valid namespace. Used to reject invalid namespaces. */\n  protected def isNamespace(n: String) = Try { EntityPath(n) } isSuccess\n}\n\n/**\n * A common trait for entity routes that require entitlement checks,\n * which share common collectionPrefix and entity operations.\n */\ntrait AuthorizedRouteProvider extends BasicAuthorizedRouteProvider {\n\n  /**\n   * Route directives for API.\n   * The default path prefix for the collection is one of\n   * '_/collection-path' matching an implicit namespace, or\n   * 'explicit-namespace/collection-path'.\n   */\n  protected lazy val collectionPrefix = pathPrefix((EntityPath.DEFAULT.toString.r | Segment) / collection.path)\n\n  /** Route directives for API. The methods that are supported on entities. */\n  override protected lazy val entityOps = put | get | delete | post\n\n  /**\n   * Common REST API for Whisk Entities. Defines all the routes handled by this API. They are:\n   *\n   * GET  namespace/entities[/]   -- list all entities in namespace\n   * GET  namespace/entities/name -- fetch entity by name from namespace\n   * PUT  namespace/entities/name -- create or update entity by name from namespace with content\n   * DEL  namespace/entities/name -- remove entity by name form namespace\n   * POST namespace/entities/name -- \"activate\" entity by name from namespace with content\n   *\n   * @param user the authenticated user for this route\n   */\n  def routes(user: Identity)(implicit transid: TransactionId) = {\n    collectionPrefix { segment =>\n      namespace(user, segment) { ns =>\n        (collectionOps & requestMethod) {\n          // matched /namespace/collection\n          authorizeAndDispatch(_, user, Resource(ns, collection, None))\n        } ~ innerRoutes(user, ns)\n      }\n    }\n  }\n\n  /**\n   * Handles the inner routes of the collection. This allows customizing nested resources.\n   */\n  protected def innerRoutes(user: Identity, ns: EntityPath)(implicit transid: TransactionId) = {\n    (entityPrefix & entityOps & requestMethod) { (segment, m) =>\n      // matched /namespace/collection/entity\n      (entityname(segment) & pathEnd) { name =>\n        authorizeAndDispatch(m, user, Resource(ns, collection, Some(name)))\n      }\n    }\n  }\n\n  /** Extracts and validates entity name from the matched path segment. */\n  protected def entityname(segment: String): Directive1[String]\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Backend.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.entitlement._\nimport org.apache.openwhisk.core.entity.ActivationId.ActivationIdGenerator\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancer\n\n/**\n * A trait which defines a few services which a whisk microservice may rely on.\n */\ntrait WhiskServices {\n\n  /** Whisk configuration object. */\n  protected val whiskConfig: WhiskConfig\n\n  /** An entitlement service to check access rights. */\n  protected val entitlementProvider: EntitlementProvider\n\n  /** A generator for new activation ids. */\n  protected val activationIdFactory: ActivationIdGenerator\n\n  /** A load balancing service that launches invocations. */\n  protected val loadBalancer: LoadBalancer\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/BasicAuthenticationDirective.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.headers._\nimport org.apache.pekko.http.scaladsl.server.Directives._\nimport org.apache.pekko.http.scaladsl.server.directives.{AuthenticationDirective, AuthenticationResult}\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.database.NoDocumentException\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types.AuthStore\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.Try\n\nobject BasicAuthenticationDirective extends AuthenticationDirectiveProvider {\n\n  def validateCredentials(credentials: Option[BasicHttpCredentials])(implicit transid: TransactionId,\n                                                                     ec: ExecutionContext,\n                                                                     logging: Logging,\n                                                                     authStore: AuthStore): Future[Option[Identity]] = {\n    credentials flatMap { pw =>\n      Try {\n        // authkey deserialization is wrapped in a try to guard against malformed values\n        val authkey = BasicAuthenticationAuthKey(UUID(pw.username), Secret(pw.password))\n        val future = Identity.get(authStore, authkey) map { result =>\n          if (authkey == result.authkey) {\n            logging.debug(this, s\"authentication valid\")\n            Some(result)\n          } else {\n            logging.debug(this, s\"authentication not valid\")\n            None\n          }\n        } recover {\n          case _: NoDocumentException | _: IllegalArgumentException =>\n            logging.debug(this, s\"authentication not valid\")\n            None\n        }\n        future.failed.foreach(t => logging.error(this, s\"authentication error: $t\"))\n        future\n      }.toOption\n    } getOrElse {\n      credentials.foreach(_ => logging.debug(this, s\"credentials are malformed\"))\n      Future.successful(None)\n    }\n  }\n\n  /** Creates HTTP BasicAuth handler */\n  def basicAuth[A](verify: Option[BasicHttpCredentials] => Future[Option[A]]): AuthenticationDirective[A] = {\n    extractExecutionContext.flatMap { implicit ec =>\n      authenticateOrRejectWithChallenge[BasicHttpCredentials, A] { creds =>\n        verify(creds).map {\n          case Some(t) => AuthenticationResult.success(t)\n          case None    => AuthenticationResult.failWithChallenge(HttpChallenges.basic(\"OpenWhisk secure realm\"))\n        }\n      }\n    }\n  }\n\n  def identityByNamespace(\n    namespace: EntityName)(implicit transid: TransactionId, system: ActorSystem, authStore: AuthStore) = {\n    Identity.get(authStore, namespace)\n  }\n\n  def authenticate(implicit transid: TransactionId,\n                   authStore: AuthStore,\n                   logging: Logging): AuthenticationDirective[Identity] = {\n    extractExecutionContext.flatMap { implicit ec =>\n      basicAuth(validateCredentials)\n    }\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Controller.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.{ActorSystem, CoordinatedShutdown}\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.model.{StatusCodes, Uri}\nimport org.apache.pekko.http.scaladsl.server.Route\nimport kamon.Kamon\nimport org.apache.openwhisk.common.Https.HttpsConfig\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.core.connector.MessagingProvider\nimport org.apache.openwhisk.core.containerpool.logging.LogStoreProvider\nimport org.apache.openwhisk.core.database.{ActivationStoreProvider, CacheChangeNotification, RemoteCacheInvalidation}\nimport org.apache.openwhisk.core.entitlement._\nimport org.apache.openwhisk.core.entity.ActivationId.ActivationIdGenerator\nimport org.apache.openwhisk.core.entity.ExecManifest.Runtimes\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancerProvider\nimport org.apache.openwhisk.http.CorsSettings.RespondWithServerCorsHeaders\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport org.apache.openwhisk.http.{BasicHttpService, BasicRasService}\nimport org.apache.openwhisk.spi.SpiLoader\nimport pureconfig._\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.Await\nimport scala.concurrent.ExecutionContext.Implicits\nimport scala.concurrent.duration.DurationInt\nimport scala.util.{Failure, Success}\n\n/**\n * The Controller is the service that provides the REST API for OpenWhisk.\n *\n * It extends the BasicRasService so it includes a ping endpoint for monitoring.\n *\n * Pekko sends messages to pekko Actors -- the Controller is an Actor, ready to receive messages.\n *\n * It is possible to deploy a hot-standby controller. Each controller needs its own instance. This instance is a\n * consecutive numbering, starting with 0.\n * The state and cache of each controller is not shared to the other controllers.\n * If the base controller crashes, the hot-standby controller will be used. After the base controller is up again,\n * it will be used again. Because of the empty cache after restart, there are no problems with inconsistency.\n * The only problem that could occur is, that the base controller is not reachable, but does not restart. After switching\n * back to the base controller, there could be an inconsistency in the cache (e.g. if a user has updated an action). This\n * inconsistency will be resolved by its own after removing the cached item, 5 minutes after it has been generated.\n *\n * Uses the Pekko routing DSL: https://pekko.apache.org/docs/pekko-http/current/routing-dsl/index.html\n *\n * @param config A set of properties needed to run an instance of the controller service\n * @param instance if running in scale-out, a unique identifier for this instance in the group\n * @param verbosity logging verbosity\n * @param executionContext Scala runtime support for concurrent operations\n */\nclass Controller(val instance: ControllerInstanceId,\n                 runtimes: Runtimes,\n                 implicit val whiskConfig: WhiskConfig,\n                 implicit val actorSystem: ActorSystem,\n                 implicit val logging: Logging)\n    extends BasicRasService\n    with RespondWithServerCorsHeaders {\n\n  TransactionId.controller.mark(\n    this,\n    LoggingMarkers.CONTROLLER_STARTUP(instance.asString),\n    s\"starting controller instance ${instance.asString}\",\n    logLevel = InfoLevel)\n\n  /**\n   * A Route in Pekko is technically a function taking a RequestContext as a parameter.\n   *\n   * The \"~\" Pekko DSL operator composes two independent Routes, building a routing tree structure.\n   *\n   * @see https://pekko.apache.org/docs/pekko-http/current/routing-dsl/routes.html#composing-routes\n   */\n  override def routes(implicit transid: TransactionId): Route = {\n    super.routes ~ {\n      (pathEndOrSingleSlash & get) {\n        complete(info)\n      }\n    } ~ apiV1.routes ~ swagger.swaggerRoutes ~ adminRoutes\n  }\n\n  // initialize datastores\n  private implicit val authStore = WhiskAuthStore.datastore()\n  private implicit val entityStore = WhiskEntityStore.datastore()\n  private implicit val cacheChangeNotification = Some(new CacheChangeNotification {\n    val remoteCacheInvalidaton = new RemoteCacheInvalidation(whiskConfig, \"controller\", instance)\n    override def apply(k: CacheKey) = {\n      remoteCacheInvalidaton.invalidateWhiskActionMetaData(k)\n      remoteCacheInvalidaton.notifyOtherInstancesAboutInvalidation(k)\n    }\n  })\n\n  // initialize backend services\n  private implicit val loadBalancer =\n    SpiLoader.get[LoadBalancerProvider].instance(whiskConfig, instance)\n  logging.info(this, s\"loadbalancer initialized: ${loadBalancer.getClass.getSimpleName}\")(TransactionId.controller)\n\n  private implicit val entitlementProvider =\n    SpiLoader.get[EntitlementSpiProvider].instance(whiskConfig, loadBalancer, instance)\n  private implicit val activationIdFactory = new ActivationIdGenerator {}\n  private implicit val logStore = SpiLoader.get[LogStoreProvider].instance(actorSystem)\n  private implicit val activationStore =\n    SpiLoader.get[ActivationStoreProvider].instance(actorSystem, logging)\n\n  // register collections\n  Collection.initialize(entityStore)\n\n  /** The REST APIs. */\n  implicit val controllerInstance = instance\n  private val apiV1 = new RestAPIVersion(whiskConfig, \"api\", \"v1\")\n  private val swagger = new SwaggerDocs(Uri.Path.Empty, \"infoswagger.json\")\n\n  /**\n   * Handles GET /invokers - list of invokers\n   *             /invokers/healthy/count - nr of healthy invokers\n   *             /invokers/ready - 200 in case # of healthy invokers are above the expected value\n   *                             - 500 in case # of healthy invokers are bellow the expected value\n   *\n   * @return JSON with details of invoker health or count of healthy invokers respectively.\n   */\n  protected[controller] val internalInvokerHealth = {\n    implicit val executionContext = actorSystem.dispatcher\n    (pathPrefix(\"invokers\") & get) {\n      pathEndOrSingleSlash {\n        complete {\n          loadBalancer\n            .invokerHealth()\n            .map(_.map(i => i.id.toString -> i.status.asString).toMap.toJson.asJsObject)\n        }\n      } ~ path(\"healthy\" / \"count\") {\n        complete {\n          loadBalancer\n            .invokerHealth()\n            .map(_.count(_.status == InvokerState.Healthy).toJson)\n        }\n      } ~ path(\"ready\") {\n        onSuccess(loadBalancer.invokerHealth()) { invokersHealth =>\n          val all = invokersHealth.size\n          val healthy = invokersHealth.count(_.status == InvokerState.Healthy)\n          val ready = Controller.readyState(all, healthy, Controller.readinessThreshold.getOrElse(1))\n          if (ready)\n            complete(JsObject(\"healthy\" -> s\"$healthy/$all\".toJson))\n          else\n            complete(InternalServerError -> JsObject(\"unhealthy\" -> s\"${all - healthy}/$all\".toJson))\n        }\n      }\n    }\n  }\n\n  /**\n   * Handles GET /activation URI.\n   *\n   * @return running activation\n   */\n  protected[controller] val activationStatus = {\n    implicit val executionContext = actorSystem.dispatcher\n    (pathPrefix(\"activation\") & get) {\n      pathEndOrSingleSlash {\n        complete(loadBalancer.activeActivationsByController.map(_.toJson))\n      } ~ path(\"count\") {\n        complete(loadBalancer.activeActivationsByController(controllerInstance.asString).map(_.toJson))\n      }\n    }\n  }\n\n  // controller top level info\n  private val info = Controller.info(\n    whiskConfig,\n    TimeLimit.config,\n    MemoryLimit.config,\n    LogLimit.config,\n    runtimes,\n    List(apiV1.basepath()))\n\n  private val controllerUsername = loadConfigOrThrow[String](ConfigKeys.whiskControllerUsername)\n  private val controllerPassword = loadConfigOrThrow[String](ConfigKeys.whiskControllerPassword)\n\n  /**\n   * disable controller\n   */\n  private def disable(implicit transid: TransactionId) = {\n    implicit val executionContext = actorSystem.dispatcher\n    implicit val jsonPrettyResponsePrinter = PrettyPrinter\n    (path(\"disable\") & post) {\n      extractCredentials {\n        case Some(BasicHttpCredentials(username, password)) =>\n          if (username == controllerUsername && password == controllerPassword) {\n            loadBalancer.close\n            logging.warn(this, \"controller is disabled\")\n            complete(\"controller is disabled\")\n          } else {\n            terminate(StatusCodes.Unauthorized, \"username or password are wrong.\")\n          }\n        case _ => terminate(StatusCodes.Unauthorized)\n      }\n    }\n  }\n\n  private def adminRoutes(implicit transid: TransactionId) = {\n    sendCorsHeaders {\n      options {\n        complete(OK)\n      } ~ internalInvokerHealth ~ activationStatus ~ disable\n    }\n  }\n}\n\n/**\n * Singleton object provides a factory to create and start an instance of the Controller service.\n */\nobject Controller {\n\n  protected val protocol = loadConfigOrThrow[String](\"whisk.controller.protocol\")\n  protected val interface = loadConfigOrThrow[String](\"whisk.controller.interface\")\n  protected val readinessThreshold = loadConfig[Double](\"whisk.controller.readiness-fraction\")\n\n  val topicPrefix = loadConfigOrThrow[String](ConfigKeys.kafkaTopicsPrefix)\n  val userEventTopicPrefix = loadConfigOrThrow[String](ConfigKeys.kafkaTopicsUserEventPrefix)\n\n  // requiredProperties is a Map whose keys define properties that must be bound to\n  // a value, and whose values are default values.   A null value in the Map means there is\n  // no default value specified, so it must appear in the properties file\n  def requiredProperties =\n    ExecManifest.requiredProperties ++\n      RestApiCommons.requiredProperties ++\n      SpiLoader.get[LoadBalancerProvider].requiredProperties ++\n      EntitlementProvider.requiredProperties\n\n  private def info(config: WhiskConfig,\n                   timeLimit: TimeLimitConfig,\n                   memLimit: MemoryLimitConfig,\n                   logLimit: MemoryLimitConfig,\n                   runtimes: Runtimes,\n                   apis: List[String]) =\n    JsObject(\n      \"description\" -> \"OpenWhisk\".toJson,\n      \"support\" -> JsObject(\n        \"github\" -> \"https://github.com/apache/openwhisk/issues\".toJson,\n        \"slack\" -> \"http://slack.openwhisk.org\".toJson),\n      \"api_paths\" -> apis.toJson,\n      \"limits\" -> JsObject(\n        \"actions_per_minute\" -> config.actionInvokePerMinuteLimit.toInt.toJson,\n        \"triggers_per_minute\" -> config.triggerFirePerMinuteLimit.toInt.toJson,\n        \"concurrent_actions\" -> config.actionInvokeConcurrentLimit.toInt.toJson,\n        \"sequence_length\" -> config.actionSequenceLimit.toInt.toJson,\n        \"default_min_action_duration\" -> TimeLimit.namespaceDefaultConfig.min.toMillis.toJson,\n        \"default_max_action_duration\" -> TimeLimit.namespaceDefaultConfig.max.toMillis.toJson,\n        \"default_min_action_memory\" -> MemoryLimit.namespaceDefaultConfig.min.toBytes.toJson,\n        \"default_max_action_memory\" -> MemoryLimit.namespaceDefaultConfig.max.toBytes.toJson,\n        \"default_min_action_logs\" -> LogLimit.namespaceDefaultConfig.min.toBytes.toJson,\n        \"default_max_action_logs\" -> LogLimit.namespaceDefaultConfig.max.toBytes.toJson,\n        \"min_action_duration\" -> timeLimit.min.toMillis.toJson,\n        \"max_action_duration\" -> timeLimit.max.toMillis.toJson,\n        \"min_action_memory\" -> memLimit.min.toBytes.toJson,\n        \"max_action_memory\" -> memLimit.max.toBytes.toJson,\n        \"min_action_logs\" -> logLimit.min.toBytes.toJson,\n        \"max_action_logs\" -> logLimit.max.toBytes.toJson),\n      \"runtimes\" -> runtimes.toJson)\n\n  def readyState(allInvokers: Int, healthyInvokers: Int, readinessThreshold: Double): Boolean = {\n    if (allInvokers > 0) (healthyInvokers / allInvokers) >= readinessThreshold else false\n  }\n\n  def main(args: Array[String]): Unit = {\n    implicit val actorSystem = ActorSystem(\"controller-actor-system\")\n    implicit val logger = new PekkoLogging(org.apache.pekko.event.Logging.getLogger(actorSystem, this))\n    start(args)\n  }\n\n  def start(args: Array[String])(implicit actorSystem: ActorSystem, logger: Logging): Unit = {\n    // Configure Jackson 2.15+ StreamReadConstraints before any Jackson usage\n    // Jackson 2.15+ has a 20MB default limit, but OpenWhisk allows 48MB action code\n    // Set to 100MB for safety margin\n    import com.fasterxml.jackson.core.StreamReadConstraints\n    StreamReadConstraints.overrideDefaultStreamReadConstraints(\n      StreamReadConstraints\n        .builder()\n        .maxStringLength(104857600) // 100MB\n        .build())\n\n    ConfigMXBean.register()\n    Kamon.init()\n\n    // Prepare Kamon shutdown\n    CoordinatedShutdown(actorSystem).addTask(CoordinatedShutdown.PhaseActorSystemTerminate, \"shutdownKamon\") { () =>\n      logger.info(this, s\"Shutting down Kamon with coordinated shutdown\")\n      Kamon.stopModules().map(_ => Done)(Implicits.global)\n    }\n\n    // extract configuration data from the environment\n    val config = new WhiskConfig(requiredProperties)\n    val port = config.servicePort.toInt\n\n    // if deploying multiple instances (scale out), must pass the instance number as the\n    require(args.length >= 1, \"controller instance required\")\n    val instance = ControllerInstanceId(args(0))\n\n    def abort(message: String) = {\n      logger.error(this, message)\n      actorSystem.terminate()\n      Await.result(actorSystem.whenTerminated, 30.seconds)\n      sys.exit(1)\n    }\n\n    if (!config.isValid) {\n      abort(\"Bad configuration, cannot start.\")\n    }\n\n    val msgProvider = SpiLoader.get[MessagingProvider]\n\n    Seq(\n      (topicPrefix + \"completed\" + instance.asString, \"completed\", Some(ActivationEntityLimit.MAX_ACTIVATION_LIMIT)),\n      (topicPrefix + \"health\", \"health\", None),\n      (topicPrefix + \"cacheInvalidation\", \"cache-invalidation\", None),\n      (userEventTopicPrefix + \"events\", \"events\", None)).foreach {\n      case (topic, topicConfigurationKey, maxMessageBytes) =>\n        if (msgProvider.ensureTopic(config, topic, topicConfigurationKey, maxMessageBytes).isFailure) {\n          abort(s\"failure during msgProvider.ensureTopic for topic $topic\")\n        }\n    }\n\n    ExecManifest.initialize(config) match {\n      case Success(_) =>\n        val controller = new Controller(instance, ExecManifest.runtimesManifest, config, actorSystem, logger)\n\n        val httpsConfig =\n          if (Controller.protocol == \"https\") Some(loadConfigOrThrow[HttpsConfig](\"whisk.controller.https\")) else None\n\n        BasicHttpService.startHttpService(controller.route, port, httpsConfig, interface)(actorSystem)\n\n      case Failure(t) =>\n        abort(s\"Invalid runtimes manifest: $t\")\n    }\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Entities.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport scala.concurrent.Future\nimport scala.language.postfixOps\nimport scala.util.Try\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.ContentTooLarge\nimport org.apache.pekko.http.scaladsl.model.headers.RawHeader\nimport org.apache.pekko.http.scaladsl.server.Directive0\nimport org.apache.pekko.http.scaladsl.server.Directives\nimport org.apache.pekko.http.scaladsl.server.RequestContext\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.server.RouteResult\nimport spray.json.JsonPrinter\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entitlement.Privilege._\nimport org.apache.openwhisk.core.entitlement.Privilege\nimport org.apache.openwhisk.core.entitlement.Privilege.READ\nimport org.apache.openwhisk.core.entitlement.Resource\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport org.apache.openwhisk.http.Messages\n\nprotected[controller] trait ValidateRequestSize extends Directives {\n\n  protected def validateSize(check: => Option[SizeError])(implicit tid: TransactionId, jsonPrinter: JsonPrinter) =\n    new Directive0 {\n      override def tapply(f: Unit => Route) = {\n        check map {\n          case e: SizeError => terminate(ContentTooLarge, Messages.entityTooBig(e))\n        } getOrElse f(None)\n      }\n    }\n\n  /** Checks if request entity is within allowed length range. */\n  protected def isWhithinRange(userLimits: UserLimits, length: Long) = {\n    if (length <= userLimits.allowedMaxPayloadSize.toBytes) {\n      None\n    } else\n      Some {\n        SizeError(fieldDescriptionForSizeError, length.B, userLimits.allowedMaxPayloadSize.toBytes.B)\n      }\n  }\n  protected val fieldDescriptionForSizeError = \"Request\"\n}\n\nprotected trait CustomHeaders extends Directives {\n  val ActivationIdHeader = \"x-openwhisk-activation-id\"\n\n  /** Add activation ID in headers */\n  protected def respondWithActivationIdHeader(activationId: ActivationId): Directive0 = {\n    respondWithHeader(RawHeader(ActivationIdHeader, activationId.asString))\n  }\n}\n\n/** A trait implementing the basic operations on WhiskEntities in support of the various APIs. */\ntrait WhiskCollectionAPI\n    extends Directives\n    with AuthenticatedRouteProvider\n    with AuthorizedRouteProvider\n    with ValidateRequestSize\n    with ReadOps\n    with WriteOps\n    with CustomHeaders {\n  /** The core collections require backend services to be injected in this trait. */\n  services: WhiskServices =>\n\n  /** Creates an entity, or updates an existing one, in namespace. Terminates HTTP request. */\n  protected def create(user: Identity, entityName: FullyQualifiedEntityName)(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult]\n\n  /** Activates entity. Examples include invoking an action, firing a trigger, enabling/disabling a rule. */\n  protected def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult]\n\n  /** Removes entity from namespace. Terminates HTTP request. */\n  protected def remove(user: Identity, entityName: FullyQualifiedEntityName)(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult]\n\n  /** Gets entity from namespace. Terminates HTTP request. */\n  protected def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult]\n\n  /** Gets all entities from namespace. If necessary filter only entities that are shared. Terminates HTTP request. */\n  protected def list(user: Identity, path: EntityPath)(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult]\n\n  /** Dispatches resource to the proper handler depending on context. */\n  protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(\n    implicit transid: TransactionId) = {\n    resource.entity match {\n      case Some(EntityName(name)) =>\n        op match {\n          case READ => fetch(user, FullyQualifiedEntityName(resource.namespace, name), resource.env)\n          case PUT =>\n            entity(as[LimitedWhiskEntityPut]) { e =>\n              validateSize(e.isWithinSizeLimits(user.limits))(transid, RestApiCommons.jsonDefaultResponsePrinter) {\n                create(user, FullyQualifiedEntityName(resource.namespace, name))\n              }\n            }\n          case ACTIVATE =>\n            extract(_.request.entity.contentLengthOption) { length =>\n              validateSize(isWhithinRange(user.limits, length.getOrElse(0)))(\n                transid,\n                RestApiCommons.jsonDefaultResponsePrinter) {\n                activate(user, FullyQualifiedEntityName(resource.namespace, name), resource.env)\n              }\n            }\n\n          case DELETE => remove(user, FullyQualifiedEntityName(resource.namespace, name))\n          case _      => reject\n        }\n      case None =>\n        op match {\n          case READ =>\n            // the entitlement service will authorize any subject to list PACKAGES\n            // in any namespace regardless of ownership but the list operation CANNOT\n            // produce all entities in the requested namespace UNLESS the subject is\n            // entitled to them which for now means they own the namespace. If the\n            // subject does not own the namespace, then exclude packages that are private\n            // in the API handler\n            list(user, resource.namespace)\n\n          case _ => reject\n        }\n    }\n  }\n\n  /** Validates entity name from the matched path segment. */\n  protected val segmentDescriptionForSizeError = \"Name segement\"\n\n  protected override final def entityname(s: String) = {\n    validate(\n      isEntity(s), {\n        if (s.length > EntityName.ENTITY_NAME_MAX_LENGTH) {\n          Messages.entityNameTooLong(\n            SizeError(segmentDescriptionForSizeError, s.length.B, EntityName.ENTITY_NAME_MAX_LENGTH.B))\n        } else {\n          Messages.entityNameIllegal\n        }\n      }) & extract(_ => s)\n  }\n\n  /** Confirms that a path segment is a valid entity name. Used to reject invalid entity names. */\n  protected final def isEntity(n: String) = Try { EntityName(n) } isSuccess\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Limits.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.server.{Directive1, Directives}\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.entitlement.{Collection, Privilege, Resource}\nimport org.apache.openwhisk.core.entitlement.Privilege.READ\nimport org.apache.openwhisk.core.entity.{Identity, IntraConcurrencyLimit, LogLimit, MemoryLimit, TimeLimit}\n\ntrait WhiskLimitsApi extends Directives with AuthenticatedRouteProvider with AuthorizedRouteProvider {\n\n  protected val whiskConfig: WhiskConfig\n\n  protected override val collection = Collection(Collection.LIMITS)\n\n  protected val invocationsPerMinuteSystemDefault = whiskConfig.actionInvokePerMinuteLimit.toInt\n  protected val concurrentInvocationsSystemDefault = whiskConfig.actionInvokeConcurrentLimit.toInt\n  protected val firePerMinuteSystemDefault = whiskConfig.triggerFirePerMinuteLimit.toInt\n\n  override protected lazy val entityOps = get\n\n  /** JSON response formatter. */\n  import RestApiCommons.jsonDefaultResponsePrinter\n\n  /** Dispatches resource to the proper handler depending on context. */\n  protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(\n    implicit transid: TransactionId) = {\n\n    resource.entity match {\n      case Some(_) =>\n        //TODO: Process entity level requests for an individual limit here\n        reject //should never get here\n      case None =>\n        op match {\n          case READ =>\n            val limits = user.limits.copy(\n              Some(user.limits.invocationsPerMinute.getOrElse(invocationsPerMinuteSystemDefault)),\n              Some(user.limits.concurrentInvocations.getOrElse(concurrentInvocationsSystemDefault)),\n              Some(user.limits.firesPerMinute.getOrElse(firePerMinuteSystemDefault)),\n              maxActionMemory = Some(MemoryLimit(user.limits.allowedMaxActionMemory)),\n              minActionMemory = Some(MemoryLimit(user.limits.allowedMinActionMemory)),\n              maxActionLogs = Some(LogLimit(user.limits.allowedMaxActionLogs)),\n              minActionLogs = Some(LogLimit(user.limits.allowedMinActionLogs)),\n              maxActionTimeout = Some(TimeLimit(user.limits.allowedMaxActionTimeout)),\n              minActionTimeout = Some(TimeLimit(user.limits.allowedMinActionTimeout)),\n              maxActionConcurrency = Some(IntraConcurrencyLimit(user.limits.allowedMaxActionConcurrency)),\n              minActionConcurrency = Some(IntraConcurrencyLimit(user.limits.allowedMinActionConcurrency)),\n              maxParameterSize = Some(user.limits.allowedMaxParameterSize),\n              maxActionInstances = Some(user.limits.concurrentInvocations.getOrElse(concurrentInvocationsSystemDefault)))\n            pathEndOrSingleSlash { complete(OK, limits) }\n          case _ => reject //should never get here\n        }\n    }\n  }\n\n  protected override def entityname(n: String): Directive1[String] = {\n    validate(false, \"Inner entity level routes for limits are not yet implemented.\") & extract(_ => n)\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Namespaces.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.server.Directives\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entitlement.Collection\nimport org.apache.openwhisk.core.entity.Identity\n\ntrait WhiskNamespacesApi extends Directives with AuthenticatedRouteProvider {\n\n  protected val collection = Collection(Collection.NAMESPACES)\n  protected val collectionOps = pathEndOrSingleSlash & get\n\n  /** JSON response formatter. */\n  import RestApiCommons.jsonDefaultResponsePrinter\n\n  /**\n   * Rest API for managing namespaces. Defines all the routes handled by this API. They are:\n   *\n   * GET  namespaces[/] -- gets namespace for authenticated user\n   *\n   * @param user the authenticated user for this route\n   */\n  override def routes(user: Identity)(implicit transid: TransactionId) = {\n    (pathPrefix(collection.path) & collectionOps) {\n      complete(OK, List(user.namespace.name))\n    }\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Packages.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport scala.concurrent.Future\nimport scala.util.{Failure, Success}\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.server.{RequestContext, RouteResult}\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshaller\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.controller.RestApiCommons.{ListLimit, ListSkip}\nimport org.apache.openwhisk.core.database.{CacheChangeNotification, DocumentTypeMismatchException, NoDocumentException}\nimport org.apache.openwhisk.core.entitlement._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types.EntityStore\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.http.Messages._\nimport pureconfig._\nimport org.apache.openwhisk.core.ConfigKeys\n\ntrait WhiskPackagesApi extends WhiskCollectionAPI with ReferencedEntities {\n  services: WhiskServices =>\n\n  protected override val collection = Collection(Collection.PACKAGES)\n\n  /** Database service to CRUD packages. */\n  protected val entityStore: EntityStore\n\n  /** Config flag for Execute Only for Shared Packages */\n  protected def executeOnly =\n    loadConfigOrThrow[Boolean](ConfigKeys.sharedPackageExecuteOnly)\n\n  /** Notification service for cache invalidation. */\n  protected implicit val cacheChangeNotification: Some[CacheChangeNotification]\n\n  /** Route directives for API. The methods that are supported on packages. */\n  protected override lazy val entityOps = put | get | delete\n\n  /** JSON response formatter. */\n  import RestApiCommons.jsonDefaultResponsePrinter\n\n  /** Reserved package names. */\n  protected[core] val RESERVED_NAMES = Set(\"default\")\n\n  /**\n   * Creates or updates package/binding if it already exists. The PUT content is deserialized into a\n   * WhiskPackagePut which is a subset of WhiskPackage (it eschews the namespace and entity name since\n   * the former is derived from the authenticated user and the latter is derived from the URI). If the\n   * binding property is defined, creates or updates a package binding as long as resource is already a\n   * binding.\n   *\n   * The WhiskPackagePut is merged with the existing WhiskPackage in the datastore, overriding old values\n   * with new values that are defined. Any values not defined in the PUT content are replaced with old values.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskPackage as JSON\n   * - 400 Bad Request\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n    parameter('overwrite ? false) { overwrite =>\n      if (!RESERVED_NAMES.contains(entityName.name.asString)) {\n        entity(as[WhiskPackagePut]) { content =>\n          val request = content.resolve(entityName.namespace)\n          request.binding.map { b =>\n            logging.debug(this, \"checking if package is accessible\")\n          }\n          val referencedentities = referencedEntities(request)\n\n          onComplete(entitlementProvider.check(user, Privilege.READ, referencedentities)) {\n            case Success(_) =>\n              putEntity(\n                WhiskPackage,\n                entityStore,\n                entityName.toDocId,\n                overwrite,\n                update(request) _,\n                () => create(request, entityName))\n            case Failure(f) =>\n              rewriteEntitlementFailure(f)\n          }\n        }\n      } else {\n        terminate(BadRequest, Messages.packageNameIsReserved(entityName.name.asString))\n      }\n    }\n  }\n\n  /**\n   * Activating a package is not supported. This method is not permitted and is not reachable.\n   *\n   * Responses are one of (Code, Message)\n   * - 405 Not Allowed\n   */\n  override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId) = {\n    logging.error(this, \"activate is not permitted on packages\")\n    reject\n  }\n\n  /**\n   * Deletes package/binding. If a package, may only be deleted if there are no entities in the package\n   * or force parameter is set to true, which will delete all contents of the package before deleting.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskPackage as JSON\n   * - 404 Not Found\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n    parameter('force ? false) { force =>\n      deleteEntity(\n        WhiskPackage,\n        entityStore,\n        entityName.toDocId,\n        (wp: WhiskPackage) => {\n          wp.binding map {\n            // this is a binding, it is safe to remove\n            _ =>\n              Future.successful({})\n          } getOrElse {\n            // may only delete a package if all its ingredients are deleted already or force flag is set\n            WhiskAction\n              .listCollectionInNamespace(\n                entityStore,\n                wp.namespace.addPath(wp.name),\n                includeDocs = true,\n                skip = 0,\n                limit = 0) flatMap {\n              case Right(list) if list.nonEmpty && force =>\n                Future sequence {\n                  list.map(action => {\n                    WhiskAction.get(\n                      entityStore,\n                      wp.fullyQualifiedName(false)\n                        .add(action.fullyQualifiedName(false).name)\n                        .toDocId) flatMap { actionWithRevision =>\n                      WhiskAction.del(entityStore, actionWithRevision.docinfo)\n                    }\n                  })\n                } flatMap { _ =>\n                  Future.successful({})\n                }\n              case Right(list) if list.nonEmpty && !force =>\n                Future failed {\n                  RejectRequest(\n                    Conflict,\n                    s\"Package not empty (contains ${list.size} ${if (list.size == 1) \"entity\" else \"entities\"}). Set force param or delete package contents.\")\n                }\n              case _ =>\n                Future.successful({})\n            }\n          }\n        })\n    }\n  }\n\n  /**\n   * Gets package/binding.\n   * The package/binding name is prefixed with the namespace to create the primary index key.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskPackage has JSON\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId) = {\n    if (executeOnly && user.namespace.name != entityName.namespace) {\n      val value = entityName.toString\n      terminate(Forbidden, forbiddenGetPackage(entityName.asString))\n    } else {\n      getEntity(WhiskPackage.get(entityStore, entityName.toDocId), Some {\n        mergePackageWithBinding() _\n      })\n    }\n  }\n\n  /**\n   * Gets all packages/bindings in namespace.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 [] or [WhiskPackage as JSON]\n   * - 500 Internal Server Error\n   */\n  override def list(user: Identity, namespace: EntityPath)(implicit transid: TransactionId) = {\n    parameter(\n      'skip.as[ListSkip] ? ListSkip(collection.defaultListSkip),\n      'limit.as[ListLimit] ? ListLimit(collection.defaultListLimit),\n      'count ? false) { (skip, limit, count) =>\n      val viewName = if (user.namespace.name.toPath == namespace) WhiskPackage.view else WhiskPackage.publicPackagesView\n      if (!count) {\n        listEntities {\n          WhiskPackage\n            .listCollectionInNamespace(\n              entityStore,\n              namespace,\n              skip.n,\n              limit.n,\n              includeDocs = false,\n              viewName = viewName)\n            .map(_.fold((js) => js, (ps) => ps.map(WhiskPackage.serdes.write(_))))\n        }\n      } else {\n        countEntities {\n          WhiskPackage.countCollectionInNamespace(entityStore, namespace, skip.n, viewName = viewName)\n        }\n      }\n    }\n  }\n\n  /**\n   * Validates that a referenced binding exists.\n   */\n  private def checkBinding(binding: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[Unit] = {\n    WhiskPackage.get(entityStore, binding.toDocId) recoverWith {\n      case t: NoDocumentException => Future.failed(RejectRequest(BadRequest, Messages.bindingDoesNotExist))\n      case t: DocumentTypeMismatchException =>\n        Future.failed(RejectRequest(Conflict, Messages.requestedBindingIsNotValid))\n      case t => Future.failed(RejectRequest(BadRequest, t))\n    } flatMap {\n      // trying to create a new package binding that refers to another binding\n      case provider if provider.binding.nonEmpty =>\n        Future.failed(RejectRequest(BadRequest, Messages.bindingCannotReferenceBinding))\n      // or creating a package binding that refers to a package\n      case _ => Future.successful({})\n    }\n  }\n\n  /**\n   * Creates a WhiskPackage from PUT content, generating default values where necessary.\n   * If this is a binding, confirm the referenced package exists.\n   */\n  private def create(content: WhiskPackagePut, pkgName: FullyQualifiedEntityName)(\n    implicit transid: TransactionId): Future[WhiskPackage] = {\n    val validateBinding = content.binding map { b =>\n      checkBinding(b.fullyQualifiedName)\n    } getOrElse Future.successful({})\n\n    validateBinding map { _ =>\n      WhiskPackage(\n        pkgName.path,\n        pkgName.name,\n        content.binding,\n        content.parameters getOrElse Parameters(),\n        content.version getOrElse SemVer(),\n        content.publish getOrElse false,\n        // remove any binding annotation from PUT (always set by the controller)\n        (content.annotations getOrElse Parameters())\n          - WhiskPackage.bindingFieldName\n          ++ bindingAnnotation(content.binding))\n    }\n  }\n\n  /** Updates a WhiskPackage from PUT content, merging old package/binding where necessary. */\n  private def update(content: WhiskPackagePut)(wp: WhiskPackage)(\n    implicit transid: TransactionId): Future[WhiskPackage] = {\n    val validateBinding = content.binding map { binding =>\n      wp.binding map {\n        // pre-existing entity is a binding, check that new binding is valid\n        _ =>\n          checkBinding(binding.fullyQualifiedName)\n      } getOrElse {\n        // pre-existing entity is a package, cannot make it a binding\n        Future.failed(RejectRequest(Conflict, Messages.packageCannotBecomeBinding))\n      }\n    } getOrElse Future.successful({})\n\n    validateBinding map { _ =>\n      WhiskPackage(\n        wp.namespace,\n        wp.name,\n        content.binding orElse wp.binding,\n        content.parameters getOrElse wp.parameters,\n        content.version getOrElse wp.version.upPatch,\n        content.publish getOrElse wp.publish,\n        // override any binding annotation from PUT (always set by the controller)\n        (content.annotations getOrElse wp.annotations)\n          - WhiskPackage.bindingFieldName\n          ++ bindingAnnotation(content.binding orElse wp.binding)).revision[WhiskPackage](wp.docinfo.rev)\n    }\n  }\n\n  private def rewriteEntitlementFailure(failure: Throwable)(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult] = {\n    logging.debug(this, s\"rewriting failure $failure\")\n    failure match {\n      case RejectRequest(NotFound, _) => terminate(BadRequest, Messages.bindingDoesNotExist)\n      case RejectRequest(Conflict, _) => terminate(Conflict, Messages.requestedBindingIsNotValid)\n      case _                          => super.handleEntitlementFailure(failure)\n    }\n  }\n\n  /**\n   * Constructs a \"binding\" annotation. This is redundant with the binding\n   * information available in WhiskPackage but necessary for some clients which\n   * fetch package lists but cannot determine which package may be bound. An\n   * alternative is to include the binding in the package list \"view\" but this\n   * will require an API change. So using an annotation instead.\n   */\n  private def bindingAnnotation(binding: Option[Binding]): Parameters = {\n    binding map { b =>\n      Parameters(WhiskPackage.bindingFieldName, Binding.serdes.write(b))\n    } getOrElse Parameters()\n  }\n\n  /**\n   * Constructs a WhiskPackage that is a merger of a package with its packing binding (if any).\n   * If this is a binding, fetch package for binding, merge parameters then emit.\n   * Otherwise this is a package, emit it.\n   */\n  private def mergePackageWithBinding(ref: Option[WhiskPackage] = None)(wp: WhiskPackage)(\n    implicit transid: TransactionId): RequestContext => Future[RouteResult] = {\n    wp.binding map {\n      case b: Binding =>\n        val docid = b.fullyQualifiedName.toDocId\n        logging.debug(this, s\"fetching package '$docid' for reference\")\n        if (docid == wp.docid) {\n          logging.error(this, s\"unexpected package binding refers to itself: $docid\")\n          terminate(UnprocessableContent, Messages.packageBindingCircularReference(b.fullyQualifiedName.toString))\n        } else {\n\n          /** Here's where I check package execute only case with package binding. */\n          if (executeOnly && wp.namespace.asString != b.namespace.asString) {\n            terminate(Forbidden, forbiddenGetPackageBinding(wp.name.asString))\n          } else {\n            getEntity(WhiskPackage.get(entityStore, docid), Some {\n              mergePackageWithBinding(Some {\n                wp\n              }) _\n            })\n          }\n        }\n    } getOrElse {\n      val pkg = ref map { _ inherit wp.parameters } getOrElse wp\n      logging.debug(this, s\"fetching package actions in '${wp.fullPath}'\")\n      val actions = WhiskAction.listCollectionInNamespace(entityStore, wp.fullPath, skip = 0, limit = 0) flatMap {\n        case Left(list) =>\n          Future.successful {\n            pkg withPackageActions (list map { o =>\n              WhiskPackageAction.serdes.read(o)\n            })\n          }\n        case t =>\n          Future.failed {\n            logging.error(this, \"unexpected result in package action lookup: $t\")\n            new IllegalStateException(s\"unexpected result in package action lookup: $t\")\n          }\n      }\n\n      onComplete(actions) {\n        case Success(p) =>\n          logging.debug(this, s\"[GET] entity success\")\n          complete(OK, p)\n        case Failure(t) =>\n          logging.error(this, s\"[GET] failed: ${t.getMessage}\")\n          terminate(InternalServerError)\n      }\n    }\n  }\n\n  /** Custom unmarshaller for query parameters \"limit\" for \"list\" operations. */\n  private implicit val stringToListLimit: Unmarshaller[String, ListLimit] = RestApiCommons.stringToListLimit(collection)\n\n  /** Custom unmarshaller for query parameters \"skip\" for \"list\" operations. */\n  private implicit val stringToListSkip: Unmarshaller[String, ListSkip] = RestApiCommons.stringToListSkip(collection)\n\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/RestAPIs.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport org.apache.pekko.http.scaladsl.server.directives.AuthenticationDirective\nimport org.apache.pekko.http.scaladsl.server.{Directives, Route}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.containerpool.logging.LogStore\nimport org.apache.openwhisk.core.database.{ActivationStore, CacheChangeNotification}\nimport org.apache.openwhisk.core.entitlement._\nimport org.apache.openwhisk.core.entity.ActivationId.ActivationIdGenerator\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types._\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancer\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.http.{CorsSettings, Messages}\nimport org.apache.openwhisk.spi.{Spi, SpiLoader}\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success, Try}\n\n/**\n * Abstract class which provides basic Directives which are used to construct route structures\n * which are common to all versions of the Rest API.\n */\nprotected[controller] class SwaggerDocs(apipath: Uri.Path, doc: String)(implicit actorSystem: ActorSystem)\n    extends Directives {\n  case class SwaggerConfig(fileSystem: Boolean, dirPath: String)\n\n  /** Swagger end points. */\n  protected val swaggeruipath = \"docs\"\n  protected val swaggerdocpath = \"api-docs\"\n  private val swaggerConfig = loadConfigOrThrow[SwaggerConfig](ConfigKeys.swaggerUi)\n\n  def basepath(url: Uri.Path = apipath): String = {\n    (if (url.startsWithSlash) url else Uri.Path./(url)).toString\n  }\n\n  /**\n   * Defines the routes to serve the swagger docs.\n   */\n  val swaggerRoutes: Route = {\n    pathPrefix(swaggeruipath) {\n      if (swaggerConfig.fileSystem) getFromDirectory(swaggerConfig.dirPath)\n      else getFromResourceDirectory(swaggerConfig.dirPath)\n    } ~ path(swaggeruipath) {\n      redirect(s\"$swaggeruipath/index.html?url=$apiDocsUrl\", PermanentRedirect)\n    } ~ pathPrefix(swaggerdocpath) {\n      pathEndOrSingleSlash {\n        getFromResource(doc)\n      }\n    }\n  }\n\n  /** Forces add leading slash for swagger api-doc url rewrite to work. */\n  private def apiDocsUrl = basepath(apipath / swaggerdocpath)\n}\n\nprotected[controller] object RestApiCommons {\n  def requiredProperties =\n    Map(WhiskConfig.servicePort -> 8080.toString) ++\n      EntitlementProvider.requiredProperties ++\n      WhiskActionsApi.requiredProperties\n\n  import org.apache.pekko.http.scaladsl.model.HttpCharsets\n  import org.apache.pekko.http.scaladsl.model.MediaTypes.`application/json`\n  import org.apache.pekko.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}\n\n  /**\n   * Extract an empty entity into a JSON object. This is useful for the\n   * main APIs which accept JSON content type by default but may accept\n   * no entity in the request.\n   */\n  implicit val emptyEntityToJsObject: FromEntityUnmarshaller[JsObject] = {\n    Unmarshaller.byteStringUnmarshaller.forContentTypes(`application/json`).mapWithCharset { (data, charset) =>\n      if (data.size == 0) {\n        JsObject.empty\n      } else {\n        val input = {\n          if (charset == HttpCharsets.`UTF-8`) ParserInput(data.toArray)\n          else ParserInput(data.decodeString(charset.nioCharset))\n        }\n\n        JsonParser(input).asJsObject\n      }\n    }\n  }\n\n  /** Custom unmarshaller for query parameters \"limit\" for \"list\" operations. */\n  case class ListLimit(n: Int)\n\n  def stringToListLimit(collection: Collection): Unmarshaller[String, ListLimit] = {\n    Unmarshaller.strict[String, ListLimit] { value =>\n      Try { value.toInt } match {\n        case Success(n) if (n == 0)                                  => ListLimit(Collection.MAX_LIST_LIMIT)\n        case Success(n) if (n > 0 && n <= Collection.MAX_LIST_LIMIT) => ListLimit(n)\n        case Success(n) =>\n          throw new IllegalArgumentException(\n            Messages.listLimitOutOfRange(collection.path, n, Collection.MAX_LIST_LIMIT))\n        case Failure(t) => throw new IllegalArgumentException(Messages.argumentNotInteger(collection.path, value))\n      }\n    }\n  }\n\n  /** Custom unmarshaller for query parameters \"skip\" for \"list\" operations. */\n  case class ListSkip(n: Int)\n\n  def stringToListSkip(collection: Collection): Unmarshaller[String, ListSkip] = {\n    Unmarshaller.strict[String, ListSkip] { value =>\n      Try { value.toInt } match {\n        case Success(n) if (n >= 0) => ListSkip(n)\n        case Success(n) =>\n          throw new IllegalArgumentException(Messages.listSkipOutOfRange(collection.path, n))\n        case Failure(t) => throw new IllegalArgumentException(Messages.argumentNotInteger(collection.path, value))\n      }\n    }\n  }\n\n  /** Pretty print JSON response. */\n  implicit val jsonPrettyResponsePrinter = PrettyPrinter\n\n  /** Standard compact JSON printer. */\n  implicit val jsonDefaultResponsePrinter = CompactPrinter\n}\n\n/**\n * A trait for wrapping routes with headers to include in response.\n * Useful for CORS.\n */\nprotected[controller] trait RespondWithHeaders extends Directives with CorsSettings.RestAPIs {\n  val sendCorsHeaders = respondWithHeaders(allowOrigin, allowHeaders, allowMethods)\n}\n\ncase class WhiskInformation(buildNo: String, date: String)\n\nclass RestAPIVersion(config: WhiskConfig, apiPath: String, apiVersion: String)(\n  implicit val activeAckTopicIndex: ControllerInstanceId,\n  implicit val actorSystem: ActorSystem,\n  implicit val logging: Logging,\n  implicit val entityStore: EntityStore,\n  implicit val entitlementProvider: EntitlementProvider,\n  implicit val activationIdFactory: ActivationIdGenerator,\n  implicit val loadBalancer: LoadBalancer,\n  implicit val cacheChangeNotification: Some[CacheChangeNotification],\n  implicit val activationStore: ActivationStore,\n  implicit val logStore: LogStore,\n  implicit val whiskConfig: WhiskConfig)\n    extends SwaggerDocs(Uri.Path(apiPath) / apiVersion, \"apiv1swagger.json\")\n    with RespondWithHeaders {\n  implicit val executionContext = actorSystem.dispatcher\n  implicit val authStore = WhiskAuthStore.datastore()\n  val whiskInfo = loadConfigOrThrow[WhiskInformation](ConfigKeys.buildInformation)\n\n  private implicit val authenticationDirectiveProvider =\n    SpiLoader.get[AuthenticationDirectiveProvider]\n\n  def prefix = pathPrefix(apiPath / apiVersion)\n\n  /**\n   * Describes details of a particular API path.\n   */\n  val info = (pathEndOrSingleSlash & get) {\n    complete(\n      JsObject(\n        \"description\" -> \"OpenWhisk API\".toJson,\n        \"api_version\" -> SemVer(1, 0, 0).toJson,\n        \"api_version_path\" -> apiVersion.toJson,\n        \"build\" -> whiskInfo.date.toJson,\n        \"buildno\" -> whiskInfo.buildNo.toJson,\n        \"swagger_paths\" -> JsObject(\"ui\" -> s\"/$swaggeruipath\".toJson, \"api-docs\" -> s\"/$swaggerdocpath\".toJson)))\n  }\n\n  def routes(implicit transid: TransactionId): Route = {\n    prefix {\n      sendCorsHeaders {\n        info ~\n          authenticationDirectiveProvider.authenticate(transid, authStore, logging) { user =>\n            namespaces.routes(user) ~\n              pathPrefix(Collection.NAMESPACES) {\n                actions.routes(user) ~\n                  triggers.routes(user) ~\n                  rules.routes(user) ~\n                  activations.routes(user) ~\n                  packages.routes(user) ~\n                  limits.routes(user)\n              }\n          } ~\n          swaggerRoutes\n      } ~ {\n        // web actions are distinct to separate the cors header\n        // and allow the actions themselves to respond to options\n        authenticationDirectiveProvider.authenticate(transid, authStore, logging) { user =>\n          web.routes(user)\n        } ~ {\n          web.routes()\n        } ~\n          options {\n            sendCorsHeaders {\n              complete(OK)\n            }\n          }\n      }\n    }\n  }\n\n  private val namespaces = new NamespacesApi(apiPath, apiVersion)\n  private val actions = new ActionsApi(apiPath, apiVersion)\n  private val packages = new PackagesApi(apiPath, apiVersion)\n  private val triggers = new TriggersApi(apiPath, apiVersion)\n  private val activations = new ActivationsApi(apiPath, apiVersion)\n  private val rules = new RulesApi(apiPath, apiVersion)\n  private val limits = new LimitsApi(apiPath, apiVersion)\n  private val web = new WebActionsApi(Seq(\"web\"), new WebApiDirectives())\n\n  class NamespacesApi(val apiPath: String, val apiVersion: String) extends WhiskNamespacesApi\n\n  class LimitsApi(val apiPath: String, val apiVersion: String)(\n    implicit override val entitlementProvider: EntitlementProvider,\n    override val executionContext: ExecutionContext,\n    override val whiskConfig: WhiskConfig)\n      extends WhiskLimitsApi\n\n  class ActionsApi(val apiPath: String, val apiVersion: String)(\n    implicit override val actorSystem: ActorSystem,\n    override val activeAckTopicIndex: ControllerInstanceId,\n    override val entityStore: EntityStore,\n    override val activationStore: ActivationStore,\n    override val entitlementProvider: EntitlementProvider,\n    override val activationIdFactory: ActivationIdGenerator,\n    override val loadBalancer: LoadBalancer,\n    override val cacheChangeNotification: Some[CacheChangeNotification],\n    override val executionContext: ExecutionContext,\n    override val logging: Logging,\n    override val whiskConfig: WhiskConfig)\n      extends WhiskActionsApi\n      with WhiskServices {\n    logging.info(this, s\"actionSequenceLimit '${whiskConfig.actionSequenceLimit}'\")(TransactionId.controller)\n    assert(whiskConfig.actionSequenceLimit.toInt > 0)\n  }\n\n  class ActivationsApi(val apiPath: String, val apiVersion: String)(\n    implicit override val activationStore: ActivationStore,\n    override val logStore: LogStore,\n    override val entitlementProvider: EntitlementProvider,\n    override val executionContext: ExecutionContext,\n    override val logging: Logging)\n      extends WhiskActivationsApi\n\n  class PackagesApi(val apiPath: String, val apiVersion: String)(\n    implicit override val entityStore: EntityStore,\n    override val entitlementProvider: EntitlementProvider,\n    override val activationIdFactory: ActivationIdGenerator,\n    override val loadBalancer: LoadBalancer,\n    override val cacheChangeNotification: Some[CacheChangeNotification],\n    override val executionContext: ExecutionContext,\n    override val logging: Logging,\n    override val whiskConfig: WhiskConfig)\n      extends WhiskPackagesApi\n      with WhiskServices\n\n  class RulesApi(val apiPath: String, val apiVersion: String)(\n    implicit override val actorSystem: ActorSystem,\n    override val entityStore: EntityStore,\n    override val entitlementProvider: EntitlementProvider,\n    override val activationIdFactory: ActivationIdGenerator,\n    override val loadBalancer: LoadBalancer,\n    override val cacheChangeNotification: Some[CacheChangeNotification],\n    override val executionContext: ExecutionContext,\n    override val logging: Logging,\n    override val whiskConfig: WhiskConfig)\n      extends WhiskRulesApi\n      with WhiskServices\n\n  class TriggersApi(val apiPath: String, val apiVersion: String)(\n    implicit override val actorSystem: ActorSystem,\n    implicit override val entityStore: EntityStore,\n    override val entitlementProvider: EntitlementProvider,\n    override val activationStore: ActivationStore,\n    override val activationIdFactory: ActivationIdGenerator,\n    override val loadBalancer: LoadBalancer,\n    override val cacheChangeNotification: Some[CacheChangeNotification],\n    override val executionContext: ExecutionContext,\n    override val logging: Logging,\n    override val whiskConfig: WhiskConfig)\n      extends WhiskTriggersApi\n      with WhiskServices\n\n  protected[controller] class WebActionsApi(override val webInvokePathSegments: Seq[String],\n                                            override val webApiDirectives: WebApiDirectives)(\n    implicit override val authStore: AuthStore,\n    implicit val entityStore: EntityStore,\n    override val activeAckTopicIndex: ControllerInstanceId,\n    override val activationStore: ActivationStore,\n    override val entitlementProvider: EntitlementProvider,\n    override val activationIdFactory: ActivationIdGenerator,\n    override val loadBalancer: LoadBalancer,\n    override val actorSystem: ActorSystem,\n    override val executionContext: ExecutionContext,\n    override val logging: Logging,\n    override val whiskConfig: WhiskConfig)\n      extends WhiskWebActionsApi\n      with WhiskServices\n}\n\ntrait AuthenticationDirectiveProvider extends Spi {\n\n  /**\n   * Returns an authentication directive used to validate the\n   * passed user credentials.\n   * At runtime the directive returns an user identity\n   * which is passed to the following routes.\n   *\n   * @return authentication directive used to verify the user credentials\n   */\n  def authenticate(implicit transid: TransactionId,\n                   authStore: AuthStore,\n                   logging: Logging): AuthenticationDirective[Identity]\n\n  /**\n   * Retrieves an Identity based on a given namespace name.\n   *\n   * For use-cases of anonymous invocation (i.e. WebActions),\n   * we need to an identity based on a given namespace-name to\n   * give the invocation all the context needed.\n   *\n   * @param namespace the namespace that the identity will be based on\n   * @return identity based on the given namespace\n   */\n  def identityByNamespace(\n    namespace: EntityName)(implicit transid: TransactionId, system: ActorSystem, authStore: AuthStore): Future[Identity]\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Rules.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.server.StandardRoute\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshaller\nimport spray.json.DeserializationException\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.controller.RestApiCommons.{ListLimit, ListSkip}\nimport org.apache.openwhisk.core.database.{CacheChangeNotification, DocumentConflictException, NoDocumentException}\nimport org.apache.openwhisk.core.entitlement.{Collection, Privilege, ReferencedEntities}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types.EntityStore\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport org.apache.openwhisk.http.Messages._\n\nimport scala.concurrent.Future\nimport scala.util.{Failure, Success}\n\n/** A trait implementing the rules API */\ntrait WhiskRulesApi extends WhiskCollectionAPI with ReferencedEntities {\n  services: WhiskServices =>\n\n  protected override val collection = Collection(Collection.RULES)\n\n  /** An actor system for timed based futures. */\n  protected implicit val actorSystem: ActorSystem\n\n  /** Database service to CRUD rules. */\n  protected val entityStore: EntityStore\n\n  /** JSON response formatter. */\n  import RestApiCommons.jsonDefaultResponsePrinter\n\n  /** Notification service for cache invalidation. */\n  protected implicit val cacheChangeNotification: Some[CacheChangeNotification]\n\n  /** Path to Rules REST API. */\n  protected val rulesPath = \"rules\"\n\n  /**\n   * Creates or updates rule if it already exists. The PUT content is deserialized into a WhiskRulePut\n   * which is a subset of WhiskRule (it eschews the namespace, entity name and status since the former\n   * are derived from the authenticated user and the URI and the status is managed automatically).\n   * The WhiskRulePut is merged with the existing WhiskRule in the datastore, overriding old values\n   * with new values that are defined. Any values not defined in the PUT content are replaced with\n   * old values.\n   *\n   * The rule will not update if the status of the entity in the datastore is not INACTIVE. It rejects\n   * such requests with Conflict.\n   *\n   * The create/update is also guarded by a predicate that confirm the trigger and action are valid.\n   * Otherwise rejects the request with Bad Request and an appropriate message. It is true that the\n   * trigger/action may be deleted after creation but at the very least confirming dependences here\n   * prevents use errors where a rule is created with an invalid trigger/action which then fails\n   * testing (fire a trigger and expect an action activation to occur).\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskRule as JSON\n   * - 400 Bad Request\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n    parameter('overwrite ? false) { overwrite =>\n      entity(as[WhiskRulePut]) { content =>\n        val request = content.resolve(entityName.namespace)\n        onComplete(entitlementProvider.check(user, Privilege.READ, referencedEntities(request))) {\n          case Success(_) =>\n            putEntity(\n              WhiskRule,\n              entityStore,\n              entityName.toDocId,\n              overwrite,\n              update(request) _,\n              () => {\n                create(request, entityName)\n              },\n              postProcess = Some { rule: WhiskRule =>\n                if (overwrite == true) {\n                  val getRuleWithStatus = getTrigger(rule.trigger) map { trigger =>\n                    getStatus(trigger, FullyQualifiedEntityName(rule.namespace, rule.name))\n                  } map { status =>\n                    rule.withStatus(status)\n                  }\n\n                  onComplete(getRuleWithStatus) {\n                    case Success(r) => completeAsRuleResponse(rule, r.status)\n                    case Failure(t) => terminate(InternalServerError)\n                  }\n                } else {\n                  completeAsRuleResponse(rule, Status.ACTIVE)\n                }\n              })\n          case Failure(f) =>\n            handleEntitlementFailure(f)\n        }\n      }\n    }\n  }\n\n  /**\n   * Toggles rule status from enabled -> disabled and vice versa. The action are not confirmed\n   * to still exist. This is deferred to trigger activation which will fail to post activations\n   * for non-existent actions.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 OK rule in desired state\n   * - 202 Accepted rule state change accepted\n   * - 404 Not Found\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId) = {\n    extractStatusRequest { requestedState =>\n      val docid = entityName.toDocId\n\n      getEntity(WhiskRule.get(entityStore, docid), Some {\n        rule: WhiskRule =>\n          val ruleName = rule.fullyQualifiedName(false)\n\n          val changeStatus = getTrigger(rule.trigger) map { trigger =>\n            getStatus(trigger, ruleName)\n          } flatMap {\n            oldStatus =>\n              if (requestedState != oldStatus) {\n                logging.debug(this, s\"[POST] rule state change initiated: ${oldStatus} -> $requestedState\")\n                Future successful requestedState\n              } else {\n                logging.debug(\n                  this,\n                  s\"[POST] rule state will not be changed, the requested state is the same as the old state: ${oldStatus} -> $requestedState\")\n                Future failed { IgnoredRuleActivation(requestedState == oldStatus) }\n              }\n          } flatMap {\n            case (newStatus) =>\n              logging.debug(this, s\"[POST] attempting to set rule state to: ${newStatus}\")\n              WhiskTrigger.get(entityStore, rule.trigger.toDocId) flatMap { trigger =>\n                val newTrigger = trigger.removeRule(ruleName)\n                val triggerLink = ReducedRule(rule.action, newStatus)\n                WhiskTrigger.put(entityStore, newTrigger.addRule(ruleName, triggerLink), Some(trigger))\n              }\n          }\n\n          onComplete(changeStatus) {\n            case Success(response) =>\n              complete(OK)\n            case Failure(t) =>\n              t match {\n                case _: DocumentConflictException =>\n                  logging.debug(this, s\"[POST] rule update conflict\")\n                  terminate(Conflict, conflictMessage)\n                case IgnoredRuleActivation(ok) =>\n                  logging.debug(this, s\"[POST] rule update ignored\")\n                  if (ok) complete(OK) else terminate(Conflict)\n                case _: NoDocumentException =>\n                  logging.debug(this, s\"[POST] the trigger attached to the rule doesn't exist\")\n                  terminate(NotFound, \"Only rules with existing triggers can be activated\")\n                case _: DeserializationException =>\n                  logging.error(this, s\"[POST] rule update failed: ${t.getMessage}\")\n                  terminate(InternalServerError, corruptedEntity)\n                case _: Throwable =>\n                  logging.error(this, s\"[POST] rule update failed: ${t.getMessage}\")\n                  terminate(InternalServerError)\n              }\n          }\n      })\n    }\n  }\n\n  /**\n   * Deletes rule iff rule is inactive.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskRule as JSON\n   * - 404 Not Found\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n    deleteEntity(\n      WhiskRule,\n      entityStore,\n      entityName.toDocId,\n      (r: WhiskRule) => {\n        val ruleName = FullyQualifiedEntityName(r.namespace, r.name)\n        getTrigger(r.trigger) map { trigger =>\n          (getStatus(trigger, ruleName), trigger)\n        } flatMap {\n          case (status, triggerOpt) =>\n            triggerOpt map { trigger =>\n              WhiskTrigger.put(entityStore, trigger.removeRule(ruleName), triggerOpt) map { _ =>\n                {}\n              }\n            } getOrElse Future.successful({})\n        }\n      },\n      postProcess = Some { rule: WhiskRule =>\n        completeAsRuleResponse(rule, Status.INACTIVE)\n      })\n  }\n\n  /**\n   * Gets rule. The rule name is prefixed with the namespace to create the primary index key.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskRule has JSON\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId) = {\n    getEntity(\n      WhiskRule.get(entityStore, entityName.toDocId),\n      Some { rule: WhiskRule =>\n        val getRuleWithStatus = getTrigger(rule.trigger) map { trigger =>\n          getStatus(trigger, entityName)\n        } map { status =>\n          rule.withStatus(status)\n        }\n\n        onComplete(getRuleWithStatus) {\n          case Success(r) => complete(OK, r)\n          case Failure(t) => terminate(InternalServerError)\n        }\n      })\n  }\n\n  /**\n   * Gets all rules in namespace.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 [] or [WhiskRule as JSON]\n   * - 500 Internal Server Error\n   */\n  override def list(user: Identity, namespace: EntityPath)(implicit transid: TransactionId) = {\n    parameter(\n      'skip.as[ListSkip] ? ListSkip(collection.defaultListSkip),\n      'limit.as[ListLimit] ? ListLimit(collection.defaultListLimit),\n      'count ? false) { (skip, limit, count) =>\n      if (!count) {\n        listEntities {\n          WhiskRule.listCollectionInNamespace(entityStore, namespace, skip.n, limit.n, includeDocs = true) map { list =>\n            list.fold((js) => js, (rls) => rls.map(WhiskRule.serdes.write(_)))\n          }\n        }\n      } else {\n        countEntities {\n          WhiskRule.countCollectionInNamespace(entityStore, namespace, skip.n)\n        }\n      }\n    }\n  }\n\n  /** Creates a WhiskRule from PUT content, generating default values where necessary. */\n  private def create(content: WhiskRulePut, ruleName: FullyQualifiedEntityName)(\n    implicit transid: TransactionId): Future[WhiskRule] = {\n    if (content.trigger.isDefined && content.action.isDefined) {\n      val triggerName = content.trigger.get\n      val actionName = content.action.get\n\n      checkTriggerAndActionExist(triggerName, actionName) recoverWith {\n        case t => Future.failed(RejectRequest(BadRequest, t))\n      } flatMap {\n        case (trigger, action) =>\n          val rule = WhiskRule(\n            ruleName.path,\n            ruleName.name,\n            content.trigger.get,\n            content.action.get,\n            content.version getOrElse SemVer(),\n            content.publish getOrElse false,\n            content.annotations getOrElse Parameters())\n\n          val triggerLink = ReducedRule(actionName, Status.ACTIVE)\n          logging.debug(this, s\"about to put ${trigger.addRule(ruleName, triggerLink)}\")\n          WhiskTrigger.put(entityStore, trigger.addRule(ruleName, triggerLink), old = None) map { _ =>\n            rule\n          }\n      }\n    } else Future.failed(RejectRequest(BadRequest, \"rule requires a valid trigger and a valid action\"))\n  }\n\n  /** Updates a WhiskTrigger from PUT content, merging old trigger where necessary. */\n  private def update(content: WhiskRulePut)(rule: WhiskRule)(implicit transid: TransactionId): Future[WhiskRule] = {\n    val ruleName = FullyQualifiedEntityName(rule.namespace, rule.name)\n    val oldTriggerName = rule.trigger\n\n    getTrigger(oldTriggerName) flatMap { oldTriggerOpt =>\n      val status = getStatus(oldTriggerOpt, ruleName)\n      val newTriggerEntity = content.trigger getOrElse rule.trigger\n      val newTriggerName = newTriggerEntity\n\n      val actionEntity = content.action getOrElse rule.action\n      val actionName = actionEntity\n\n      checkTriggerAndActionExist(newTriggerName, actionName) recoverWith {\n        case t => Future.failed(RejectRequest(BadRequest, t))\n      } flatMap {\n        case (newTrigger, newAction) =>\n          val r = WhiskRule(\n            rule.namespace,\n            rule.name,\n            newTriggerEntity,\n            actionEntity,\n            content.version getOrElse rule.version.upPatch,\n            content.publish getOrElse rule.publish,\n            content.annotations getOrElse rule.annotations).revision[WhiskRule](rule.docinfo.rev)\n\n          // Deletes reference from the old trigger iff it is different from the new one\n          val deleteOldLink = for {\n            isDifferentTrigger <- content.trigger.filter(_ => newTriggerName != oldTriggerName)\n            oldTrigger <- oldTriggerOpt\n          } yield {\n            WhiskTrigger.put(entityStore, oldTrigger.removeRule(ruleName), oldTriggerOpt)\n          }\n\n          val triggerLink = ReducedRule(actionName, status)\n          val update = WhiskTrigger.put(entityStore, newTrigger.addRule(ruleName, triggerLink), oldTriggerOpt)\n          Future.sequence(Seq(deleteOldLink.getOrElse(Future.successful(true)), update)).map(_ => r)\n      }\n    }\n  }\n\n  /**\n   * Gets a WhiskTrigger defined by the given DocInfo. Gracefully falls back to None iff the trigger is not found.\n   *\n   * @param tid DocInfo defining the trigger to get\n   * @return a WhiskTrigger iff found, else None\n   */\n  private def getTrigger(t: FullyQualifiedEntityName)(implicit transid: TransactionId): Future[Option[WhiskTrigger]] = {\n    WhiskTrigger.get(entityStore, t.toDocId) map { trigger =>\n      Some(trigger)\n    } recover {\n      case _: NoDocumentException | DeserializationException(_, _, _) => None\n    }\n  }\n\n  /**\n   * Extracts the Status for the rule out of a WhiskTrigger that may be there. Falls back to INACTIVE if the trigger\n   * could not be found or the rule being worked on has not yet been written into the trigger record.\n   *\n   * @param triggerOpt Option containing a WhiskTrigger\n   * @param ruleName Namespace the name of the rule being worked on\n   * @return Status of the rule\n   */\n  private def getStatus(triggerOpt: Option[WhiskTrigger], ruleName: FullyQualifiedEntityName)(\n    implicit transid: TransactionId): Status = {\n    val statusFromTrigger = for {\n      trigger <- triggerOpt\n      rules <- trigger.rules\n      rule <- rules.get(ruleName)\n    } yield {\n      rule.status\n    }\n    statusFromTrigger getOrElse Status.INACTIVE\n  }\n\n  /**\n   * Completes an HTTP request with a WhiskRule including the computed Status\n   *\n   * @param rule the rule to send\n   * @param status the status to include in the response\n   */\n  private def completeAsRuleResponse(rule: WhiskRule, status: Status = Status.INACTIVE): StandardRoute = {\n    complete(OK, rule.withStatus(status))\n  }\n\n  /**\n   * Checks if trigger and action are valid documents (that is, they exist) in the datastore.\n   *\n   * @param trigger the trigger id\n   * @param action the action id\n   * @return future that completes with references trigger and action if they exist\n   */\n  private def checkTriggerAndActionExist(trigger: FullyQualifiedEntityName, action: FullyQualifiedEntityName)(\n    implicit transid: TransactionId): Future[(WhiskTrigger, WhiskActionMetaData)] = {\n\n    for {\n      triggerExists <- WhiskTrigger.get(entityStore, trigger.toDocId) recoverWith {\n        case _: NoDocumentException =>\n          Future.failed {\n            new NoDocumentException(s\"trigger ${trigger.qualifiedNameWithLeadingSlash} does not exist\")\n          }\n        case _: DeserializationException =>\n          Future.failed {\n            new DeserializationException(s\"trigger ${trigger.qualifiedNameWithLeadingSlash} is corrupted\")\n          }\n      }\n\n      actionExists <- WhiskAction.resolveAction(entityStore, action) flatMap { resolvedName =>\n        WhiskActionMetaData.get(entityStore, resolvedName.toDocId)\n      } recoverWith {\n        case _: NoDocumentException =>\n          Future.failed {\n            new NoDocumentException(s\"action ${action.qualifiedNameWithLeadingSlash} does not exist\")\n          }\n        case _: DeserializationException =>\n          Future.failed {\n            new DeserializationException(s\"action ${action.qualifiedNameWithLeadingSlash} is corrupted\")\n          }\n      }\n    } yield (triggerExists, actionExists)\n  }\n\n  /** Extracts status request subject to allowed values. */\n  private def extractStatusRequest = {\n    implicit val statusSerdes = Status.serdesRestricted\n    entity(as[Status])\n  }\n\n  /** Custom unmarshaller for query parameters \"limit\" for \"list\" operations. */\n  private implicit val stringToListLimit: Unmarshaller[String, ListLimit] = RestApiCommons.stringToListLimit(collection)\n\n  /** Custom unmarshaller for query parameters \"skip\" for \"list\" operations. */\n  private implicit val stringToListSkip: Unmarshaller[String, ListSkip] = RestApiCommons.stringToListSkip(collection)\n\n}\n\nprivate case class IgnoredRuleActivation(noop: Boolean) extends Throwable\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/Triggers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport java.time.{Clock, Instant}\nimport scala.collection.immutable.Map\nimport scala.concurrent.Future\nimport scala.util.Try\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.POST\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.{\n  Accepted,\n  BadRequest,\n  InternalServerError,\n  NoContent,\n  OK,\n  ServerError\n}\nimport org.apache.pekko.http.scaladsl.model.Uri.Path\nimport org.apache.pekko.http.scaladsl.model.headers.{`Timeout-Access`, Authorization}\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.server.{RequestContext, RouteResult}\nimport org.apache.pekko.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller}\nimport spray.json.DefaultJsonProtocol._\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\nimport org.apache.openwhisk.common.Https.HttpsConfig\nimport org.apache.openwhisk.common.{Https, TransactionId}\nimport org.apache.openwhisk.core.controller.RestApiCommons.{ListLimit, ListSkip}\nimport org.apache.openwhisk.core.database.{ActivationStore, CacheChangeNotification}\nimport org.apache.openwhisk.core.entitlement.Collection\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types.EntityStore\nimport org.apache.openwhisk.http.ErrorResponse\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.core.database.UserContext\n\n/** A trait implementing the triggers API. */\ntrait WhiskTriggersApi extends WhiskCollectionAPI {\n  services: WhiskServices =>\n\n  protected override val collection = Collection(Collection.TRIGGERS)\n\n  /** An actor system for timed based futures. */\n  protected implicit val actorSystem: ActorSystem\n\n  /** Database service to CRUD triggers. */\n  protected val entityStore: EntityStore\n\n  /** Connection context for HTTPS */\n  protected lazy val httpsConnectionContext = {\n    val httpsConfig = loadConfigOrThrow[HttpsConfig](\"whisk.controller.https\")\n    Https.connectionContextClient(httpsConfig, true)\n  }\n\n  protected val controllerProtocol = loadConfigOrThrow[String](\"whisk.controller.protocol\")\n\n  /**\n   * Sends a request either over http or https depending on the configuration\n   * @param request http request to send\n   * @return http response packed in a future\n   */\n  private def singleRequest(request: HttpRequest): Future[HttpResponse] = {\n    if (controllerProtocol == \"https\")\n      Http().singleRequest(request, connectionContext = httpsConnectionContext)\n    else\n      Http().singleRequest(request)\n  }\n\n  /** Notification service for cache invalidation. */\n  protected implicit val cacheChangeNotification: Some[CacheChangeNotification]\n\n  /** Database service to get activations. */\n  protected val activationStore: ActivationStore\n\n  /** JSON response formatter. */\n  /** Path to Triggers REST API. */\n  protected val triggersPath = \"triggers\"\n  protected val url = Uri(s\"${controllerProtocol}://localhost:${whiskConfig.servicePort}\")\n\n  import RestApiCommons.emptyEntityToJsObject\n\n  /**\n   * Creates or updates trigger if it already exists. The PUT content is deserialized into a WhiskTriggerPut\n   * which is a subset of WhiskTrigger (it eschews the namespace and entity name since the former is derived\n   * from the authenticated user and the latter is derived from the URI). The WhiskTriggerPut is merged with\n   * the existing WhiskTrigger in the datastore, overriding old values with new values that are defined.\n   * Any values not defined in the PUT content are replaced with old values.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskAction as JSON\n   * - 400 Bad Request\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  override def create(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n    parameter('overwrite ? false) { overwrite =>\n      entity(as[WhiskTriggerPut]) { content =>\n        putEntity(WhiskTrigger, entityStore, entityName.toDocId, overwrite, update(content) _, () => {\n          create(content, entityName)\n        }, postProcess = Some { trigger =>\n          completeAsTriggerResponse(trigger)\n        })\n      }\n    }\n  }\n\n  /**\n   * Fires trigger if it exists. The POST content is deserialized into a Payload and posted\n   * to the loadbalancer.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 ActivationId as JSON\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId) = {\n    extractRequest { request =>\n      val context = UserContext(user, request)\n\n      entity(as[Option[JsObject]]) { payload =>\n        getEntity(WhiskTrigger.get(entityStore, entityName.toDocId), Some {\n          trigger: WhiskTrigger =>\n            // List of active rules associated with the trigger\n            val activeRules: Map[FullyQualifiedEntityName, ReducedRule] =\n              trigger.rules.map(_.filter(_._2.status == Status.ACTIVE)).getOrElse(Map.empty)\n\n            if (activeRules.nonEmpty) {\n              val triggerActivationId = activationIdFactory.make()\n              logging.info(this, s\"[POST] trigger activation id: ${triggerActivationId}\")\n              val triggerActivation = WhiskActivation(\n                namespace = user.namespace.name.toPath, // all activations should end up in the one space regardless trigger.namespace\n                entityName.name,\n                user.subject,\n                triggerActivationId,\n                Instant.now(Clock.systemUTC()),\n                Instant.EPOCH,\n                response = ActivationResponse.success(payload orElse Some(JsObject.empty)),\n                version = trigger.version,\n                duration = None)\n              val headers = JsObject(\n                Map(new WebApiDirectives().headers -> request.headers\n                  .collect {\n                    case h if h.name != `Timeout-Access`.name => h.lowercaseName -> h.value\n                  }\n                  .toMap\n                  .toJson))\n\n              val mergedPayload = Some {\n                (headers.fields ++ (payload getOrElse JsObject.empty).fields).toJson.asJsObject\n              }\n\n              val args: JsObject = trigger.parameters.merge(mergedPayload).getOrElse(JsObject.empty)\n\n              activateRules(user, args, trigger.rules.getOrElse(Map.empty))\n                .map(results => triggerActivation.withLogs(ActivationLogs(results.map(_.toJson.compactPrint).toVector)))\n                .recover {\n                  case e =>\n                    logging.error(this, s\"Failed to write action activation results to trigger activation: $e\")\n                    triggerActivation\n                }\n                .map { activation =>\n                  activationStore.storeAfterCheck(activation, false, None, None, context)\n                }\n\n              respondWithActivationIdHeader(triggerActivationId) {\n                complete(Accepted, triggerActivationId.toJsObject)\n              }\n            } else {\n              logging\n                .debug(\n                  this,\n                  s\"[POST] trigger without an active rule was activated; no trigger activation record created for $entityName\")\n              complete(NoContent)\n            }\n        })\n      }\n    }\n  }\n\n  /**\n   * Deletes trigger.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskTrigger as JSON\n   * - 404 Not Found\n   * - 409 Conflict\n   * - 500 Internal Server Error\n   */\n  override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n    deleteEntity(\n      WhiskTrigger,\n      entityStore,\n      entityName.toDocId,\n      (t: WhiskTrigger) => Future.successful({}),\n      postProcess = Some { trigger =>\n        completeAsTriggerResponse(trigger)\n      })\n  }\n\n  /**\n   * Gets trigger. The trigger name is prefixed with the namespace to create the primary index key.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 WhiskTrigger has JSON\n   * - 404 Not Found\n   * - 500 Internal Server Error\n   */\n  override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(\n    implicit transid: TransactionId) = {\n    getEntity(WhiskTrigger.get(entityStore, entityName.toDocId), Some { trigger =>\n      completeAsTriggerResponse(trigger)\n    })\n  }\n\n  /**\n   * Gets all triggers in namespace.\n   *\n   * Responses are one of (Code, Message)\n   * - 200 [] or [WhiskTrigger as JSON]\n   * - 500 Internal Server Error\n   */\n  override def list(user: Identity, namespace: EntityPath)(implicit transid: TransactionId) = {\n    parameter(\n      'skip.as[ListSkip] ? ListSkip(collection.defaultListSkip),\n      'limit.as[ListLimit] ? ListLimit(collection.defaultListLimit),\n      'count ? false) { (skip, limit, count) =>\n      if (!count) {\n        listEntities {\n          WhiskTrigger.listCollectionInNamespace(entityStore, namespace, skip.n, limit.n, includeDocs = false) map {\n            list =>\n              list.fold((js) => js, (ts) => ts.map(WhiskTrigger.serdes.write(_)))\n          }\n        }\n      } else {\n        countEntities {\n          WhiskTrigger.countCollectionInNamespace(entityStore, namespace, skip.n)\n        }\n      }\n    }\n  }\n\n  /** Creates a WhiskTrigger from PUT content, generating default values where necessary. */\n  private def create(content: WhiskTriggerPut, triggerName: FullyQualifiedEntityName)(\n    implicit transid: TransactionId): Future[WhiskTrigger] = {\n    val newTrigger = WhiskTrigger(\n      triggerName.path,\n      triggerName.name,\n      content.parameters getOrElse Parameters(),\n      content.limits getOrElse TriggerLimits(),\n      content.version getOrElse SemVer(),\n      content.publish getOrElse false,\n      content.annotations getOrElse Parameters())\n    validateTriggerFeed(newTrigger)\n  }\n\n  /** Updates a WhiskTrigger from PUT content, merging old trigger where necessary. */\n  private def update(content: WhiskTriggerPut)(trigger: WhiskTrigger)(\n    implicit transid: TransactionId): Future[WhiskTrigger] = {\n    val newTrigger = WhiskTrigger(\n      trigger.namespace,\n      trigger.name,\n      content.parameters getOrElse trigger.parameters,\n      content.limits getOrElse trigger.limits,\n      content.version getOrElse trigger.version.upPatch,\n      content.publish getOrElse trigger.publish,\n      content.annotations getOrElse trigger.annotations,\n      trigger.rules).revision[WhiskTrigger](trigger.docinfo.rev)\n    Future.successful(newTrigger)\n  }\n\n  /**\n   * Validates a trigger feed annotation.\n   * A trigger feed must be a valid entity name, e.g., one of 'namespace/package/name'\n   * or 'namespace/name', or just 'name'.\n   *\n   * TODO: check if the feed actually exists. This is deferred because the macro\n   * operation of creating a trigger and initializing the feed is handled as one\n   * atomic operation in the CLI and the UI. At some point these may be promoted\n   * to a single atomic operation in the controller; at which point, validating\n   * the trigger feed should execute the action (verifies it is a valid name that\n   * the subject is entitled to) and iff that succeeds will the trigger be created\n   * or updated.\n   */\n  private def validateTriggerFeed(trigger: WhiskTrigger)(implicit transid: TransactionId) = {\n    trigger.annotations.get(Parameters.Feed) map {\n      case JsString(f) if (EntityPath.validate(f)) =>\n        Future successful trigger\n      case _ =>\n        Future failed {\n          RejectRequest(BadRequest, \"Feed name is not valid\")\n        }\n    } getOrElse {\n      Future successful trigger\n    }\n  }\n\n  /**\n   * Completes an HTTP request with a WhiskRule including the computed Status\n   *\n   * @param rule the rule to send\n   * @param status the status to include in the response\n   */\n  private def completeAsTriggerResponse(trigger: WhiskTrigger): RequestContext => Future[RouteResult] = {\n    complete(OK, trigger)\n  }\n\n  /**\n   * Iterates through each rule and invoking each active rule's mapped action.\n   */\n  private def activateRules(user: Identity,\n                            args: JsObject,\n                            rulesToActivate: Map[FullyQualifiedEntityName, ReducedRule])(\n    implicit transid: TransactionId): Future[Iterable[RuleActivationResult]] = {\n    val ruleResults = rulesToActivate.map {\n      case (ruleName, rule) if (rule.status != Status.ACTIVE) =>\n        Future.successful {\n          RuleActivationResult(\n            ActivationResponse.ApplicationError,\n            ruleName,\n            rule.action,\n            Left(Messages.triggerWithInactiveRule(ruleName.asString, rule.action.asString)))\n        }\n      case (ruleName, rule) =>\n        // Invoke the action. Retain action results for inclusion in the trigger activation record\n        postActivation(user, rule, args)\n          .flatMap { response =>\n            response.status match {\n              case OK | Accepted =>\n                Unmarshal(response.entity).to[JsObject].map { activationResponse =>\n                  val activationId = activationResponse.fields(\"activationId\").convertTo[ActivationId]\n                  logging.debug(this, s\"trigger-fired action '${rule.action}' invoked with activation $activationId\")\n                  RuleActivationResult(ActivationResponse.Success, ruleName, rule.action, Right(activationId))\n                }\n\n              case code =>\n                Unmarshal(response.entity).to[String].map { error =>\n                  val failureType = code match {\n                    case _: ServerError => ActivationResponse.WhiskError // all 500s are to be considered whisk errors\n                    case _              => ActivationResponse.ApplicationError\n                  }\n                  val errorMessage: String = Try(error.parseJson.convertTo[ErrorResponse])\n                    .map { e =>\n                      def logMsg = s\"trigger-fired action '${rule.action}' failed to invoke with ${e.error}, ${e.code}\"\n                      if (failureType == ActivationResponse.ApplicationError) logging.debug(this, logMsg)\n                      else logging.error(this, logMsg)\n\n                      e.error\n                    }\n                    .getOrElse {\n                      logging\n                        .error(this, s\"trigger-fired action '${rule.action}' failed to invoke with status code $code\")\n                      InternalServerError.defaultMessage\n                    }\n\n                  RuleActivationResult(failureType, ruleName, rule.action, Left(errorMessage))\n                }\n            }\n          }\n          .recover {\n            case t =>\n              logging.error(this, s\"trigger-fired action '${rule.action}' failed to invoke with $t\")\n              RuleActivationResult(\n                ActivationResponse.WhiskError,\n                ruleName,\n                rule.action,\n                Left(InternalServerError.defaultMessage))\n          }\n    }\n\n    Future.sequence(ruleResults)\n  }\n\n  /**\n   * Posts an action activation. Currently done by posting internally to the controller.\n   * TODO: use a proper path that does not route through HTTP.\n   *\n   * @param rule the name of the rule that is activated\n   * @param args the arguments to post to the action\n   * @return a future with the HTTP response from the action activation\n   */\n  private def postActivation(user: Identity, rule: ReducedRule, args: JsObject)(\n    implicit transid: TransactionId): Future[HttpResponse] = {\n    // Build the url to invoke an action mapped to the rule\n    val actionUrl = baseControllerPath / rule.action.path.root.asString / \"actions\"\n\n    val actionPath = rule.action.path.relativePath\n      .map(pkg => Path / pkg.namespace / rule.action.name.asString)\n      .getOrElse(Path / rule.action.name.asString)\n\n    user.authkey.getCredentials\n      .map { creds =>\n        val request = HttpRequest(\n          method = POST,\n          uri = url.withPath(actionUrl ++ actionPath),\n          headers = List(Authorization(creds), transid.toHeader),\n          entity = HttpEntity(MediaTypes.`application/json`, args.compactPrint))\n\n        singleRequest(request)\n      }\n      .getOrElse(Future.failed(new NoCredentialsAvailable()))\n  }\n\n  /** Contains the result of invoking a rule */\n  case class RuleActivationResult(statusCode: Int,\n                                  ruleName: FullyQualifiedEntityName,\n                                  actionName: FullyQualifiedEntityName,\n                                  response: Either[String, ActivationId]) {\n    def toJson: JsObject =\n      JsObject(\n        Map(\n          \"rule\" -> ruleName.asString.toJson,\n          \"action\" -> actionName.asString.toJson,\n          \"statusCode\" -> statusCode.toJson,\n          \"success\" -> (statusCode == ActivationResponse.Success).toJson,\n          response.fold(\"error\" -> _.toJson, \"activationId\" -> _.toJson)))\n  }\n\n  /** Common base bath for the controller, used by internal action activation mechanism. */\n  private val baseControllerPath = Path / \"api\" / \"v1\" / \"namespaces\"\n\n  /** Custom unmarshaller for query parameters \"limit\" for \"list\" operations. */\n  private implicit val stringToListLimit: Unmarshaller[String, ListLimit] = RestApiCommons.stringToListLimit(collection)\n\n  /** Custom unmarshaller for query parameters \"skip\" for \"list\" operations. */\n  private implicit val stringToListSkip: Unmarshaller[String, ListSkip] = RestApiCommons.stringToListSkip(collection)\n\n  private case class NoCredentialsAvailable() extends IllegalArgumentException\n\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/WebActions.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller\n\nimport java.util.Base64\nimport scala.concurrent.Future\nimport scala.util.{Failure, Success, Try}\nimport org.apache.pekko.http.scaladsl.model.HttpEntity.Empty\nimport org.apache.pekko.http.scaladsl.server.Directives\nimport org.apache.pekko.http.scaladsl.model.HttpMethod\nimport org.apache.pekko.http.scaladsl.model.HttpHeader\nimport org.apache.pekko.http.scaladsl.model.MediaType\nimport org.apache.pekko.http.scaladsl.model.MediaTypes\nimport org.apache.pekko.http.scaladsl.model.MediaTypes._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\nimport org.apache.pekko.http.scaladsl.model.StatusCode\nimport org.apache.pekko.http.scaladsl.model.headers.RawHeader\nimport org.apache.pekko.http.scaladsl.model.headers._\nimport org.apache.pekko.http.scaladsl.model.Uri.Query\nimport org.apache.pekko.http.scaladsl.model.HttpEntity\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.model.headers.`Content-Type`\nimport org.apache.pekko.http.scaladsl.model.headers.`Timeout-Access`\nimport org.apache.pekko.http.scaladsl.model.ContentType\nimport org.apache.pekko.http.scaladsl.model.ContentTypes\nimport org.apache.pekko.http.scaladsl.model.FormData\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.OPTIONS\nimport org.apache.pekko.http.scaladsl.model.HttpCharsets\nimport org.apache.pekko.http.scaladsl.model.HttpResponse\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport WhiskWebActionsApi.MediaExtension\nimport RestApiCommons.{jsonPrettyResponsePrinter => jsonPrettyPrinter}\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.controller.actions.PostActionActivation\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entitlement.{Collection, Privilege, Resource}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types._\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancerException\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport org.apache.openwhisk.http.{CorsSettings, Messages}\nimport org.apache.openwhisk.http.LenientSprayJsonSupport._\nimport org.apache.openwhisk.spi.SpiLoader\nimport org.apache.openwhisk.utils.JsHelpers._\nimport org.apache.openwhisk.core.entity.Exec\n\nprotected[controller] sealed class WebApiDirectives(prefix: String = \"__ow_\") {\n  // enforce the presence of an extension (e.g., .http) in the URI path\n  val enforceExtension = false\n\n  // the field name that represents the status code for an http action response\n  val statusCode = \"statusCode\"\n\n  // parameters that are added to an action input to pass HTTP request context values\n  val method: String = fields(\"method\")\n  val headers: String = fields(\"headers\")\n  val path: String = fields(\"path\")\n  val namespace: String = fields(\"user\")\n  val query: String = fields(\"query\")\n  val body: String = fields(\"body\")\n\n  lazy val reservedProperties: Set[String] = Set(method, headers, path, namespace, query, body)\n\n  protected final def fields(f: String) = s\"$prefix$f\"\n}\n\nprivate case class Context(propertyMap: WebApiDirectives,\n                           method: HttpMethod,\n                           headers: Seq[HttpHeader],\n                           path: String,\n                           query: Query,\n                           body: Option[JsValue] = None) {\n  val queryAsMap = query.toMap\n\n  // returns true iff the attached query and body parameters contain a property\n  // that conflicts with the given reserved parameters\n  def overrides(reservedParams: Set[String]): Boolean = {\n    val queryParams = queryAsMap.keySet\n    val bodyParams = body\n      .map {\n        case JsObject(fields) => fields.keySet\n        case _                => Set.empty\n      }\n      .getOrElse(Set.empty)\n\n    (queryParams ++ bodyParams).forall(key => !reservedParams.contains(key))\n  }\n\n  // attach the body to the Context\n  def withBody(b: Option[JsValue]) = Context(propertyMap, method, headers, path, query, b)\n\n  def metadata(user: Option[Identity]): Map[String, JsValue] = {\n    Map(\n      propertyMap.method -> method.value.toLowerCase.toJson,\n      propertyMap.headers -> headers\n        .collect {\n          case h if h.name != `Timeout-Access`.name => h.lowercaseName -> h.value\n        }\n        .toMap\n        .toJson,\n      propertyMap.path -> path.toJson) ++\n      user.map(u => propertyMap.namespace -> u.namespace.name.asString.toJson)\n  }\n\n  def toActionArgument(user: Option[Identity], boxQueryAndBody: Boolean): Map[String, JsValue] = {\n    val queryParams = if (boxQueryAndBody) {\n      Map(propertyMap.query -> JsString(query.toString))\n    } else {\n      queryAsMap.map(kv => kv._1 -> JsString(kv._2))\n    }\n\n    // if the body is a json object, merge with query parameters\n    // otherwise, this is an opaque body that will be nested under\n    // __ow_body in the parameters sent to the action as an argument\n    val bodyParams: Map[String, JsValue] = body match {\n      case Some(JsObject(fields)) if !boxQueryAndBody => fields\n      case Some(v)                                    => Map(propertyMap.body -> v)\n      case None if !boxQueryAndBody                   => Map.empty\n      case _                                          => Map(propertyMap.body -> JsString.empty)\n    }\n\n    // precedence order is: query params -> body (last wins)\n    metadata(user) ++ queryParams ++ bodyParams\n  }\n}\n\nprotected[core] object WhiskWebActionsApi extends Directives {\n\n  private val mediaTranscoders = {\n    // extensions are expected to contain only [a-z]\n    Seq(\n      MediaExtension(\".http\", resultAsHttp _),\n      MediaExtension(\".json\", resultAsJson _),\n      MediaExtension(\".html\", resultAsHtml _),\n      MediaExtension(\".svg\", resultAsSvg _),\n      MediaExtension(\".text\", resultAsText _))\n  }\n\n  private val defaultMediaTranscoder: MediaExtension = mediaTranscoders.find(_.extension == \".http\").get\n\n  val allowedExtensions: Set[String] = mediaTranscoders.map(_.extension).toSet\n\n  /**\n   * Splits string into a base name plus optional extension.\n   * If name ends with \".xxxx\" which matches a known extension, accept it as the extension.\n   * Otherwise, the extension is \".http\" by definition unless enforcing the presence of an extension.\n   */\n  def mediaTranscoderForName(name: String, enforceExtension: Boolean): (String, Option[MediaExtension]) = {\n    mediaTranscoders\n      .find(mt => name.endsWith(mt.extension))\n      .map { mt =>\n        val base = name.dropRight(mt.extensionLength)\n        (base, Some(mt))\n      }\n      .getOrElse {\n        (name, if (enforceExtension) None else Some(defaultMediaTranscoder))\n      }\n  }\n\n  /**\n   * Supported extensions and transcoder to complete a request.\n   *\n   * @param extension  the supported media types for action response\n   * @param transcoder the HTTP decoder and terminator for the extension\n   */\n  protected case class MediaExtension(extension: String,\n                                      transcoder: (JsValue, TransactionId, WebApiDirectives) => Route) {\n    val extensionLength = extension.length\n  }\n\n  private def resultAsHtml(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {\n    val htmlResult = result match {\n      case JsObject(fields) => fields.get(\"body\").orElse(fields.get(\"html\")).getOrElse(JsNull)\n      case _                => result\n    }\n\n    htmlResult match {\n      case JsString(html) => complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, html))\n      case _              => terminate(BadRequest, Messages.invalidMedia(`text/html`))(transid, jsonPrettyPrinter)\n    }\n  }\n\n  private def resultAsSvg(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {\n    val svgResult = result match {\n      case JsObject(fields) => fields.get(\"body\").orElse(fields.get(\"svg\")).getOrElse(JsNull)\n      case _                => result\n    }\n\n    svgResult match {\n      case JsString(svg) => complete(HttpEntity(`image/svg+xml`, svg.getBytes))\n      case _             => terminate(BadRequest, Messages.invalidMedia(`image/svg+xml`))(transid, jsonPrettyPrinter)\n    }\n  }\n\n  private def resultAsText(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {\n    val txtResult = result match {\n      case JsObject(fields) => fields.get(\"body\").orElse(fields.get(\"text\"))\n      case _                => Some(result)\n    }\n\n    txtResult match {\n      case Some(r: JsObject)  => complete(OK, r.prettyPrint)\n      case Some(r: JsArray)   => complete(OK, r.prettyPrint)\n      case Some(JsString(s))  => complete(OK, s)\n      case Some(JsBoolean(b)) => complete(OK, b.toString)\n      case Some(JsNumber(n))  => complete(OK, n.toString)\n      case Some(JsNull)       => complete(OK, JsNull.toString)\n      case _                  => terminate(NotFound, Messages.propertyNotFound)(transid, jsonPrettyPrinter)\n    }\n  }\n\n  private def resultAsJson(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {\n    result match {\n      case r: JsObject => complete(OK, r)\n      case r: JsArray  => complete(OK, r)\n      case _           => terminate(BadRequest, Messages.invalidMedia(`application/json`))(transid, jsonPrettyPrinter)\n    }\n  }\n\n  private def resultAsHttp(result: JsValue, transid: TransactionId, rp: WebApiDirectives) = {\n    Try {\n      val JsObject(fields) = result\n      val headers = fields.get(\"headers\").map {\n        case JsObject(hs) =>\n          hs.flatMap {\n            case (k, v) => headersFromJson(k, v)\n          }.toList\n\n        case _ => throw new Throwable(\"Invalid header\")\n      } getOrElse List.empty\n\n      val body = fields.get(\"body\")\n\n      val intCode = fields.get(rp.statusCode).map {\n        // the following throws an exception if the code is not a whole number or a valid code\n        case JsNumber(c) => c.toIntExact\n\n        // parse the string to an Int (not a BigInt) matching JsNumber case match above\n        // c.toInt could throw an exception if the string isn't an integer\n        case JsString(c) => c.toInt\n\n        case _ => throw new Throwable(\"Illegal status code\")\n      }\n\n      val code: Option[StatusCode] = intCode.map(c => StatusCodes.getForKey(c).getOrElse(StatusCodes.custom(c, \"\")))\n\n      body.collect {\n        case JsString(str) if str.nonEmpty   => interpretHttpResponse(code.getOrElse(OK), headers, str, transid)\n        case JsString(str) /* str.isEmpty */ => respondWithEmptyEntity(code.getOrElse(NoContent), headers)\n        case js if js != JsNull              => interpretHttpResponseAsJson(code.getOrElse(OK), headers, js, transid)\n      } getOrElse respondWithEmptyEntity(code.getOrElse(NoContent), headers)\n\n    } getOrElse {\n      // either the result was not a JsObject or there was an exception validating the\n      // response as an http result (including an invalid status code)\n      terminate(BadRequest, Messages.invalidMedia(`message/http`))(transid, jsonPrettyPrinter)\n    }\n  }\n\n  private def respondWithEmptyEntity(code: StatusCode, headers: List[RawHeader]) = {\n    respondWithHeaders(removeContentTypeHeader(headers)) {\n      // note that if header defined a content-type, it will be ignored\n      // since the type must be compatible with the data response\n      complete(HttpResponse(code, entity = HttpEntity.Empty))\n    }\n  }\n\n  private def headersFromJson(k: String, v: JsValue): Seq[RawHeader] = v match {\n    case JsString(v)  => Seq(RawHeader(k, v))\n    case JsBoolean(v) => Seq(RawHeader(k, v.toString))\n    case JsNumber(v)  => Seq(RawHeader(k, v.toString))\n    case JsArray(v)   => v.flatMap(inner => headersFromJson(k, inner))\n    case _            => throw new Throwable(\"Invalid header\")\n  }\n\n  /**\n   * Finds the content-type in the header list and ensures that it is a valid format. If it is not\n   * valid, construct a failure with appropriate message.\n   * If the content-type header is missing, then return the supplied defaultType\n   */\n  private def findContentTypeInHeader(headers: List[RawHeader],\n                                      transid: TransactionId,\n                                      defaultType: MediaType): Try[MediaType] = {\n    headers.find(_.lowercaseName == `Content-Type`.lowercaseName) match {\n      case Some(header) =>\n        MediaType.parse(header.value) match {\n          case Right(mediaType: MediaType) => Success(mediaType)\n          case _                           => Failure(RejectRequest(BadRequest, Messages.httpUnknownContentType)(transid))\n        }\n      case None => Success(defaultType)\n    }\n  }\n\n  def isJsonFamily(mt: MediaType): Boolean = {\n    mt == `application/json` || mt.value.endsWith(\"+json\")\n  }\n\n  private def interpretHttpResponseAsJson(code: StatusCode,\n                                          headers: List[RawHeader],\n                                          js: JsValue,\n                                          transid: TransactionId) = {\n    findContentTypeInHeader(headers, transid, `application/json`) match {\n      // use the default pekko-http response marshaler for standard application/json\n      case Success(mediaType) if mediaType == `application/json` =>\n        respondWithHeaders(removeContentTypeHeader(headers)) {\n          complete(code, js)\n        }\n\n      // for all other json-family content-type, explicitly marshal the response;\n      // the order of the case statement matters; isJsonFamily returns true for application/json\n      case Success(mediaType) if isJsonFamily(mediaType) =>\n        respondWithHeaders(removeContentTypeHeader(headers)) {\n          complete(\n            code,\n            HttpEntity(\n              ContentType(\n                MediaType.customWithFixedCharset(mediaType.mainType, mediaType.subType, HttpCharsets.`UTF-8`)),\n              js.prettyPrint))\n        }\n\n      case _ =>\n        terminate(BadRequest, Messages.httpContentTypeError)(transid, jsonPrettyPrinter)\n    }\n  }\n\n  private def interpretHttpResponse(code: StatusCode, headers: List[RawHeader], str: String, transid: TransactionId) = {\n    findContentTypeInHeader(headers, transid, `text/html`).flatMap { mediaType =>\n      val ct = ContentType(mediaType, () => HttpCharsets.`UTF-8`)\n      ct match {\n        // TODO: remove this extract check for base64 on json response\n        // this is here for legacy reasons to not brake old webactions returning base64 json that have not migrated yet\n        case nonbinary: ContentType.NonBinary if (isJsonFamily(mediaType) && Exec.isBinaryCode(str)) =>\n          Try(Base64.getDecoder().decode(str)).map(HttpEntity(ct, _))\n        case nonbinary: ContentType.NonBinary => Success(HttpEntity(nonbinary, str))\n\n        // because of the default charset provided to the content type constructor\n        // the remaining content types to match against are binary at this point\n        case _ /* ContentType.Binary */ => Try(Base64.getDecoder().decode(str)).map(HttpEntity(ct, _))\n      }\n    } match {\n      case Success(entity) =>\n        respondWithHeaders(removeContentTypeHeader(headers)) {\n          complete(code, entity)\n        }\n\n      case Failure(RejectRequest(code, message)) =>\n        terminate(code, message)(transid, jsonPrettyPrinter)\n\n      case _ =>\n        terminate(BadRequest, Messages.httpContentTypeError)(transid, jsonPrettyPrinter)\n    }\n  }\n\n  private def removeContentTypeHeader(headers: List[RawHeader]) =\n    headers.filter(_.lowercaseName != `Content-Type`.lowercaseName)\n}\n\ntrait WhiskWebActionsApi\n    extends Directives\n    with ValidateRequestSize\n    with PostActionActivation\n    with CustomHeaders\n    with CorsSettings.WebActions {\n  services: WhiskServices =>\n\n  /** API path invocation path for posting activations directly through the host. */\n  protected val webInvokePathSegments: Seq[String]\n\n  /** Mapping of HTTP request fields to action parameter names. */\n  protected val webApiDirectives: WebApiDirectives\n\n  /** Store for identities. */\n  protected val authStore: AuthStore\n\n  /** Configured authentication provider. */\n  protected val authenticationProvider = SpiLoader.get[AuthenticationDirectiveProvider]\n\n  /** The collection type for this trait. */\n  protected val collection = Collection(Collection.ACTIONS)\n\n  /** The prefix for web invokes e.g., /web. */\n  private lazy val webRoutePrefix = {\n    pathPrefix(webInvokePathSegments.map(_segmentStringToPathMatcher(_)).reduceLeft(_ / _))\n  }\n\n  /** Allowed verbs. */\n  private lazy val allowedOperations = get | delete | post | put | head | options | patch\n\n  private lazy val validNameSegment = pathPrefix(EntityName.REGEX.r)\n  private lazy val packagePrefix = pathPrefix(\"default\".r | EntityName.REGEX.r)\n\n  private val defaultCorsBaseResponse =\n    List(allowOrigin, allowMethods)\n\n  private val defaultCorsWithAllowHeader = {\n    defaultCorsBaseResponse :+ allowHeaders\n  }\n\n  private def defaultCorsResponse(headers: Seq[HttpHeader]): List[HttpHeader] = {\n    headers.find(_.name == `Access-Control-Request-Headers`.name).map { h =>\n      defaultCorsBaseResponse :+ `Access-Control-Allow-Headers`(h.value)\n    } getOrElse defaultCorsWithAllowHeader\n  }\n\n  private def contentTypeFromEntity(entity: HttpEntity) = entity.contentType match {\n    case ct if ct == ContentTypes.NoContentType => None\n    case ct                                     => Some(RawHeader(`Content-Type`.lowercaseName, ct.toString))\n  }\n\n  /** Extracts the HTTP method, headers, query params and unmatched (remaining) path. */\n  private val requestMethodParamsAndPath = {\n    extract { ctx =>\n      val method = ctx.request.method\n      val query = ctx.request.uri.query()\n      val path = ctx.unmatchedPath.toString\n      val headers = ctx.request.headers ++ contentTypeFromEntity(ctx.request.entity)\n      Context(webApiDirectives, method, headers, path, query)\n    }\n  }\n\n  def routes(user: Identity)(implicit transid: TransactionId): Route = routes(Some(user))\n\n  def routes()(implicit transid: TransactionId): Route = routes(None)\n\n  private val maxWaitForWebActionResult = Some(controllerActivationConfig.maxWaitForBlockingActivation)\n\n  /**\n   * Adds route to web based activations. Actions invoked this way are anonymous in that the\n   * caller is not authenticated. The intended action must be named in the path as a fully qualified\n   * name as in /web/some-namespace/some-package/some-action. The package is optional\n   * in that the action may be in the default package, in which case, the string \"default\" must be used.\n   * If the action doesn't exist (or the namespace is not valid) NotFound is generated. Following the\n   * action name, an \"extension\" is required to specify the desired content type for the response. This\n   * extension is one of supported media types. An example is \".json\" for a JSON response or \".html\" for\n   * an text/html response.\n   *\n   * Actions may be exposed to this web proxy by adding an annotation (\"export\" -> true).\n   */\n  def routes(user: Option[Identity])(implicit transid: TransactionId): Route = {\n    (allowedOperations & webRoutePrefix) {\n      validNameSegment { namespace =>\n        packagePrefix { pkg =>\n          validNameSegment { seg =>\n            handleMatch(namespace, pkg, seg, user)\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * Gets identity from datastore.\n   * This method is factored out to allow mock testing.\n   */\n  protected def getIdentity(namespace: EntityName)(implicit transid: TransactionId): Future[Identity] = {\n    // ask auth provider to create an identity for the given namespace\n    authenticationProvider.identityByNamespace(namespace)(transid, actorSystem, authStore)\n  }\n\n  private def handleMatch(namespaceSegment: String,\n                          pkgSegment: String,\n                          actionNameWithExtension: String,\n                          onBehalfOf: Option[Identity])(implicit transid: TransactionId) = {\n\n    def fullyQualifiedActionName(actionName: String) = {\n      val namespace = EntityName(namespaceSegment)\n      val pkgName = if (pkgSegment == \"default\") None else Some(EntityName(pkgSegment))\n      namespace.addPath(pkgName).addPath(EntityName(actionName)).toFullyQualifiedEntityName\n    }\n\n    provide(WhiskWebActionsApi.mediaTranscoderForName(actionNameWithExtension, webApiDirectives.enforceExtension)) {\n      case (actionName, Some(extension)) =>\n        // extract request context, checks for overrides of reserved properties, and constructs action arguments\n        // as the context body which may be the incoming request when the content type is JSON or formdata, or\n        // the raw body as __ow_body (and query parameters as __ow_query) otherwise\n        extract(_.request.entity) { e =>\n          requestMethodParamsAndPath { context =>\n            provide(fullyQualifiedActionName(actionName)) { fullActionName =>\n              onComplete(verifyWebAction(fullActionName)) {\n                case Success((actionOwnerIdentity, action)) =>\n                  validateSize(isWhithinRange(actionOwnerIdentity.limits, e.contentLengthOption.getOrElse(0)))(\n                    transid,\n                    jsonPrettyPrinter) {\n\n                    val actionDelegatesCors =\n                      !action.annotations.getAs[Boolean](Annotations.WebCustomOptionsAnnotationName).getOrElse(false)\n\n                    if (actionDelegatesCors) {\n                      respondWithHeaders(defaultCorsResponse(context.headers)) {\n                        if (context.method == OPTIONS) {\n                          complete(OK, HttpEntity.Empty)\n                        } else {\n                          extractEntityAndProcessRequest(\n                            confirmAuthenticated(action.annotations, context.headers, onBehalfOf).getOrElse(true),\n                            actionOwnerIdentity,\n                            action,\n                            extension,\n                            onBehalfOf,\n                            context,\n                            e)\n                        }\n                      }\n                    } else {\n                      val allowedToProceed = if (context.method != OPTIONS) {\n                        confirmAuthenticated(action.annotations, context.headers, onBehalfOf).getOrElse(true)\n                      } else {\n                        // invoke the action for OPTIONS even if user is not authorized\n                        // so that action can respond to option request\n                        true\n                      }\n\n                      extractEntityAndProcessRequest(\n                        allowedToProceed,\n                        actionOwnerIdentity,\n                        action,\n                        extension,\n                        onBehalfOf,\n                        context,\n                        e)\n                    }\n                  }\n\n                case Failure(t: RejectRequest) =>\n                  terminate(t.code, t.message)\n\n                case Failure(t) =>\n                  logging.error(this, s\"exception in handleMatch: $t\")\n                  terminate(InternalServerError)\n              }\n            }\n          }\n        }\n\n      case (_, None) =>\n        terminate(NotAcceptable, Messages.contentTypeExtensionNotSupported(WhiskWebActionsApi.allowedExtensions))\n    }\n  }\n\n  /**\n   * Checks that subject has right to post an activation and fetch the action\n   * followed by the package and merge parameters. The action is fetched first since\n   * it will not succeed for references relative to a binding, and the export bit is\n   * confirmed before fetching the package and merging parameters.\n   *\n   * @return Future that completes with the action and action-owner-identity on success otherwise\n   *         a failed future with a request rejection error which may be one of the following:\n   *         not entitled (throttled), package/action not found, action not web enabled,\n   *         or request overrides final parameters\n   */\n  private def verifyWebAction(actionName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {\n\n    // lookup the identity for the action namespace\n    identityLookup(actionName.path.root) flatMap { actionOwnerIdentity =>\n      confirmExportedAction(actionLookup(actionName)) flatMap { a =>\n        checkEntitlement(actionOwnerIdentity, a) map { _ =>\n          (actionOwnerIdentity, a)\n        }\n      }\n    }\n  }\n\n  private def extractEntityAndProcessRequest(authorizedToProceed: Boolean,\n                                             actionOwnerIdentity: Identity,\n                                             action: WhiskActionMetaData,\n                                             extension: MediaExtension,\n                                             onBehalfOf: Option[Identity],\n                                             context: Context,\n                                             httpEntity: HttpEntity)(implicit transid: TransactionId) = {\n\n    def process(body: Option[JsValue], isRawHttpAction: Boolean) = {\n      processRequest(actionOwnerIdentity, action, extension, onBehalfOf, context.withBody(body), isRawHttpAction)\n    }\n\n    if (authorizedToProceed) {\n      provide(action.annotations.getAs[Boolean](Annotations.RawHttpAnnotationName).getOrElse(false)) {\n        isRawHttpAction =>\n          httpEntity match {\n            case Empty =>\n              process(None, isRawHttpAction)\n\n            case HttpEntity.Strict(ct, json) if WhiskWebActionsApi.isJsonFamily(ct.mediaType) && !isRawHttpAction =>\n              if (json.nonEmpty) {\n                entity(as[JsValue]) { body =>\n                  process(Some(body), isRawHttpAction)\n                }\n              } else {\n                process(None, isRawHttpAction)\n              }\n\n            case HttpEntity.Strict(ContentType(MediaTypes.`application/x-www-form-urlencoded`, _), _)\n                if !isRawHttpAction =>\n              entity(as[FormData]) { form =>\n                val body = form.fields.toMap.toJson.asJsObject\n                process(Some(body), isRawHttpAction)\n              }\n\n            case HttpEntity.Strict(contentType, data) =>\n              // for legacy, we are encoding application/json still\n              if (contentType.mediaType.binary || contentType.mediaType == `application/json`) {\n                Try(JsString(Base64.getEncoder.encodeToString(data.toArray))) match {\n                  case Success(bytes) => process(Some(bytes), isRawHttpAction)\n                  case Failure(t)     => terminate(BadRequest, Messages.unsupportedContentType(contentType.mediaType))\n                }\n              } else {\n                val str = JsString(data.utf8String)\n                process(Some(str), isRawHttpAction)\n              }\n\n            case _ => terminate(BadRequest, Messages.unsupportedContentType)\n          }\n      }\n    } else {\n      terminate(Unauthorized)\n    }\n  }\n\n  private def processRequest(actionOwnerIdentity: Identity,\n                             action: WhiskActionMetaData,\n                             responseType: MediaExtension,\n                             onBehalfOf: Option[Identity],\n                             context: Context,\n                             isRawHttpAction: Boolean)(implicit transid: TransactionId) = {\n\n    def queuedActivation = {\n      // checks (1) if any of the query or body parameters override final action parameters\n      // computes overrides if any relative to the reserved __ow_* properties, and (2) if\n      // action is a raw http handler\n      //\n      // NOTE: it is assumed the action parameters do not intersect with the reserved properties\n      // since these are system properties, the action should not define them, and if it does,\n      // they will be overwritten\n      if (isRawHttpAction || context\n            .overrides(webApiDirectives.reservedProperties ++ action.immutableParameters)) {\n        val content = context.toActionArgument(onBehalfOf, isRawHttpAction)\n        invokeAction(actionOwnerIdentity, action, Some(JsObject(content)), maxWaitForWebActionResult, cause = None)\n      } else {\n        Future.failed(RejectRequest(BadRequest, Messages.parametersNotAllowed))\n      }\n    }\n\n    completeRequest(queuedActivation, responseType)\n  }\n\n  private def completeRequest(queuedActivation: Future[Either[ActivationId, WhiskActivation]],\n                              responseType: MediaExtension)(implicit transid: TransactionId) = {\n    onComplete(queuedActivation) {\n      case Success(Right(activation)) =>\n        respondWithActivationIdHeader(activation.activationId) {\n          val result = activation.resultAsJson\n\n          if (activation.response.isSuccess || activation.response.isApplicationError) {\n            val resultPath = if (activation.response.isSuccess) {\n              List.empty\n            } else {\n              // the activation produced an error response, so look for an error property\n              // in the response, unwrap it and use it to terminate the response\n              List(ActivationResponse.ERROR_FIELD)\n            }\n\n            val result = getFieldPath(activation.resultAsJson, resultPath)\n            result match {\n              case Some(projection) =>\n                val marshaler = Future(responseType.transcoder(projection, transid, webApiDirectives))\n                onComplete(marshaler) {\n                  case Success(done) => done // all transcoders terminate the connection\n                  case Failure(t)    => terminate(InternalServerError)\n                }\n              case _ => terminate(NotFound, Messages.propertyNotFound)\n            }\n          } else {\n            terminate(BadRequest, Messages.errorProcessingRequest)\n          }\n        }\n\n      case Success(Left(activationId)) =>\n        // blocking invoke which got queued instead\n        // this should not happen, instead it should be a blocking invoke timeout\n        logging.debug(this, \"activation waiting period expired\")\n        respondWithActivationIdHeader(activationId) {\n          terminate(Accepted, Messages.responseNotReady)\n        }\n\n      case Failure(t: RejectRequest) => terminate(t.code, t.message)\n\n      case Failure(t: LoadBalancerException) =>\n        logging.error(this, s\"failed in loadbalancer: $t\")\n        terminate(ServiceUnavailable)\n\n      case Failure(t) =>\n        logging.error(this, s\"exception in completeRequest: $t\")\n        terminate(InternalServerError)\n    }\n  }\n\n  /**\n   * Gets the action if it exists and fail future with RejectRequest if it does not.\n   *\n   * @return future action document or NotFound rejection\n   */\n  private def actionLookup(actionName: FullyQualifiedEntityName)(\n    implicit transid: TransactionId): Future[WhiskActionMetaData] = {\n    WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, actionName) recoverWith {\n      case _: ArtifactStoreException | DeserializationException(_, _, _) =>\n        Future.failed(RejectRequest(NotFound))\n    }\n  }\n\n  /**\n   * Gets the identity for the namespace.\n   */\n  private def identityLookup(namespace: EntityName)(implicit transid: TransactionId): Future[Identity] = {\n    getIdentity(namespace) recoverWith {\n      case _: ArtifactStoreException | DeserializationException(_, _, _) =>\n        Future.failed(RejectRequest(NotFound))\n      case t =>\n        // leak nothing no matter what, failure is already logged so skip here\n        Future.failed(RejectRequest(NotFound))\n    }\n  }\n\n  /**\n   * Checks if an action is exported (i.e., carries the required annotation).\n   * This function does not check if web action requires authentication.\n   */\n  private def confirmExportedAction(actionLookup: Future[WhiskActionMetaData])(\n    implicit transid: TransactionId): Future[WhiskActionMetaData] = {\n    actionLookup flatMap { action =>\n      val isExported = action.annotations.getAs[Boolean](Annotations.WebActionAnnotationName).getOrElse(false)\n\n      if (isExported) {\n        logging.debug(this, s\"${action.fullyQualifiedName(true)} is exported\")\n        Future.successful(action)\n      } else {\n        logging.debug(this, s\"${action.fullyQualifiedName(true)} not exported\")\n        Future.failed(RejectRequest(NotFound))\n      }\n    }\n  }\n\n  /**\n   * Checks if an action is executable.\n   */\n  private def checkEntitlement(identity: Identity, action: WhiskActionMetaData)(\n    implicit transid: TransactionId): Future[Unit] = {\n\n    val fqn = action.fullyQualifiedName(false)\n    val resource = Resource(fqn.path, collection, Some(fqn.name.asString))\n    entitlementProvider.check(identity, Privilege.ACTIVATE, resource)\n  }\n\n  /**\n   * Checks if an action requires authenticate and is authenticated (i.e., carries the required annotation).\n   * This function assumes the action is a web action.\n   *\n   * @param annotations the web action annotations\n   * @param reqHeaders the web action invocation request headers\n   * @param authenticatedUser true if this request is from an authenticated whisk user\n   * @return None if web annotation does not specify an authentication scheme\n   *         Some(true) if web annotation includes require-whisk-auth and value matches the request header `X-Require-Whisk-Auth` value\n   *         Some(true) if web annotation requires an authenticated whisk user and that user has already authenticated\n   *         Some(false) if web annotation includes require-whisk-auth and the request does not include the header `X-Require-Whisk-Auth`\n   *         Some(false) if web annotation includes require-whisk-auth and its value does not match the request header `X-Require-Whisk-Auth` value\n   */\n  private def confirmAuthenticated(annotations: Parameters,\n                                   reqHeaders: Seq[HttpHeader],\n                                   authenticatedUser: Option[Identity]): Option[Boolean] = {\n    def checkAuthHeader(expected: String): Boolean = {\n      reqHeaders.find(_.is(WhiskAction.requireWhiskAuthHeader)).map(_.value == expected).getOrElse(false)\n    }\n\n    annotations\n      .get(Annotations.RequireWhiskAuthAnnotation)\n      .map {\n        case JsString(auth)             => checkAuthHeader(auth) // allowed if auth matches header\n        case JsNumber(auth)             => checkAuthHeader(auth.toString) // allowed if auth matches header\n        case JsTrue | JsBoolean(true)   => authenticatedUser.isDefined // allowed if user already authenticated\n        case JsFalse | JsBoolean(false) => true // allowed if the require-whisk-auth is specified as false\n        case _                          => false // not allowed, something is not right\n      }\n  }\n\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PostActionActivation.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.actions\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration.FiniteDuration\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.BadRequest\n\nimport spray.json._\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.controller.RejectRequest\nimport org.apache.openwhisk.core.controller.WhiskServices\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.Messages\n\nprotected[core] trait PostActionActivation extends PrimitiveActions with SequenceActions {\n  /** The core collections require backend services to be injected in this trait. */\n  services: WhiskServices =>\n\n  /**\n   * Invokes an action which may be a sequence or a primitive (single) action.\n   *\n   * @param user the user posting the activation\n   * @param action the action to activate (parameters for packaged actions must already be merged)\n   * @param payload the parameters to pass to the action\n   * @param waitForResponse if not empty, wait up to specified duration for a response (this is used for blocking activations)\n   * @return a future that resolves with Left(activation id) when the request is queued, or Right(activation) for a blocking request\n   *         which completes in time iff waiting for an response\n   */\n  protected[controller] def invokeAction(\n    user: Identity,\n    action: WhiskActionMetaData,\n    payload: Option[JsValue],\n    waitForResponse: Option[FiniteDuration],\n    cause: Option[ActivationId])(implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {\n    action.toExecutableWhiskAction match {\n      // this is a topmost sequence\n      case None =>\n        val SequenceExecMetaData(components) = action.exec\n        invokeSequence(user, action, components, payload, waitForResponse, cause, topmost = true, 0).map(r => r._1)\n      // a non-deprecated ExecutableWhiskAction\n      case Some(executable) if !executable.exec.deprecated =>\n        invokeSingleAction(user, executable, payload, waitForResponse, cause)\n      // a deprecated exec\n      case _ =>\n        Future.failed(RejectRequest(BadRequest, Messages.runtimeDeprecated(action.exec)))\n    }\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PrimitiveActions.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.actions\n\nimport java.time.{Clock, Instant}\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging.InfoLevel\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.tracing.WhiskTracerProvider\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, TransactionId, UserEvents}\nimport org.apache.openwhisk.core.connector.{ActivationMessage, EventMessage, MessagingProvider}\nimport org.apache.openwhisk.core.controller.WhiskServices\nimport org.apache.openwhisk.core.database.{ActivationStore, NoDocumentException, UserContext}\nimport org.apache.openwhisk.core.entitlement.{Resource, _}\nimport org.apache.openwhisk.core.entity.ActivationResponse.ERROR_FIELD\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.apache.openwhisk.core.entity.types.EntityStore\nimport org.apache.openwhisk.http.Messages._\nimport org.apache.openwhisk.spi.SpiLoader\nimport org.apache.openwhisk.utils.ExecutionContextFactory.FutureExtensions\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.Interval\n\nimport scala.collection.mutable.Buffer\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future, Promise}\nimport scala.language.postfixOps\nimport scala.util.{Failure, Success}\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nprotected[actions] trait PrimitiveActions {\n  /** The core collections require backend services to be injected in this trait. */\n  services: WhiskServices =>\n\n  /** An actor system for timed based futures. */\n  protected implicit val actorSystem: ActorSystem\n\n  /** An execution context for futures. */\n  protected implicit val executionContext: ExecutionContext\n\n  protected implicit val logging: Logging\n\n  /**\n   *  The index of the active ack topic, this controller is listening for.\n   *  Typically this is also the instance number of the controller\n   */\n  protected val activeAckTopicIndex: ControllerInstanceId\n\n  /** Database service to CRUD actions. */\n  protected val entityStore: EntityStore\n\n  /** Database service to get activations. */\n  protected val activationStore: ActivationStore\n\n  /** Message producer. This is needed to write user-metrics. */\n  private val messagingProvider = SpiLoader.get[MessagingProvider]\n  private val producer = messagingProvider.getProducer(services.whiskConfig)\n\n  /** A method that knows how to invoke a sequence of actions. */\n  protected[actions] def invokeSequence(\n    user: Identity,\n    action: WhiskActionMetaData,\n    components: Vector[FullyQualifiedEntityName],\n    payload: Option[JsValue],\n    waitForOutermostResponse: Option[FiniteDuration],\n    cause: Option[ActivationId],\n    topmost: Boolean,\n    atomicActionsCount: Int)(implicit transid: TransactionId): Future[(Either[ActivationId, WhiskActivation], Int)]\n\n  /**\n   * A method that knows how to invoke a single primitive action or a composition.\n   *\n   * A composition is a kind of sequence of actions that is dynamically computed.\n   * The execution of a composition is triggered by the invocation of a conductor action.\n   * A conductor action is an executable action with a truthy \"conductor\" annotation.\n   * Sequences cannot be compositions: the \"conductor\" annotation on a sequence has no effect.\n   *\n   * A conductor action may either return a final result or a triplet { action, params, state }.\n   * In the latter case, the specified component action is invoked on the specified params object.\n   * Upon completion of this action the conductor action is reinvoked with a payload that combines\n   * the output of the action with the state returned by the previous conductor invocation.\n   * The composition result is the result of the final conductor invocation in the chain of invocations.\n   *\n   * The trace of a composition obeys the grammar: conductorInvocation(componentInvocation conductorInvocation)*\n   *\n   * The activation records for a composition and its components mimic the activation records of sequences.\n   * They include the same \"topmost\", \"kind\", and \"causedBy\" annotations with the same semantics.\n   * The activation record for a composition also includes a specific annotation \"conductor\" with value true.\n   */\n  protected[actions] def invokeSingleAction(\n    user: Identity,\n    action: ExecutableWhiskActionMetaData,\n    payload: Option[JsValue],\n    waitForResponse: Option[FiniteDuration],\n    cause: Option[ActivationId])(implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {\n\n    if (action.annotations.isTruthy(WhiskActivation.conductorAnnotation)) {\n      invokeComposition(user, action, payload, waitForResponse, cause)\n    } else {\n      invokeSimpleAction(user, action, payload, waitForResponse, cause)\n    }\n  }\n\n  /**\n   * A method that knows how to invoke a single primitive action.\n   *\n   * Posts request to the loadbalancer. If the loadbalancer accepts the requests with an activation id,\n   * then wait for the result of the activation if necessary.\n   *\n   * NOTE:\n   * Cause is populated only for actions that were invoked as a result of a sequence activation or a composition.\n   * For actions that are enclosed in a sequence and are activated as a result of the sequence activation, the cause\n   * contains the activation id of the immediately enclosing sequence.\n   * e.g.,: s -> a, x, c    and   x -> c  (x and s are sequences, a, b, c atomic actions)\n   * cause for a, x, c is the activation id of s\n   * cause for c is the activation id of x\n   * cause for s is not defined\n   * For actions that are enclosed in a composition and are activated as a result of the composition activation,\n   * the cause contains the activation id of the immediately enclosing composition.\n   *\n   * @param user the identity invoking the action\n   * @param action the action to invoke\n   * @param payload the dynamic arguments for the activation\n   * @param waitForResponse if not empty, wait upto specified duration for a response (this is used for blocking activations)\n   * @param cause the activation id that is responsible for this invoke/activation\n   * @param transid a transaction id for logging\n   * @return a promise that completes with one of the following successful cases:\n   *            Right(WhiskActivation) if waiting for a response and response is ready within allowed duration,\n   *            Left(ActivationId) if not waiting for a response, or allowed duration has elapsed without a result ready\n   *         or these custom failures:\n   *            RequestEntityTooLarge if the message is too large to to post to the message bus\n   */\n  private def invokeSimpleAction(\n    user: Identity,\n    action: ExecutableWhiskActionMetaData,\n    payload: Option[JsValue],\n    waitForResponse: Option[FiniteDuration],\n    cause: Option[ActivationId])(implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {\n\n    // merge package parameters with action (action parameters supersede), then merge in payload\n    val args: Option[JsValue] = payload match {\n      case Some(JsObject(fields))  => action.parameters merge Some(JsObject(fields))\n      case Some(JsArray(elements)) => Some(JsArray(elements))\n      case _                       => Some(action.parameters.toJsObject)\n    }\n    val activationId = activationIdFactory.make()\n\n    val startActivation = transid.started(\n      this,\n      waitForResponse\n        .map(_ => LoggingMarkers.CONTROLLER_ACTIVATION_BLOCKING)\n        .getOrElse(LoggingMarkers.CONTROLLER_ACTIVATION),\n      logLevel = InfoLevel)\n    val startLoadbalancer =\n      transid.started(this, LoggingMarkers.CONTROLLER_LOADBALANCER, s\"action activation id: ${activationId}\")\n\n    val keySet = payload match {\n      case Some(JsObject(fields)) => Some(fields.keySet)\n      case _                      => None\n    }\n    val message = ActivationMessage(\n      transid,\n      FullyQualifiedEntityName(action.namespace, action.name, Some(action.version), action.binding),\n      action.rev,\n      user,\n      activationId, // activation id created here\n      activeAckTopicIndex,\n      waitForResponse.isDefined,\n      args,\n      action.parameters.initParameters,\n      action.parameters.lockedParameters(keySet.getOrElse(Set.empty)),\n      cause = cause,\n      WhiskTracerProvider.tracer.getTraceContext(transid))\n\n    val postedFuture = loadBalancer.publish(action, message)\n\n    postedFuture andThen {\n      case Success(_) => transid.finished(this, startLoadbalancer)\n      case Failure(e) => transid.failed(this, startLoadbalancer, e.getMessage)\n    } flatMap { activeAckResponse =>\n      // is caller waiting for the result of the activation?\n      waitForResponse\n        .map { timeout =>\n          // yes, then wait for the activation response from the message bus\n          // (known as the active response or active ack)\n          waitForActivationResponse(user, message.activationId, timeout, activeAckResponse)\n        }\n        .getOrElse {\n          // no, return the activation id\n          Future.successful(Left(message.activationId))\n        }\n    } andThen {\n      case Success(_) => transid.finished(this, startActivation)\n      case Failure(e) => transid.failed(this, startActivation, e.getMessage)\n    }\n  }\n\n  /**\n   * Mutable cumulative accounting of what happened during the execution of a composition.\n   *\n   * Compositions are aborted if the number of action invocations exceeds a limit.\n   * The permitted max is n component invocations plus 2n+1 conductor invocations (where n is the actionSequenceLimit).\n   * The max is chosen to permit a sequence with up to n primitive actions.\n   *\n   * NOTE:\n   * A sequence invocation counts as one invocation irrespective of the number of action invocations in the sequence.\n   * If one component of a composition is also a composition, the caller and callee share the same accounting object.\n   * The counts are shared between callers and callees so the limit applies globally.\n   *\n   * @param components the current count of component actions already invoked\n   * @param conductors the current count of conductor actions already invoked\n   */\n  private case class CompositionAccounting(var components: Int = 0, var conductors: Int = 0)\n\n  /**\n   * A mutable session object to keep track of the execution of one composition.\n   *\n   * NOTE:\n   * The session object is not shared between callers and callees.\n   *\n   * @param activationId the activationId for the composition (ie the activation record for the composition)\n   * @param start the start time for the composition\n   * @param action the conductor action responsible for the execution of the composition\n   * @param cause the cause of the composition (activationId of the enclosing sequence or composition if any)\n   * @param duration the \"user\" time so far executing the composition (sum of durations for\n   *        all actions invoked so far which is different from the total time spent executing the composition)\n   * @param maxMemory the maximum memory annotation observed so far for the conductor action and components\n   * @param state the json state object to inject in the parameter object of the next conductor invocation\n   * @param accounting the global accounting object used to abort compositions requiring too many action invocations\n   * @param logs a mutable buffer that is appended with new activation ids as the composition unfolds\n   *             (in contrast with sequences, the logs of a hierarchy of compositions is not flattened)\n   */\n  private case class Session(activationId: ActivationId,\n                             start: Instant,\n                             action: ExecutableWhiskActionMetaData,\n                             cause: Option[ActivationId],\n                             var duration: Long,\n                             var maxMemory: ByteSize,\n                             var state: Option[JsObject],\n                             accounting: CompositionAccounting,\n                             logs: Buffer[ActivationId])\n\n  /**\n   * A method that knows how to invoke a composition.\n   *\n   * The method instantiates the session object for the composition and invokes the conductor action.\n   * It waits for the activation response, synthesizes the activation record and writes it to the datastore.\n   * It distinguishes nested, blocking and non-blocking invokes, returning either the activation or the activation id.\n   *\n   * @param user the identity invoking the action\n   * @param action the conductor action to invoke for the composition\n   * @param payload the dynamic arguments for the activation\n   * @param waitForResponse if not empty, wait upto specified duration for a response (this is used for blocking activations)\n   * @param cause the activation id that is responsible for this invoke/activation\n   * @param accounting the accounting object for the caller if any\n   * @param transid a transaction id for logging\n   * @return a promise that completes with one of the following successful cases:\n   *            Right(WhiskActivation) if waiting for a response and response is ready within allowed duration,\n   *            Left(ActivationId) if not waiting for a response, or allowed duration has elapsed without a result ready\n   */\n  private def invokeComposition(user: Identity,\n                                action: ExecutableWhiskActionMetaData,\n                                payload: Option[JsValue],\n                                waitForResponse: Option[FiniteDuration],\n                                cause: Option[ActivationId],\n                                accounting: Option[CompositionAccounting] = None)(\n    implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {\n\n    val session = Session(\n      activationId = activationIdFactory.make(),\n      start = Instant.now(Clock.systemUTC()),\n      action,\n      cause,\n      duration = 0,\n      maxMemory = action.limits.memory.megabytes MB,\n      state = None,\n      accounting = accounting.getOrElse(CompositionAccounting()), // share accounting with caller\n      logs = Buffer.empty)\n\n    logging.info(this, s\"invoking composition $action topmost ${cause.isEmpty} activationid '${session.activationId}'\")\n\n    val response: Future[Either[ActivationId, WhiskActivation]] =\n      invokeConductor(user, payload, session, transid).map(response =>\n        Right(completeActivation(user, session, response, waitForResponse.isDefined)))\n\n    // is caller waiting for the result of the activation?\n    cause\n      .map(_ => response) // ignore waitForResponse when not topmost\n      .orElse(\n        // blocking invoke, wait until timeout\n        waitForResponse.map(response.withAlternativeAfterTimeout(_, Future.successful(Left(session.activationId)))))\n      .getOrElse(\n        // no, return the session id\n        Future.successful(Left(session.activationId)))\n  }\n\n  /**\n   * A method that knows how to handle a conductor invocation.\n   *\n   * This method prepares the payload and invokes the conductor action.\n   * It parses the result and extracts the name of the next component action if any.\n   * It either invokes the desired component action or completes the composition invocation.\n   * It also checks the invocation counts against the limits.\n   *\n   * @param user the identity invoking the action\n   * @param payload the dynamic arguments for the activation\n   * @param session the session object for this composition\n   * @param parentTid a parent transaction id\n   */\n  private def invokeConductor(user: Identity,\n                              payload: Option[JsValue],\n                              session: Session,\n                              parentTid: TransactionId): Future[ActivationResponse] = {\n\n    implicit val transid: TransactionId = TransactionId.childOf(parentTid)\n\n    if (session.accounting.conductors > 2 * actionSequenceLimit) {\n      // composition is too long\n      Future.successful(ActivationResponse.applicationError(compositionIsTooLong))\n    } else {\n      // inject state into payload if any\n      val params: Option[JsValue] = payload match {\n        case Some(JsObject(fields)) =>\n          session.state\n            .map(state => Some(JsObject(JsObject(fields).fields ++ state.fields)))\n            .getOrElse(payload)\n        case _ => None\n      }\n\n      // invoke conductor action\n      session.accounting.conductors += 1\n      val activationResponse =\n        invokeSimpleAction(\n          user,\n          action = session.action,\n          payload = params,\n          waitForResponse = Some(session.action.limits.timeout.duration + 1.minute), // wait for result\n          cause = Some(session.activationId)) // cause is session id\n\n      waitForActivation(user, session, activationResponse).flatMap {\n        case Left(response) => // unsuccessful invocation, return error response\n          Future.successful(response)\n        case Right(activation) => // successful invocation\n          val result = activation.resultAsJson\n\n          // extract params from result, auto boxing result if not a dictionary\n          val params = result.fields.get(WhiskActivation.paramsField).map {\n            case obj: JsObject => obj\n            case value         => JsObject(WhiskActivation.valueField -> value)\n          }\n\n          // update session state, auto boxing state if not a dictionary\n          session.state = result.fields.get(WhiskActivation.stateField).map {\n            case obj: JsObject => obj\n            case value         => JsObject(WhiskActivation.stateField -> value)\n          }\n\n          // extract next action from result and invoke\n          result.fields.get(WhiskActivation.actionField) match {\n            case None =>\n              // no next action, end composition execution, return to caller\n              Future.successful(ActivationResponse(activation.response.statusCode, Some(params.getOrElse(result))))\n            case Some(next) =>\n              FullyQualifiedEntityName.resolveName(next, user.namespace.name) match {\n                case Some(fqn) if session.accounting.components < actionSequenceLimit =>\n                  tryInvokeNext(user, fqn, params, session, transid)\n\n                case Some(_) => // composition is too long\n                  invokeConductor(\n                    user,\n                    payload = Some(JsObject(ERROR_FIELD -> JsString(compositionIsTooLong))),\n                    session = session,\n                    transid)\n\n                case None => // parsing failure\n                  invokeConductor(\n                    user,\n                    payload = Some(JsObject(ERROR_FIELD -> JsString(compositionComponentInvalid(next)))),\n                    session = session,\n                    transid)\n\n              }\n          }\n      }\n    }\n\n  }\n\n  /**\n   * Checks if the user is entitled to invoke the next action and invokes it.\n   *\n   * @param user the subject\n   * @param fqn the name of the action\n   * @param params parameters for the action\n   * @param session the session for the current activation\n   * @return promise for the eventual activation\n   */\n  private def tryInvokeNext(user: Identity,\n                            fqn: FullyQualifiedEntityName,\n                            params: Option[JsObject],\n                            session: Session,\n                            parentTid: TransactionId): Future[ActivationResponse] = {\n\n    implicit val transid: TransactionId = TransactionId.childOf(parentTid)\n\n    val resource = Resource(fqn.path, Collection(Collection.ACTIONS), Some(fqn.name.asString))\n    entitlementProvider\n      .check(user, Privilege.ACTIVATE, Set(resource), noThrottle = true)\n      .flatMap { _ =>\n        // successful entitlement check\n        WhiskActionMetaData\n          .resolveActionAndMergeParameters(entityStore, fqn)\n          .flatMap {\n            case next =>\n              // successful resolution\n              invokeComponent(user, action = next, payload = params, session)\n          }\n          .recoverWith {\n            case _ =>\n              // resolution failure\n              invokeConductor(\n                user,\n                payload = Some(JsObject(ERROR_FIELD -> JsString(compositionComponentNotFound(fqn.asString)))),\n                session = session,\n                transid)\n          }\n      }\n      .recoverWith {\n        case _ =>\n          // failed entitlement check\n          invokeConductor(\n            user,\n            payload = Some(JsObject(ERROR_FIELD -> JsString(compositionComponentNotAccessible(fqn.asString)))),\n            session = session,\n            transid)\n      }\n  }\n\n  /**\n   * A method that knows how to handle a component invocation.\n   *\n   * This method distinguishes primitive actions, sequences, and compositions.\n   * The conductor action is reinvoked after the successful invocation of the component.\n   *\n   * @param user the identity invoking the action\n   * @param action the component action to invoke\n   * @param payload the dynamic arguments for the activation\n   * @param session the session object for this composition\n   * @param transid a transaction id for logging\n   */\n  private def invokeComponent(user: Identity, action: WhiskActionMetaData, payload: Option[JsObject], session: Session)(\n    implicit transid: TransactionId): Future[ActivationResponse] = {\n\n    val exec = action.toExecutableWhiskAction\n    val activationResponse: Future[Either[ActivationId, WhiskActivation]] = exec match {\n      case Some(action) if action.annotations.isTruthy(WhiskActivation.conductorAnnotation) => // composition\n        // invokeComposition will increase the invocation counts\n        invokeComposition(\n          user,\n          action,\n          payload,\n          waitForResponse = None, // not topmost, hence blocking, no need for timeout\n          cause = Some(session.activationId),\n          accounting = Some(session.accounting))\n      case Some(action) => // primitive action\n        session.accounting.components += 1\n        invokeSimpleAction(\n          user,\n          action,\n          payload,\n          waitForResponse = Some(action.limits.timeout.duration + 1.minute),\n          cause = Some(session.activationId))\n      case None => // sequence\n        session.accounting.components += 1\n        val SequenceExecMetaData(components) = action.exec\n        invokeSequence(\n          user,\n          action,\n          components,\n          payload,\n          waitForOutermostResponse = None,\n          cause = Some(session.activationId),\n          topmost = false,\n          atomicActionsCount = 0).map(r => r._1)\n    }\n\n    waitForActivation(user, session, activationResponse).flatMap {\n      case Left(response) => // unsuccessful invocation, return error response\n        Future.successful(response)\n      case Right(activation) => // reinvoke conductor on component result\n        invokeConductor(user, payload = Some(activation.resultAsJson), session = session, transid)\n    }\n  }\n\n  /**\n   * Waits for a response from a conductor of component action invocation.\n   * Handles internal errors (activation failure or timeout).\n   * Logs the activation id and updates the duration and max memory for the session.\n   * Returns the activation record if successful, the error response if not.\n   *\n   * @param user the identity invoking the action\n   * @param session the session object for this composition\n   * @param activationResponse the future activation to wait on\n   * @param transid a transaction id for logging\n   */\n  private def waitForActivation(user: Identity,\n                                session: Session,\n                                activationResponse: Future[Either[ActivationId, WhiskActivation]])(\n    implicit transid: TransactionId): Future[Either[ActivationResponse, WhiskActivation]] = {\n\n    activationResponse\n      .map {\n        case Left(activationId) => // invocation timeout\n          session.logs += activationId\n          Left(ActivationResponse.whiskError(compositionActivationTimeout(activationId)))\n        case Right(activation) => // successful invocation\n          session.logs += activation.activationId\n          // activation.duration should be defined but this is not reflected by the type so be defensive\n          // end - start is a sensible default but not the correct value for sequences and compositions\n          session.duration += activation.duration.getOrElse(activation.end.toEpochMilli - activation.start.toEpochMilli)\n          activation.annotations.get(\"limits\").foreach { limitsAnnotation =>\n            limitsAnnotation.asJsObject.getFields(\"memory\") match {\n              case Seq(JsNumber(memory)) =>\n                session.maxMemory = Math.max(session.maxMemory.toMB.toInt, memory.toInt) MB\n            }\n          }\n          Right(activation)\n      }\n      .recover { // invocation failure\n        case _ => Left(ActivationResponse.whiskError(compositionActivationFailure))\n      }\n  }\n\n  /**\n   * Creates an activation for a composition and writes it back to the datastore.\n   * Returns the activation.\n   */\n  private def completeActivation(user: Identity,\n                                 session: Session,\n                                 response: ActivationResponse,\n                                 blockingComposition: Boolean)(implicit transid: TransactionId): WhiskActivation = {\n\n    val context = UserContext(user)\n\n    // compute max memory\n    val sequenceLimits = Parameters(\n      WhiskActivation.limitsAnnotation,\n      ActionLimits(session.action.limits.timeout, MemoryLimit(session.maxMemory), session.action.limits.logs).toJson)\n\n    // set causedBy if not topmost\n    val causedBy = session.cause.map { _ =>\n      Parameters(WhiskActivation.causedByAnnotation, JsString(Exec.SEQUENCE))\n    }\n\n    // set waitTime for conductor action\n    val waitTime = {\n      Parameters(\n        WhiskActivation.waitTimeAnnotation,\n        Interval(transid.meta.start, session.start).duration.toMillis.toJson)\n    }\n\n    // set binding if invoked action is in a package binding\n    val binding =\n      session.action.binding.map(f => Parameters(WhiskActivation.bindingAnnotation, JsString(f.asString)))\n\n    val end = Instant.now(Clock.systemUTC())\n\n    // create the whisk activation\n    val activation = WhiskActivation(\n      namespace = user.namespace.name.toPath,\n      name = session.action.name,\n      user.subject,\n      activationId = session.activationId,\n      start = session.start,\n      end = end,\n      cause = session.cause,\n      response = response,\n      logs = ActivationLogs(session.logs.map(_.asString).toVector),\n      version = session.action.version,\n      publish = false,\n      annotations = Parameters(WhiskActivation.topmostAnnotation, JsBoolean(session.cause.isEmpty)) ++\n        Parameters(WhiskActivation.pathAnnotation, JsString(session.action.fullyQualifiedName(false).asString)) ++\n        Parameters(WhiskActivation.kindAnnotation, JsString(Exec.SEQUENCE)) ++\n        Parameters(WhiskActivation.conductorAnnotation, JsTrue) ++\n        causedBy ++ waitTime ++ binding ++\n        sequenceLimits,\n      duration = Some(session.duration))\n\n    if (UserEvents.enabled) {\n      EventMessage.from(activation, s\"controller${activeAckTopicIndex.asString}\", user.namespace.uuid) match {\n        case Success(msg) => UserEvents.send(producer, msg)\n        case Failure(t)   => logging.warn(this, s\"activation event was not sent: $t\")\n      }\n    }\n\n    activationStore.storeAfterCheck(activation, blockingComposition, None, None, context)(\n      transid,\n      notifier = None,\n      logging)\n\n    activation\n  }\n\n  /**\n   * Waits for a response from the message bus (e.g., Kafka) containing the result of the activation. This is the fast path\n   * used for blocking calls where only the result of the activation is needed. This path is called active acknowledgement\n   * or active ack.\n   *\n   * While waiting for the active ack, periodically poll the datastore in case there is a failure in the fast path delivery\n   * which could happen if the connection from an invoker to the message bus is disrupted, or if the publishing of the response\n   * fails because the message is too large.\n   */\n  private def waitForActivationResponse(user: Identity,\n                                        activationId: ActivationId,\n                                        totalWaitTime: FiniteDuration,\n                                        activeAckResponse: Future[Either[ActivationId, WhiskActivation]])(\n    implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {\n    val context = UserContext(user)\n    val result = Promise[Either[ActivationId, WhiskActivation]]\n    val docid = new DocId(WhiskEntity.qualifiedName(user.namespace.name.toPath, activationId))\n    logging.debug(this, s\"action activation will block for result upto $totalWaitTime\")\n\n    // 1. Wait for the active-ack to happen. Either immediately resolve the promise or poll the database quickly\n    //    in case of an incomplete active-ack (record too large for example).\n    activeAckResponse.foreach {\n      case Right(activation) => result.trySuccess(Right(activation))\n      case _ if (controllerActivationConfig.pollingFromDb) =>\n        pollActivation(docid, context, result, i => 1.seconds + (2.seconds * i), maxRetries = 4)\n      case Left(activationId) =>\n        result.trySuccess(Left(activationId)) // complete the future immediately if it's configured to not poll db for blocking activations\n    }\n\n    if (controllerActivationConfig.pollingFromDb) {\n      // 2. Poll the database slowly in case the active-ack never arrives\n      pollActivation(docid, context, result, _ => 15.seconds)\n    }\n\n    // 3. Timeout forces a fallback to activationId\n    val timeout = actorSystem.scheduler.scheduleOnce(totalWaitTime)(result.trySuccess(Left(activationId)))\n\n    result.future.andThen {\n      case _ => timeout.cancel()\n    }\n  }\n\n  /**\n   * Polls the database for an activation.\n   *\n   * Does not use Future composition because an early exit is wanted, once any possible external source resolved the\n   * Promise.\n   *\n   * @param docid the docid to poll for\n   * @param result promise to resolve on result. Is also used to abort polling once completed.\n   */\n  private def pollActivation(docid: DocId,\n                             context: UserContext,\n                             result: Promise[Either[ActivationId, WhiskActivation]],\n                             wait: Int => FiniteDuration,\n                             retries: Int = 0,\n                             maxRetries: Int = Int.MaxValue)(implicit transid: TransactionId): Unit = {\n    if (!result.isCompleted && retries < maxRetries) {\n      val schedule = actorSystem.scheduler.scheduleOnce(wait(retries)) {\n        activationStore.get(ActivationId(docid.asString), context).onComplete {\n          case Success(activation) =>\n            transid.mark(\n              this,\n              LoggingMarkers.CONTROLLER_ACTIVATION_BLOCKING_DATABASE_RETRIEVAL,\n              s\"retrieved activation for blocking invocation via DB polling\",\n              logLevel = InfoLevel)\n            result.trySuccess(Right(activation.withoutLogs))\n          case Failure(_: NoDocumentException) => pollActivation(docid, context, result, wait, retries + 1, maxRetries)\n          case Failure(t: Throwable)           => result.tryFailure(t)\n        }\n      }\n\n      // Halt the schedule if the result is provided during one execution\n      result.future.onComplete(_ => schedule.cancel())\n    }\n  }\n\n  /** Max atomic action count allowed for sequences */\n  private lazy val actionSequenceLimit = whiskConfig.actionSequenceLimit.toInt\n\n  protected val controllerActivationConfig =\n    loadConfigOrThrow[ControllerActivationConfig](ConfigKeys.controllerActivation)\n\n}\n\ncase class ControllerActivationConfig(pollingFromDb: Boolean, maxWaitForBlockingActivation: FiniteDuration)\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/SequenceActions.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.actions\n\nimport java.time.{Clock, Instant}\nimport java.util.concurrent.atomic.AtomicReference\n\nimport org.apache.pekko.actor.ActorSystem\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.{Logging, TransactionId, UserEvents}\nimport org.apache.openwhisk.core.connector.{EventMessage, MessagingProvider}\nimport org.apache.openwhisk.core.containerpool.Interval\nimport org.apache.openwhisk.core.controller.WhiskServices\nimport org.apache.openwhisk.core.database.{ActivationStore, NoDocumentException, UserContext}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.apache.openwhisk.core.entity.types._\nimport org.apache.openwhisk.http.Messages._\nimport org.apache.openwhisk.spi.SpiLoader\nimport org.apache.openwhisk.utils.ExecutionContextFactory.FutureExtensions\nimport spray.json._\n\nimport scala.collection._\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.language.postfixOps\nimport scala.util.{Failure, Success}\n\nprotected[actions] trait SequenceActions {\n  /** The core collections require backend services to be injected in this trait. */\n  services: WhiskServices =>\n\n  /** An actor system for timed based futures. */\n  protected implicit val actorSystem: ActorSystem\n\n  /** An execution context for futures. */\n  protected implicit val executionContext: ExecutionContext\n\n  protected implicit val logging: Logging\n\n  /** Database service to CRUD actions. */\n  protected val entityStore: EntityStore\n\n  /** Database service to get activations. */\n  protected val activationStore: ActivationStore\n\n  /** Instace of the controller. This is needed to write user-metrics. */\n  protected val activeAckTopicIndex: ControllerInstanceId\n\n  /** Message producer. This is needed to write user-metrics. */\n  private val messagingProvider = SpiLoader.get[MessagingProvider]\n  private val producer = messagingProvider.getProducer(services.whiskConfig)\n\n  /** A method that knows how to invoke a single primitive action. */\n  protected[actions] def invokeAction(\n    user: Identity,\n    action: WhiskActionMetaData,\n    payload: Option[JsValue],\n    waitForResponse: Option[FiniteDuration],\n    cause: Option[ActivationId])(implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]]\n\n  /**\n   * Executes a sequence by invoking in a blocking fashion each of its components.\n   *\n   * @param user the user invoking the action\n   * @param action the sequence action to be invoked\n   * @param components the actions in the sequence\n   * @param payload the dynamic arguments for the activation\n   * @param waitForOutermostResponse some duration iff this is a blocking invoke\n   * @param cause the id of the activation that caused this sequence (defined only for inner sequences and None for topmost sequences)\n   * @param topmost true iff this is the topmost sequence invoked directly through the api (not indirectly through a sequence)\n   * @param atomicActionsCount the dynamic atomic action count observed so far since the start of invocation of the topmost sequence(0 if topmost)\n   * @param transid a transaction id for logging\n   * @return a future of type (ActivationId, Some(WhiskActivation), atomicActionsCount) if blocking; else (ActivationId, None, 0)\n   */\n  protected[actions] def invokeSequence(\n    user: Identity,\n    action: WhiskActionMetaData,\n    components: Vector[FullyQualifiedEntityName],\n    payload: Option[JsValue],\n    waitForOutermostResponse: Option[FiniteDuration],\n    cause: Option[ActivationId],\n    topmost: Boolean,\n    atomicActionsCount: Int)(implicit transid: TransactionId): Future[(Either[ActivationId, WhiskActivation], Int)] = {\n    require(action.exec.kind == Exec.SEQUENCE, \"this method requires an action sequence\")\n\n    // create new activation id that corresponds to the sequence\n    val seqActivationId = activationIdFactory.make()\n    logging.info(this, s\"invoking sequence $action topmost $topmost activationid '$seqActivationId'\")\n\n    val start = Instant.now(Clock.systemUTC())\n    val futureSeqResult: Future[(Either[ActivationId, WhiskActivation], Int)] = {\n      // even though the result of completeSequenceActivation is Right[WhiskActivation],\n      // use a more general type for futureSeqResult in case a blocking invoke takes\n      // longer than expected and we must return Left[ActivationId] instead\n      completeSequenceActivation(\n        seqActivationId,\n        // the cause for the component activations is the current sequence\n        invokeSequenceComponents(\n          user,\n          action,\n          seqActivationId,\n          payload,\n          components,\n          cause = Some(seqActivationId),\n          atomicActionsCount),\n        user,\n        action,\n        topmost,\n        waitForOutermostResponse.isDefined,\n        start,\n        cause)\n    }\n\n    if (topmost) { // need to deal with blocking and closing connection\n      waitForOutermostResponse\n        .map { timeout =>\n          logging.debug(this, s\"invoke sequence blocking topmost!\")\n          futureSeqResult.withAlternativeAfterTimeout(\n            timeout,\n            Future.successful(Left(seqActivationId), atomicActionsCount))\n        }\n        .getOrElse {\n          // non-blocking sequence execution, return activation id\n          Future.successful(Left(seqActivationId), 0)\n        }\n    } else {\n      // not topmost, no need to worry about terminating incoming request\n      // and this is a blocking activation therefore by definition\n      // Note: the future for the sequence result recovers from all throwable failures\n      futureSeqResult\n    }\n  }\n\n  /**\n   * Creates an activation for the sequence and writes it back to the datastore.\n   */\n  private def completeSequenceActivation(seqActivationId: ActivationId,\n                                         futureSeqResult: Future[SequenceAccounting],\n                                         user: Identity,\n                                         action: WhiskActionMetaData,\n                                         topmost: Boolean,\n                                         blockingSequence: Boolean,\n                                         start: Instant,\n                                         cause: Option[ActivationId])(\n    implicit transid: TransactionId): Future[(Right[ActivationId, WhiskActivation], Int)] = {\n    val context = UserContext(user)\n\n    // not topmost, no need to worry about terminating incoming request\n    // Note: the future for the sequence result recovers from all throwable failures\n    futureSeqResult\n      .map { accounting =>\n        // sequence terminated, the result of the sequence is the result of the last completed activation\n        val end = Instant.now(Clock.systemUTC())\n        val seqActivation =\n          makeSequenceActivation(user, action, seqActivationId, accounting, topmost, cause, start, end)\n        (Right(seqActivation), accounting.atomicActionCnt)\n      }\n      .andThen {\n        case Success((Right(seqActivation), _)) =>\n          if (UserEvents.enabled) {\n            EventMessage.from(seqActivation, s\"controller${activeAckTopicIndex.asString}\", user.namespace.uuid) match {\n              case Success(msg) => UserEvents.send(producer, msg)\n              case Failure(t)   => logging.warn(this, s\"activation event was not sent: $t\")\n            }\n          }\n\n          activationStore.storeAfterCheck(seqActivation, blockingSequence, None, None, context)(\n            transid,\n            notifier = None,\n            logging)\n\n        // This should never happen; in this case, there is no activation record created or stored:\n        // should there be?\n        case Failure(t) => logging.error(this, s\"sequence activation failed: ${t.getMessage}\")\n      }\n  }\n\n  /**\n   * Creates an activation for a sequence.\n   */\n  private def makeSequenceActivation(user: Identity,\n                                     action: WhiskActionMetaData,\n                                     activationId: ActivationId,\n                                     accounting: SequenceAccounting,\n                                     topmost: Boolean,\n                                     cause: Option[ActivationId],\n                                     start: Instant,\n                                     end: Instant)(implicit transid: TransactionId): WhiskActivation = {\n\n    // compute max memory\n    val sequenceLimits = accounting.maxMemory map { maxMemoryAcrossActionsInSequence =>\n      Parameters(\n        WhiskActivation.limitsAnnotation,\n        ActionLimits(action.limits.timeout, MemoryLimit(maxMemoryAcrossActionsInSequence MB), action.limits.logs).toJson)\n    }\n\n    // set causedBy if not topmost sequence\n    val causedBy = if (!topmost) {\n      Some(Parameters(WhiskActivation.causedByAnnotation, JsString(Exec.SEQUENCE)))\n    } else None\n\n    // set waitTime for sequence action\n    val waitTime = {\n      Parameters(WhiskActivation.waitTimeAnnotation, Interval(transid.meta.start, start).duration.toMillis.toJson)\n    }\n\n    // set binding if an invoked action is in a package binding\n    val binding = action.binding map { path =>\n      Parameters(WhiskActivation.bindingAnnotation, JsString(path.asString))\n    }\n\n    // create the whisk activation\n    WhiskActivation(\n      namespace = user.namespace.name.toPath,\n      name = action.name,\n      user.subject,\n      activationId = activationId,\n      start = start,\n      end = end,\n      cause = if (topmost) None else cause, // propagate the cause for inner sequences, but undefined for topmost\n      response = accounting.previousResponse.getAndSet(null), // getAndSet(null) drops reference to the activation result\n      logs = accounting.finalLogs,\n      version = action.version,\n      publish = false,\n      annotations = Parameters(WhiskActivation.topmostAnnotation, JsBoolean(topmost)) ++\n        Parameters(WhiskActivation.pathAnnotation, JsString(action.fullyQualifiedName(false).asString)) ++\n        Parameters(WhiskActivation.kindAnnotation, JsString(Exec.SEQUENCE)) ++\n        causedBy ++ waitTime ++ binding ++\n        sequenceLimits,\n      duration = Some(accounting.duration))\n  }\n\n  /**\n   * Invokes the components of a sequence in a blocking fashion.\n   * Returns a vector of successful futures containing the results of the invocation of all components in the sequence.\n   * Unexpected behavior is modeled through an Either with activation(right) or activation response in case of error (left).\n   *\n   * Keeps track of the dynamic atomic action count.\n   * @param user the user invoking the sequence\n   * @param seqAction the sequence invoked\n   * @param seqActivationId the id of the sequence\n   * @param inputPayload the payload passed to the first component in the sequence\n   * @param components the components in the sequence\n   * @param cause the activation id of the sequence that lead to invoking this sequence or None if this sequence is topmost\n   * @param atomicActionCnt the dynamic atomic action count observed so far since the start of the execution of the topmost sequence\n   * @return a future which resolves with the accounting for a sequence, including the last result, duration, and activation ids\n   */\n  private def invokeSequenceComponents(\n    user: Identity,\n    seqAction: WhiskActionMetaData,\n    seqActivationId: ActivationId,\n    inputPayload: Option[JsValue],\n    components: Vector[FullyQualifiedEntityName],\n    cause: Option[ActivationId],\n    atomicActionCnt: Int)(implicit transid: TransactionId): Future[SequenceAccounting] = {\n\n    // For each action in the sequence, fetch any of its associated parameters (including package or binding).\n    // We do this for all of the actions in the sequence even though it may be short circuited. This is to\n    // hide the latency of the fetches from the datastore and the parameter merging that has to occur. It\n    // may be desirable in the future to selectively speculate over a smaller number of components rather than\n    // the entire sequence.\n    //\n    // This action/parameter resolution is done in futures; the execution starts as soon as the first component\n    // is resolved.\n    val resolvedFutureActions = resolveDefaultNamespace(components, user) map { c =>\n      WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, c)\n    }\n\n    // this holds the initial value of the accounting structure, including the input boxed as an ActivationResponse\n    val initialAccounting = Future.successful {\n      SequenceAccounting(atomicActionCnt, ActivationResponse.payloadPlaceholder(inputPayload))\n    }\n\n    // execute the actions in sequential blocking fashion\n    resolvedFutureActions\n      .foldLeft(initialAccounting) { (accountingFuture, futureAction) =>\n        accountingFuture.flatMap { accounting =>\n          if (accounting.atomicActionCnt < actionSequenceLimit) {\n            invokeNextAction(user, futureAction, accounting, cause, transid)\n              .flatMap { accounting =>\n                if (!accounting.shortcircuit) {\n                  Future.successful(accounting)\n                } else {\n                  // this is to short circuit the fold\n                  Future.failed(FailedSequenceActivation(accounting)) // terminates the fold\n                }\n              }\n              .recoverWith {\n                case _: NoDocumentException =>\n                  val updatedAccount =\n                    accounting.fail(ActivationResponse.applicationError(sequenceComponentNotFound), None)\n                  Future.failed(FailedSequenceActivation(updatedAccount)) // terminates the fold\n              }\n          } else {\n            val updatedAccount = accounting.fail(ActivationResponse.applicationError(sequenceIsTooLong), None)\n            Future.failed(FailedSequenceActivation(updatedAccount)) // terminates the fold\n          }\n        }\n      }\n      .recoverWith {\n        // turn the failed accounting back to success; this is the only possible failure\n        // since all throwables are recovered with a failed accounting instance and this is\n        // in turned boxed to FailedSequenceActivation\n        case FailedSequenceActivation(accounting) => Future.successful(accounting)\n      }\n  }\n\n  /**\n   * Invokes one component from a sequence action. Unless an unexpected whisk failure happens, the future returned is always successful.\n   * The return is a tuple of\n   *       1. either an activation (right) or an activation response (left) in case the activation could not be retrieved\n   *       2. the dynamic count of atomic actions observed so far since the start of the topmost sequence on behalf which this action is executing\n   *\n   * The method distinguishes between invoking a sequence or an atomic action.\n   * @param user the user executing the sequence\n   * @param futureAction the future which fetches the action to be invoked from the db\n   * @param accounting the state of the sequence activation, contains the dynamic activation count, logs and payload for the next action\n   * @param cause the activation id of the first sequence containing this activations\n   * @return a future which resolves with updated accounting for a sequence, including the last result, duration, and activation ids\n   */\n  private def invokeNextAction(user: Identity,\n                               futureAction: Future[WhiskActionMetaData],\n                               accounting: SequenceAccounting,\n                               cause: Option[ActivationId],\n                               parentTid: TransactionId): Future[SequenceAccounting] = {\n    futureAction.flatMap { action =>\n      implicit val transid: TransactionId = TransactionId.childOf(parentTid)\n\n      // the previous response becomes input for the next action in the sequence;\n      // the accounting no longer needs to hold a reference to it once the action is\n      // invoked, so previousResponse.getAndSet(null) drops the reference at this point\n      // which prevents dragging the previous response for the lifetime of the next activation\n      val previousResult = accounting.previousResponse.getAndSet(null).result\n      val inputPayload: Option[JsValue] = previousResult match {\n        case Some(JsObject(fields))  => Some(JsObject(fields))\n        case Some(JsArray(elements)) => Some(JsArray(elements))\n        case _                       => None\n      }\n\n      // invoke the action by calling the right method depending on whether it's an atomic action or a sequence\n      val futureWhiskActivationTuple = action.toExecutableWhiskAction match {\n        case None =>\n          val SequenceExecMetaData(components) = action.exec\n          logging.debug(this, s\"sequence invoking an enclosed sequence $action\")\n          // call invokeSequence to invoke the inner sequence; this is a blocking activation by definition\n          invokeSequence(\n            user,\n            action,\n            components,\n            inputPayload,\n            None,\n            cause,\n            topmost = false,\n            accounting.atomicActionCnt)\n        case Some(executable) =>\n          // this is an invoke for an atomic action\n          logging.debug(this, s\"sequence invoking an enclosed atomic action $action\")\n          val timeout = action.limits.timeout.duration + 1.minute\n          invokeAction(user, action, inputPayload, waitForResponse = Some(timeout), cause) map {\n            case res => (res, accounting.atomicActionCnt + 1)\n          }\n      }\n\n      futureWhiskActivationTuple\n        .map {\n          case (Right(activation), atomicActionCountSoFar) =>\n            accounting.maybe(activation, atomicActionCountSoFar, actionSequenceLimit)\n\n          case (Left(activationId), atomicActionCountSoFar) =>\n            // the result could not be retrieved in time either from active ack or from db\n            logging.error(this, s\"component activation timedout for $activationId\")\n            val activationResponse = ActivationResponse.whiskError(sequenceRetrieveActivationTimeout(activationId))\n            accounting.fail(activationResponse, Some(activationId))\n\n        }\n        .recover {\n          // check any failure here and generate an activation response to encapsulate\n          // the failure mode; consider this failure a whisk error\n          case t: Throwable =>\n            logging.error(this, s\"component activation failed: $t\")\n            accounting.fail(ActivationResponse.whiskError(sequenceActivationFailure), None)\n        }\n    }\n  }\n\n  /** Replaces default namespaces in a vector of components from a sequence with appropriate namespace. */\n  private def resolveDefaultNamespace(components: Vector[FullyQualifiedEntityName],\n                                      user: Identity): Vector[FullyQualifiedEntityName] = {\n    // resolve any namespaces that may appears as \"_\" (the default namespace)\n    components.map(c => FullyQualifiedEntityName(c.path.resolveNamespace(user.namespace), c.name))\n  }\n\n  /** Max atomic action count allowed for sequences */\n  private lazy val actionSequenceLimit = whiskConfig.actionSequenceLimit.toInt\n\n}\n\n/**\n * Cumulative accounting of what happened during the execution of a sequence.\n *\n * @param atomicActionCnt the current count of non-sequence (c.f. atomic) actions already invoked\n * @param previousResponse a reference to the previous activation result which will be nulled out\n *        when no longer needed (see previousResponse.getAndSet(null) below)\n * @param logs a mutable buffer that is appended with new activation ids as the sequence unfolds\n * @param duration the \"user\" time so far executing the sequence (sum of durations for\n *        all actions invoked so far which is different from the total time spent executing the sequence)\n * @param maxMemory the maximum memory annotation observed so far for the\n *        components (needed to annotate the sequence with GB-s)\n * @param shortcircuit when true, stops the execution of the next component in the sequence\n */\nprotected[actions] case class SequenceAccounting(atomicActionCnt: Int,\n                                                 previousResponse: AtomicReference[ActivationResponse],\n                                                 logs: mutable.Buffer[ActivationId],\n                                                 duration: Long = 0,\n                                                 maxMemory: Option[Int] = None,\n                                                 shortcircuit: Boolean = false) {\n\n  /** @return the ActivationLogs data structure for this sequence invocation */\n  def finalLogs = ActivationLogs(logs.map(id => id.asString).toVector)\n\n  /** The previous activation was successful. */\n  private def success(activation: WhiskActivation, newCnt: Int, shortcircuit: Boolean = false) = {\n    previousResponse.set(null)\n    SequenceAccounting(\n      prev = this,\n      newCnt = newCnt,\n      shortcircuit = shortcircuit,\n      incrDuration = activation.duration,\n      newResponse = activation.response,\n      newActivationId = activation.activationId,\n      newMemoryLimit = activation.annotations.get(\"limits\") map { limitsAnnotation => // we have a limits annotation\n        limitsAnnotation.asJsObject.getFields(\"memory\") match {\n          case Seq(JsNumber(memory)) =>\n            Some(memory.toInt) // we have a numerical \"memory\" field in the \"limits\" annotation\n        }\n      } getOrElse { None })\n  }\n\n  /** The previous activation failed (this is used when there is no activation record or an internal error. */\n  def fail(failureResponse: ActivationResponse, activationId: Option[ActivationId]) = {\n    require(!failureResponse.isSuccess)\n    logs.appendAll(activationId)\n    copy(previousResponse = new AtomicReference(failureResponse), shortcircuit = true)\n  }\n\n  /** Determines whether the previous activation succeeded or failed. */\n  def maybe(activation: WhiskActivation, newCnt: Int, maxSequenceCnt: Int) = {\n    // check conditions on payload that may lead to interrupting the execution of the sequence\n    //     short-circuit the execution of the sequence iff the payload contains an error field\n    //     and is the result of an action return, not the initial payload\n    val errorField: Option[JsValue] = activation.response.result match {\n      case Some(JsObject(fields)) => fields.get(ActivationResponse.ERROR_FIELD)\n      case _                      => None\n    }\n    val withinSeqLimit = newCnt <= maxSequenceCnt\n\n    if (withinSeqLimit && errorField.isEmpty) {\n      // all good with this action invocation\n      success(activation, newCnt)\n    } else {\n      val nextActivation = if (!withinSeqLimit) {\n        // no error in the activation but the dynamic count of actions exceeds the threshold\n        // this is here as defensive code; the activation should not occur if its takes the\n        // count above its limit\n        val newResponse = ActivationResponse.applicationError(sequenceIsTooLong)\n        activation.copy(response = newResponse)\n      } else {\n        assert(errorField.isDefined)\n        activation\n      }\n\n      // there is an error field in the activation response. here, we treat this like success,\n      // in the sense of tallying up the accounting fields, but terminate the sequence early\n      success(nextActivation, newCnt, shortcircuit = true)\n    }\n  }\n}\n\n/**\n *  Three constructors for SequenceAccounting:\n *     - one for successful invocation of an action in the sequence,\n *     - one for failed invocation, and\n *     - one to initialize things\n */\nprotected[actions] object SequenceAccounting {\n\n  def maxMemory(prevMemoryLimit: Option[Int], newMemoryLimit: Option[Int]): Option[Int] = {\n    (prevMemoryLimit ++ newMemoryLimit).reduceOption(Math.max)\n  }\n\n  // constructor for successful invocations, or error'ing ones (where shortcircuit = true)\n  def apply(prev: SequenceAccounting,\n            newCnt: Int,\n            incrDuration: Option[Long],\n            newResponse: ActivationResponse,\n            newActivationId: ActivationId,\n            newMemoryLimit: Option[Int],\n            shortcircuit: Boolean): SequenceAccounting = {\n\n    // compute the new max memory\n    val newMaxMemory = maxMemory(prev.maxMemory, newMemoryLimit)\n\n    // append log entry\n    prev.logs += newActivationId\n\n    SequenceAccounting(\n      atomicActionCnt = newCnt,\n      previousResponse = new AtomicReference(newResponse),\n      logs = prev.logs,\n      duration = incrDuration map { prev.duration + _ } getOrElse { prev.duration },\n      maxMemory = newMaxMemory,\n      shortcircuit = shortcircuit)\n  }\n\n  // constructor for initial payload\n  def apply(atomicActionCnt: Int, initialPayload: ActivationResponse): SequenceAccounting = {\n    SequenceAccounting(atomicActionCnt, new AtomicReference(initialPayload), mutable.Buffer.empty)\n  }\n}\n\nprotected[actions] case class FailedSequenceActivation(accounting: SequenceAccounting) extends Throwable\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/ActionCollection.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.Identity\nimport org.apache.openwhisk.core.entity.types.EntityStore\n\nclass ActionCollection(entityStore: EntityStore) extends Collection(Collection.ACTIONS) {\n\n  protected override val allowedEntityRights = Privilege.ALL\n\n  /**\n   * Computes implicit rights on an action (sequence, in package, or primitive).\n   */\n  protected[core] override def implicitRights(\n    user: Identity,\n    namespaces: Set[String],\n    right: Privilege,\n    resource: Resource)(implicit ep: EntitlementProvider, ec: ExecutionContext, transid: TransactionId) = {\n    val isOwner = namespaces.contains(resource.namespace.root.asString)\n    resource.entity map { name =>\n      right match {\n        // if action is in a package, check that the user is entitled to package [binding]\n        case (Privilege.READ | Privilege.ACTIVATE) if !resource.namespace.defaultPackage =>\n          val packageNamespace = resource.namespace.root.toPath\n          val packageName = Some(resource.namespace.last.name)\n          val packageResource = Resource(packageNamespace, Collection(Collection.PACKAGES), packageName)\n          ep.check(user, Privilege.READ, packageResource) map { _ =>\n            true\n          }\n        case _ => Future.successful(isOwner && allowedEntityRights.contains(right))\n      }\n    } getOrElse {\n      // only a READ on the action collection is permitted if this is the owner of the collection\n      Future.successful(isOwner && right == Privilege.READ)\n    }\n  }\n\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/ActivationThrottler.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.Identity\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancer\nimport org.apache.openwhisk.http.Messages\n\nimport scala.concurrent.{ExecutionContext, Future}\n\n/**\n * Determine whether the namespace currently invoking a new action should be allowed to do so.\n *\n * @param loadBalancer contains active quotas\n * @param concurrencyLimit a calculated limit relative to the user using the system\n */\nclass ActivationThrottler(loadBalancer: LoadBalancer, concurrencyLimit: Identity => Int)(\n  implicit logging: Logging,\n  executionContext: ExecutionContext) {\n\n  /**\n   * Checks whether the operation should be allowed to proceed.\n   */\n  def check(user: Identity)(implicit tid: TransactionId): Future[RateLimit] = {\n    loadBalancer.activeActivationsFor(user.namespace.uuid).map { concurrentActivations =>\n      val currentLimit = concurrencyLimit(user)\n      logging.debug(\n        this,\n        s\"namespace = ${user.namespace.uuid.asString}, concurrent activations = $concurrentActivations, below limit = $currentLimit\")\n      ConcurrentRateLimit(concurrentActivations, currentLimit)\n    }\n  }\n}\n\nsealed trait RateLimit {\n  def ok: Boolean\n  def errorMsg: String\n  def limitName: String\n}\n\ncase class ConcurrentRateLimit(count: Int, allowed: Int) extends RateLimit {\n  val ok: Boolean = count < allowed // must have slack for the current activation request\n  override def errorMsg: String = Messages.tooManyConcurrentRequests(count, allowed)\n  val limitName: String = \"ConcurrentRateLimit\"\n}\n\ncase class TimedRateLimit(count: Int, allowed: Int) extends RateLimit {\n  val ok: Boolean = count <= allowed // the count is already updated to account for the current request\n  override def errorMsg: String = Messages.tooManyRequests(count, allowed)\n  val limitName: String = \"TimedRateLimit\"\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/Collection.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport org.apache.openwhisk.core.entitlement.Privilege._\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport org.apache.pekko.http.scaladsl.model.HttpMethod\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.DELETE\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.GET\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.POST\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.PUT\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.Identity\nimport org.apache.openwhisk.core.entity.WhiskAction\nimport org.apache.openwhisk.core.entity.WhiskActivation\nimport org.apache.openwhisk.core.entity.WhiskPackage\nimport org.apache.openwhisk.core.entity.WhiskRule\nimport org.apache.openwhisk.core.entity.WhiskTrigger\nimport org.apache.openwhisk.core.entity.types.EntityStore\nimport pureconfig._\nimport pureconfig.generic.auto._\n\n/**\n * A collection encapsulates the name of a collection and implicit rights when subject\n * lacks explicit rights on a resource in the collection.\n *\n * @param path the name of the collection (the resource path in URI and the view name in the datastore)\n * @param defaultListLimit the default limit on number of entities returned from a collection on a list operation\n * @param defaultListSkip the default skip on number of entities returned from a collection on a list operation\n */\nprotected[core] case class Collection protected (val path: String,\n                                                 val defaultListLimit: Int = Collection.DEFAULT_LIST_LIMIT,\n                                                 val defaultListSkip: Int = Collection.DEFAULT_SKIP_LIMIT) {\n  override def toString = path\n\n  /** Determines the right to request for the resources and context. */\n  protected[core] def determineRight(op: HttpMethod, resource: Option[String])(\n    implicit transid: TransactionId): Privilege = {\n    op match {\n      case GET => Privilege.READ\n      case PUT =>\n        resource map { _ =>\n          Privilege.PUT\n        } getOrElse Privilege.REJECT\n      case POST =>\n        resource map { _ =>\n          activateAllowed\n        } getOrElse Privilege.REJECT\n      case DELETE =>\n        resource map { _ =>\n          Privilege.DELETE\n        } getOrElse Privilege.REJECT\n      case _ => Privilege.REJECT\n    }\n  }\n\n  protected val allowedCollectionRights: Set[Privilege] = Set(Privilege.READ)\n  protected val allowedEntityRights: Set[Privilege] = {\n    Set(Privilege.READ, Privilege.PUT, Privilege.ACTIVATE, Privilege.DELETE)\n  }\n\n  private lazy val activateAllowed = {\n    if (allowedEntityRights.contains(Privilege.ACTIVATE)) {\n      Privilege.ACTIVATE\n    } else Privilege.REJECT\n  }\n\n  /**\n   * Infers implicit rights on a resource in the collection before checking explicit\n   * rights in the entitlement matrix. The subject has CRUD and activate rights\n   * to any resource in in any of their namespaces as long as the right (the implied operation)\n   * is permitted on the resource.\n   */\n  protected[core] def implicitRights(user: Identity, namespaces: Set[String], right: Privilege, resource: Resource)(\n    implicit ep: EntitlementProvider,\n    ec: ExecutionContext,\n    transid: TransactionId): Future[Boolean] = Future.successful {\n    // if the resource root namespace is in any of the allowed namespaces\n    // then this is an owner of the resource\n    val self = namespaces.contains(resource.namespace.root.asString)\n\n    resource.entity map { _ =>\n      self && allowedEntityRights.contains(right)\n    } getOrElse {\n      self && allowedCollectionRights.contains(right)\n    }\n  }\n}\n\n/** An enumeration of known collections. */\nprotected[core] object Collection {\n\n  private case class QueryLimit(maxListLimit: Int, defaultListLimit: Int)\n  private val queryLimit = loadConfigOrThrow[QueryLimit](ConfigKeys.query)\n\n  /** Number of records allowed per query. */\n  protected[core] val DEFAULT_LIST_LIMIT = queryLimit.defaultListLimit\n  protected[core] val MAX_LIST_LIMIT = queryLimit.maxListLimit\n  protected[core] val DEFAULT_SKIP_LIMIT = 0\n\n  protected[core] val ACTIONS = WhiskAction.collectionName\n  protected[core] val TRIGGERS = WhiskTrigger.collectionName\n  protected[core] val RULES = WhiskRule.collectionName\n  protected[core] val PACKAGES = WhiskPackage.collectionName\n  protected[core] val ACTIVATIONS = WhiskActivation.collectionName\n  protected[core] val NAMESPACES = \"namespaces\"\n  protected[core] val LIMITS = \"limits\"\n\n  private val collections = scala.collection.mutable.Map[String, Collection]()\n  private def register(c: Collection) = collections += c.path -> c\n\n  protected[core] def apply(name: String) = collections.get(name).get\n\n  protected[core] def initialize(entityStore: EntityStore)(implicit logging: Logging) = {\n    register(new ActionCollection(entityStore))\n    register(new Collection(TRIGGERS))\n    register(new Collection(RULES))\n    register(new PackageCollection(entityStore))\n\n    register(new Collection(ACTIVATIONS) {\n      protected[core] override def determineRight(op: HttpMethod,\n                                                  resource: Option[String])(implicit transid: TransactionId) = {\n        if (op == GET) Privilege.READ else Privilege.REJECT\n      }\n\n      protected override val allowedEntityRights: Set[Privilege] = Set(Privilege.READ)\n    })\n\n    register(new Collection(NAMESPACES) {\n      protected[core] override def determineRight(op: HttpMethod,\n                                                  resource: Option[String])(implicit transid: TransactionId) = {\n        resource map { _ =>\n          Privilege.REJECT\n        } getOrElse {\n          if (op == GET) Privilege.READ else Privilege.REJECT\n        }\n      }\n\n      protected override val allowedEntityRights: Set[Privilege] = Set(Privilege.READ)\n    })\n\n    register(new Collection(LIMITS) {\n      protected[core] override def determineRight(op: HttpMethod,\n                                                  resource: Option[String])(implicit transid: TransactionId) = {\n        if (op == GET) Privilege.READ else Privilege.REJECT\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/Entitlement.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport scala.collection.concurrent.TrieMap\nimport scala.collection.immutable.Set\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.Failure\nimport scala.util.Success\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Forbidden\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.TooManyRequests\nimport org.apache.openwhisk.core.entitlement.Privilege.ACTIVATE\nimport org.apache.openwhisk.core.entitlement.Privilege.REJECT\nimport org.apache.openwhisk.common.{Logging, TransactionId, UserEvents}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.{EventMessage, Metric}\nimport org.apache.openwhisk.core.controller.RejectRequest\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.loadBalancer.{LoadBalancer, ShardingContainerPoolBalancer}\nimport org.apache.openwhisk.http.ErrorResponse\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.core.connector.MessagingProvider\nimport org.apache.openwhisk.spi.SpiLoader\nimport org.apache.openwhisk.spi.Spi\n\nobject types {\n  type Entitlements = TrieMap[(Subject, String), Set[Privilege]]\n}\n\n/**\n * Resource is a type that encapsulates details relevant to identify a specific resource.\n * It may be an entire collection, or an element in a collection.\n *\n * @param namespace  the namespace the resource resides in\n * @param collection the collection (e.g., actions, triggers) identifying a resource\n * @param entity     an optional entity name that identifies a specific item in the collection\n * @param env        an optional environment to bind to the resource during an activation\n */\nprotected[core] case class Resource(namespace: EntityPath,\n                                    collection: Collection,\n                                    entity: Option[String],\n                                    env: Option[Parameters] = None) {\n  def parent: String = collection.path + EntityPath.PATHSEP + namespace\n\n  def id: String = parent + entity.map(EntityPath.PATHSEP + _).getOrElse(\"\")\n\n  def fqname: String = namespace.asString + entity.map(EntityPath.PATHSEP + _).getOrElse(\"\")\n\n  override def toString: String = id\n}\n\ntrait EntitlementSpiProvider extends Spi {\n  def instance(config: WhiskConfig, loadBalancer: LoadBalancer, instance: ControllerInstanceId)(\n    implicit actorSystem: ActorSystem,\n    logging: Logging): EntitlementProvider\n}\n\nprotected[core] object EntitlementProvider {\n\n  val requiredProperties = Map(\n    WhiskConfig.actionInvokePerMinuteLimit -> null,\n    WhiskConfig.actionInvokeConcurrentLimit -> null,\n    WhiskConfig.triggerFirePerMinuteLimit -> null)\n}\n\n/**\n * A trait that implements entitlements to resources. It performs checks for CRUD and Acivation requests.\n * This is where enforcement of activation quotas takes place, in additional to basic authorization.\n */\nprotected[core] abstract class EntitlementProvider(\n  config: WhiskConfig,\n  loadBalancer: LoadBalancer,\n  controllerInstance: ControllerInstanceId)(implicit actorSystem: ActorSystem, logging: Logging) {\n\n  private implicit val executionContext: ExecutionContext = actorSystem.dispatcher\n\n  /**\n   * Allows 20% of additional requests on top of the limit to mitigate possible unfair round-robin loadbalancing between\n   * controllers\n   */\n  private def overcommit(clusterSize: Int) = if (clusterSize > 1) 1.2 else 1\n\n  private def dilateLimit(limit: Int): Int = Math.ceil(limit.toDouble * overcommit(loadBalancer.clusterSize)).toInt\n\n  /**\n   * Calculates a possibly dilated limit relative to the current user.\n   *\n   * @param defaultLimit the default limit across the whole system\n   * @param user         the user to apply that limit to\n   * @return a calculated limit\n   */\n  private def calculateLimit(defaultLimit: Int, overrideLimit: Identity => Option[Int])(user: Identity): Int = {\n    val absoluteLimit = overrideLimit(user).getOrElse(defaultLimit)\n    dilateLimit(absoluteLimit)\n  }\n\n  /**\n   * Calculates a limit which applies only to this instance individually.\n   *\n   * The state needed to correctly check this limit is not shared between all instances, which want to check that\n   * limit, so it needs to be divided between the parties who want to perform that check.\n   *\n   * @param defaultLimit the default limit across the whole system\n   * @param user         the user to apply that limit to\n   * @return a calculated limit\n   */\n  private def calculateIndividualLimit(defaultLimit: Int, overrideLimit: Identity => Option[Int])(\n    user: Identity): Int = {\n    val limit = calculateLimit(defaultLimit, overrideLimit)(user)\n    if (limit == 0) {\n      0\n    } else {\n      // Edge case: Iff the divided limit is < 1 no loadbalancer would allow an action to be executed, thus we range\n      // bound to at least 1\n      (limit / loadBalancer.clusterSize).max(1)\n    }\n  }\n\n  private val invokeRateThrottler =\n    new RateThrottler(\n      \"actions per minute\",\n      calculateIndividualLimit(config.actionInvokePerMinuteLimit.toInt, _.limits.invocationsPerMinute))\n  private val triggerRateThrottler =\n    new RateThrottler(\n      \"triggers per minute\",\n      calculateIndividualLimit(config.triggerFirePerMinuteLimit.toInt, _.limits.firesPerMinute))\n\n  private val activationThrottleCalculator = loadBalancer match {\n    // This loadbalancer applies sharding and does not share any state\n    case _: ShardingContainerPoolBalancer => calculateIndividualLimit _\n    // Activation relevant data is shared by all other loadbalancers\n    case _ => calculateLimit _\n  }\n  private val concurrentInvokeThrottler =\n    new ActivationThrottler(\n      loadBalancer,\n      activationThrottleCalculator(config.actionInvokeConcurrentLimit.toInt, _.limits.concurrentInvocations))\n\n  private val messagingProvider = SpiLoader.get[MessagingProvider]\n  protected val eventProducer = messagingProvider.getProducer(this.config)\n\n  /**\n   * Grants a subject the right to access a resources.\n   *\n   * @param user     the subject to grant right to\n   * @param right    the privilege to grant the subject\n   * @param resource the resource to grant the subject access to\n   * @return a promise that completes with true iff the subject is granted the right to access the requested resource\n   */\n  protected[core] def grant(user: Identity, right: Privilege, resource: Resource)(\n    implicit transid: TransactionId): Future[Boolean]\n\n  /**\n   * Revokes a subject the right to access a resources.\n   *\n   * @param user     the subject to revoke right to\n   * @param right    the privilege to revoke the subject\n   * @param resource the resource to revoke the subject access to\n   * @return a promise that completes with true iff the subject is revoked the right to access the requested resource\n   */\n  protected[core] def revoke(user: Identity, right: Privilege, resource: Resource)(\n    implicit transid: TransactionId): Future[Boolean]\n\n  /**\n   * Checks if a subject is entitled to a resource because it was granted the right explicitly.\n   *\n   * @param user     the subject to check rights for\n   * @param right    the privilege the subject is requesting\n   * @param resource the resource the subject requests access to\n   * @return a promise that completes with true iff the subject is permitted to access the request resource\n   */\n  protected def entitled(user: Identity, right: Privilege, resource: Resource)(\n    implicit transid: TransactionId): Future[Boolean]\n\n  /**\n   * Checks action activation rate throttles for an identity.\n   *\n   * @param user the identity to check rate throttles for\n   * @return a promise that completes with success iff the user is within their activation quota\n   */\n  protected[core] def checkThrottles(user: Identity)(implicit transid: TransactionId): Future[Unit] = {\n\n    logging.debug(this, s\"checking user '${user.subject}' has not exceeded activation quota\")\n    checkThrottleOverload(Future.successful(invokeRateThrottler.check(user)), user)\n      .flatMap(_ => checkThrottleOverload(concurrentInvokeThrottler.check(user), user))\n  }\n\n  /**\n   * Checks action activation rate throttles for an identity.\n   *\n   * @param user      the identity to check rate throttles for\n   * @param right     the privilege the subject is requesting\n   * @param resources the set of resource the subject requests access to\n   * @return a promise that completes with success iff the user is within their activation quota\n   */\n  protected[core] def checkThrottles(user: Identity, right: Privilege, resources: Set[Resource])(\n    implicit transid: TransactionId): Future[Unit] = {\n    checkUserThrottle(user, right, resources).flatMap(_ => checkConcurrentUserThrottle(user, right, resources))\n  }\n\n  private val kindRestrictor = {\n    import pureconfig._\n    import pureconfig.generic.auto._\n    import org.apache.openwhisk.core.ConfigKeys\n    case class AllowedKinds(whitelist: Option[Set[String]] = None)\n    val allowedKinds = loadConfigOrThrow[AllowedKinds](ConfigKeys.runtimes)\n    KindRestrictor(allowedKinds.whitelist)\n  }\n\n  /**\n   * Checks if an action kind is allowed for a given subject.\n   *\n   * @param user the identity to check for restrictions\n   * @param exec the action executable details\n   * @return a promise that completes with success iff the user's action kind is allowed\n   */\n  protected[core] def check(user: Identity, exec: Option[Exec])(implicit transid: TransactionId): Future[Unit] = {\n    exec\n      .map {\n        case e =>\n          if (kindRestrictor.check(user, e.kind)) {\n            Future.successful(())\n          } else {\n            Future.failed(\n              RejectRequest(Forbidden, Some(ErrorResponse(Messages.notAuthorizedtoActionKind(e.kind), transid))))\n          }\n      }\n      .getOrElse(Future.successful(()))\n  }\n\n  /**\n   * Checks if a subject has the right to access a specific resource. The entitlement may be implicit,\n   * that is, inferred based on namespaces that a subject belongs to and the namespace of the\n   * resource for example, or explicit. The implicit check is computed here. The explicit check\n   * is delegated to the service implementing this interface.\n   *\n   * NOTE: do not use this method to check a package binding because this method does not allow\n   * for a continuation to check that both the binding and the references package are both either\n   * implicitly or explicitly granted. Instead, resolve the package binding first and use the alternate\n   * method which authorizes a set of resources.\n   *\n   * @param user     the subject to check rights for\n   * @param right    the privilege the subject is requesting (applies to the entire set of resources)\n   * @param resource the resource the subject requests access to\n   * @return a promise that completes with success iff the subject is permitted to access the requested resource\n   */\n  protected[core] def check(user: Identity, right: Privilege, resource: Resource)(\n    implicit transid: TransactionId): Future[Unit] = check(user, right, Set(resource))\n\n  /**\n   * Constructs a RejectRequest containing the forbidden resources.\n   *\n   * @param resources resources forbidden to access\n   * @return a RejectRequest with the appropriate message\n   */\n  private def unauthorizedOn(resources: Set[Resource])(implicit transid: TransactionId) = {\n    RejectRequest(\n      Forbidden,\n      Some(\n        ErrorResponse(\n          Messages.notAuthorizedtoAccessResource(resources.map(_.fqname).toSeq.sorted.toSet.mkString(\", \")),\n          transid)))\n  }\n\n  /**\n   * Checks if a subject has the right to access a set of resources. The entitlement may be implicit,\n   * that is, inferred based on namespaces that a subject belongs to and the namespace of the\n   * resource for example, or explicit. The implicit check is computed here. The explicit check\n   * is delegated to the service implementing this interface.\n   *\n   * @param user       the subject identity to check rights for\n   * @param right      the privilege the subject is requesting (applies to the entire set of resources)\n   * @param resources  the set of resources the subject requests access to\n   * @param noThrottle ignore throttle limits\n   * @return a promise that completes with success iff the subject is permitted to access all of the requested resources\n   */\n  protected[core] def check(user: Identity, right: Privilege, resources: Set[Resource], noThrottle: Boolean = false)(\n    implicit transid: TransactionId): Future[Unit] = {\n    val subject = user.subject\n\n    val entitlementCheck: Future[Unit] = if (user.rights.contains(right)) {\n      if (resources.nonEmpty) {\n        logging.debug(this, s\"checking user '$subject' has privilege '$right' for '${resources.mkString(\", \")}'\")\n        val throttleCheck = if (noThrottle) Future.successful(()) else checkThrottles(user, right, resources)\n        throttleCheck\n          .flatMap(_ => checkPrivilege(user, right, resources))\n          .flatMap(checkedResources => {\n            val failedResources = checkedResources.filterNot(_._2)\n            if (failedResources.isEmpty) Future.successful(())\n            else Future.failed(unauthorizedOn(failedResources.map(_._1)))\n          })\n      } else Future.successful(())\n    } else if (right != REJECT) {\n      logging.debug(\n        this,\n        s\"supplied authkey for user '$subject' does not have privilege '$right' for '${resources.mkString(\", \")}'\")\n      Future.failed(unauthorizedOn(resources))\n    } else {\n      Future.failed(unauthorizedOn(resources))\n    }\n\n    entitlementCheck andThen {\n      case Success(rs) =>\n        logging.debug(this, \"authorized\")\n      case Failure(r: RejectRequest) =>\n        logging.debug(this, s\"not authorized: $r\")\n      case Failure(t) =>\n        logging.error(this, s\"failed while checking entitlement: ${t.getMessage}\")\n    }\n  }\n\n  /**\n   * NOTE: explicit grants do not work with package bindings because this method does not allow\n   * for a continuation to check that both the binding and the references package are both either\n   * implicitly or explicitly granted. Instead, the given resource set should include both the binding\n   * and the referenced package.\n   */\n  protected def checkPrivilege(user: Identity, right: Privilege, resources: Set[Resource])(\n    implicit transid: TransactionId): Future[Set[(Resource, Boolean)]] = {\n    // check the default namespace first, bypassing additional checks if permitted\n    val defaultNamespaces = Set(user.namespace.name.asString)\n    implicit val es: EntitlementProvider = this\n\n    Future.sequence {\n      resources.map { resource =>\n        resource.collection.implicitRights(user, defaultNamespaces, right, resource) flatMap {\n          case true => Future.successful(resource -> true)\n          case false =>\n            logging.debug(this, \"checking explicit grants\")\n            entitled(user, right, resource).flatMap(b => Future.successful(resource -> b))\n        }\n      }\n    }\n  }\n\n  /**\n   * Limits activations if subject exceeds their own limits.\n   * If the requested right is an activation, the set of resources must contain an activation of an action or filter to be throttled.\n   * While it is possible for the set of resources to contain more than one action or trigger, the plurality is ignored and treated\n   * as one activation since these should originate from a single macro resources (e.g., a sequence).\n   *\n   * @param user      the subject identity to check rights for\n   * @param right     the privilege, if ACTIVATE then check quota else return None\n   * @param resources the set of resources must contain at least one resource that can be activated else return None\n   * @return future completing successfully if user is below limits else failing with a rejection\n   */\n  protected[core] def checkUserThrottle(user: Identity, right: Privilege, resources: Set[Resource])(\n    implicit transid: TransactionId): Future[Unit] = {\n    if (right == ACTIVATE) {\n      if (resources.exists(_.collection.path == Collection.ACTIONS)) {\n        checkThrottleOverload(Future.successful(invokeRateThrottler.check(user)), user)\n      } else if (resources.exists(_.collection.path == Collection.TRIGGERS)) {\n        checkThrottleOverload(Future.successful(triggerRateThrottler.check(user)), user)\n      } else Future.successful(())\n    } else Future.successful(())\n  }\n\n  /**\n   * Limits activations if subject exceeds limit of concurrent invocations.\n   * If the requested right is an activation, the set of resources must contain an activation of an action to be throttled.\n   * While it is possible for the set of resources to contain more than one action, the plurality is ignored and treated\n   * as one activation since these should originate from a single macro resources (e.g., a sequence).\n   *\n   * @param user      the subject identity to check rights for\n   * @param right     the privilege, if ACTIVATE then check quota else return None\n   * @param resources the set of resources must contain at least one resource that can be activated else return None\n   * @return future completing successfully if user is below limits else failing with a rejection\n   */\n  private def checkConcurrentUserThrottle(user: Identity, right: Privilege, resources: Set[Resource])(\n    implicit transid: TransactionId): Future[Unit] = {\n    if (right == ACTIVATE && resources.exists(_.collection.path == Collection.ACTIONS)) {\n      checkThrottleOverload(concurrentInvokeThrottler.check(user), user)\n    } else Future.successful(())\n  }\n\n  private def checkThrottleOverload(throttle: Future[RateLimit], user: Identity)(\n    implicit transid: TransactionId): Future[Unit] = {\n    throttle.flatMap { limit =>\n      val userId = user.namespace.uuid\n      if (limit.ok) {\n        limit match {\n          case c: ConcurrentRateLimit => {\n            val metric =\n              Metric(\"ConcurrentInvocations\", c.count + 1)\n            UserEvents.send(\n              eventProducer,\n              EventMessage(\n                s\"controller${controllerInstance.asString}\",\n                metric,\n                user.subject,\n                user.namespace.name.toString,\n                userId,\n                metric.typeName))\n          }\n          case _ => // ignore\n        }\n        Future.successful(())\n      } else {\n        logging.info(this, s\"'${user.namespace.name}' has exceeded its throttle limit, ${limit.errorMsg}\")\n        val metric = Metric(limit.limitName, 1)\n        UserEvents.send(\n          eventProducer,\n          EventMessage(\n            s\"controller${controllerInstance.asString}\",\n            metric,\n            user.subject,\n            user.namespace.name.toString,\n            userId,\n            metric.typeName))\n        Future.failed(RejectRequest(TooManyRequests, limit.errorMsg))\n      }\n    }\n  }\n}\n\n/**\n * A trait to consolidate gathering of referenced entities for various types.\n * Current entities that refer to others: action sequences, rules, and package bindings.\n */\ntrait ReferencedEntities {\n\n  /**\n   * Gathers referenced resources for types knows to refer to others.\n   * This is usually done on a PUT request, hence the types are not one of the\n   * canonical datastore types. Hence this method accepts Any reference but is\n   * only defined for WhiskPackagePut, WhiskRulePut, and SequenceExec.\n   *\n   * It is plausible to lift these disambiguation below to a new trait which is\n   * implemented by these types - however this will require exposing the Resource\n   * type outside of the controller which is not yet desirable (although this could\n   * cause further consolidation of the WhiskEntity and Resource types).\n   *\n   * @return Set of Resource instances if there are referenced entities.\n   */\n  def referencedEntities(reference: Any): Set[Resource] = {\n    reference match {\n      case WhiskPackagePut(Some(binding), _, _, _, _) =>\n        Set(Resource(binding.namespace.toPath, Collection(Collection.PACKAGES), Some(binding.name.asString)))\n      case r: WhiskRulePut =>\n        val triggerResource = r.trigger.map { t =>\n          Resource(t.path, Collection(Collection.TRIGGERS), Some(t.name.asString))\n        }\n        val actionResource = r.action map { a =>\n          Resource(a.path, Collection(Collection.ACTIONS), Some(a.name.asString))\n        }\n        Set(triggerResource, actionResource).flatten\n      case e: SequenceExec =>\n        e.components.map { c =>\n          Resource(c.path, Collection(Collection.ACTIONS), Some(c.name.asString))\n        }.toSet\n      case e: SequenceExecMetaData =>\n        e.components.map { c =>\n          Resource(c.path, Collection(Collection.ACTIONS), Some(c.name.asString))\n        }.toSet\n      case _ => Set.empty\n    }\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/FPCEntitlement.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport scala.concurrent.Future\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.TooManyRequests\nimport org.apache.openwhisk.common.{Logging, TransactionId, UserEvents}\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.core.connector.{EventMessage, Metric}\nimport org.apache.openwhisk.core.controller.RejectRequest\nimport org.apache.openwhisk.core.entitlement.Privilege.ACTIVATE\nimport org.apache.openwhisk.core.entity.{ControllerInstanceId, Identity}\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancer\nimport pureconfig.loadConfigOrThrow\nimport pureconfig.generic.auto._\n\ncase class FPCEntitlementConfig(usePerMinThrottles: Boolean)\n\nprotected[core] class FPCEntitlementProvider(\n  private val config: WhiskConfig,\n  private val loadBalancer: LoadBalancer,\n  private val controllerInstance: ControllerInstanceId)(implicit actorSystem: ActorSystem, logging: Logging)\n    extends LocalEntitlementProvider(config, loadBalancer, controllerInstance) {\n\n  private implicit val executionContext = actorSystem.dispatcher\n\n  private val fpcEntitlementConfig: FPCEntitlementConfig =\n    loadConfigOrThrow[FPCEntitlementConfig](ConfigKeys.fpcLoadBalancer)\n\n  override protected[core] def checkThrottles(user: Identity, right: Privilege, resources: Set[Resource])(\n    implicit transid: TransactionId): Future[Unit] = {\n    if (fpcEntitlementConfig.usePerMinThrottles) {\n      checkUserThrottle(user, right, resources).flatMap(_ => checkFPCConcurrentThrottle(user, right, resources))\n    } else {\n      checkFPCConcurrentThrottle(user, right, resources)\n    }\n  }\n\n  private def checkFPCConcurrentThrottle(user: Identity, right: Privilege, resources: Set[Resource])(\n    implicit transid: TransactionId): Future[Unit] = {\n    if (right == ACTIVATE) {\n      val checks = resources.filter(_.collection.path == Collection.ACTIONS).map { res =>\n        loadBalancer.checkThrottle(user.namespace.name.toPath, res.fqname)\n      }\n      if (checks.contains(true)) {\n        val metric = Metric(\"ConcurrentRateLimit\", 1)\n        UserEvents.send(\n          eventProducer,\n          EventMessage(\n            s\"controller${controllerInstance.asString}\",\n            metric,\n            user.subject,\n            user.namespace.name.toString,\n            user.namespace.uuid,\n            metric.typeName))\n        Future.failed(RejectRequest(TooManyRequests, \"Too many requests\"))\n      } else Future.successful(())\n    } else Future.successful(())\n  }\n}\n\nprivate object FPCEntitlementProvider extends EntitlementSpiProvider {\n\n  override def instance(config: WhiskConfig, loadBalancer: LoadBalancer, instance: ControllerInstanceId)(\n    implicit actorSystem: ActorSystem,\n    logging: Logging) =\n    new FPCEntitlementProvider(config: WhiskConfig, loadBalancer: LoadBalancer, instance)\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/KindRestrictor.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.entity.Identity\n\n/**\n * The runtimes manifest specifies all runtimes enabled for the deployment.\n * Not all runtimes are available for all subject however.\n *\n * A subject is entitled to a runtime (kind) if:\n * 1. they are explicitly granted rights to it in their identity record, or\n * 2. the runtime kind is whitelisted\n *\n * If a white list is not specified (i.e., whitelist == None), then all runtimes are allowed.\n * In other words, no whitelist is the same as setting the white list to all allowed runtimes.\n *\n * @param whitelist set of default allowed kinds when not explicitly available via namespace limits.\n */\ncase class KindRestrictor(whitelist: Option[Set[String]] = None)(implicit logging: Logging) {\n\n  logging.info(\n    this, {\n      whitelist\n        .map {\n          case list if list.nonEmpty => s\"white-listed kinds: ${list.mkString(\", \")}\"\n          case _                     => \"no kinds are allowed, the white-list is empty\"\n        }\n        .getOrElse(\"all kinds are allowed, the white-list is not specified\")\n    })(TransactionId.controller)\n\n  def check(user: Identity, kind: String): Boolean = {\n    val kindList = user.limits.allowedKinds.getOrElse(Set.empty).union(whitelist.getOrElse(Set.empty))\n    kindList.isEmpty || kindList.contains(kind)\n  }\n\n}\n\nobject KindRestrictor {\n  def apply(whitelist: Set[String])(implicit logging: Logging) = new KindRestrictor(Some(whitelist))\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/LocalEntitlement.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.Future\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.entity.{ControllerInstanceId, Identity, Subject}\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancer\n\nprotected[core] class LocalEntitlementProvider(\n  private val config: WhiskConfig,\n  private val loadBalancer: LoadBalancer,\n  private val controllerInstance: ControllerInstanceId)(implicit actorSystem: ActorSystem, logging: Logging)\n    extends EntitlementProvider(config, loadBalancer, controllerInstance) {\n\n  private implicit val executionContext = actorSystem.dispatcher\n\n  private val matrix = LocalEntitlementProvider.matrix\n\n  /** Grants subject right to resource by adding them to the entitlement matrix. */\n  protected[core] override def grant(user: Identity, right: Privilege, resource: Resource)(\n    implicit transid: TransactionId) = Future {\n    val subject = user.subject\n    synchronized {\n      val key = (subject, resource.id)\n      matrix.put(key, matrix.get(key) map { _ + right } getOrElse Set(right))\n      logging.debug(this, s\"granted user '$subject' privilege '$right' for '$resource'\")\n      true\n    }\n  }\n\n  /** Revokes subject right to resource by removing them from the entitlement matrix. */\n  protected[core] override def revoke(user: Identity, right: Privilege, resource: Resource)(\n    implicit transid: TransactionId) = Future {\n    val subject = user.subject\n    synchronized {\n      val key = (subject, resource.id)\n      val newrights = matrix.get(key) map { _ - right } map { matrix.put(key, _) }\n      logging.debug(this, s\"revoked user '$subject' privilege '$right' for '$resource'\")\n      true\n    }\n  }\n\n  /** Checks if subject has explicit grant for a resource. */\n  protected override def entitled(user: Identity, right: Privilege, resource: Resource)(\n    implicit transid: TransactionId) = Future.successful {\n    val subject = user.subject\n    lazy val one = matrix.get((subject, resource.id)) map { _ contains right } getOrElse false\n    lazy val any = matrix.get((subject, resource.parent)) map { _ contains right } getOrElse false\n    one || any\n  }\n}\n\nprivate object LocalEntitlementProvider extends EntitlementSpiProvider {\n\n  /** Poor mans entitlement matrix. Must persist to datastore eventually. */\n  private val matrix = TrieMap[(Subject, String), Set[Privilege]]()\n  override def instance(config: WhiskConfig, loadBalancer: LoadBalancer, instance: ControllerInstanceId)(\n    implicit actorSystem: ActorSystem,\n    logging: Logging) =\n    new LocalEntitlementProvider(config: WhiskConfig, loadBalancer: LoadBalancer, instance)\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/PackageCollection.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\n\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.controller.RejectRequest\nimport org.apache.openwhisk.core.database.DocumentTypeMismatchException\nimport org.apache.openwhisk.core.database.NoDocumentException\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types.EntityStore\nimport org.apache.openwhisk.http.Messages\n\nclass PackageCollection(entityStore: EntityStore)(implicit logging: Logging) extends Collection(Collection.PACKAGES) {\n\n  protected override val allowedEntityRights: Set[Privilege] = {\n    Set(Privilege.READ, Privilege.PUT, Privilege.DELETE)\n  }\n\n  /**\n   * Computes implicit rights on a package/binding.\n   *\n   * Must fetch the resource (a package or binding) to determine if it is in allowed namespaces.\n   * There are two cases:\n   *\n   * 1. the resource is a package: then either it is in allowed namespaces or it is public.\n   * 2. the resource is a binding: then it must be in allowed namespaces and (1) must hold for\n   *    the referenced package.\n   *\n   * A published package makes all its assets public regardless of their shared bit.\n   * All assets that are not in an explicit package are private because the default package is private.\n   */\n  protected[core] override def implicitRights(user: Identity,\n                                              namespaces: Set[String],\n                                              right: Privilege,\n                                              resource: Resource)(implicit ep: EntitlementProvider,\n                                                                  ec: ExecutionContext,\n                                                                  transid: TransactionId): Future[Boolean] = {\n    resource.entity map { pkgname =>\n      val isOwner = namespaces.contains(resource.namespace.root.asString)\n      right match {\n        case Privilege.READ =>\n          // must determine if this is a public or owned package\n          // or, for a binding, that it references a public or owned package\n          val docid = FullyQualifiedEntityName(resource.namespace.root.toPath, EntityName(pkgname)).toDocId\n          checkPackageReadPermission(namespaces, isOwner, docid)\n        case _ => Future.successful(isOwner && allowedEntityRights.contains(right))\n      }\n    } getOrElse {\n      // only a READ on the package collection is permitted;\n      // NOTE: currently, the implementation allows any subject to\n      // list packages in any namespace, and defers the filtering of\n      // public packages to non-owning subjects to the API handlers\n      // for packages\n      Future.successful(right == Privilege.READ)\n    }\n  }\n\n  /**\n   * @param namespaces the set of namespaces the subject is entitled to\n   * @param isOwner indicates if the resource is owned by the subject requesting authorization\n   * @param docid the package (or binding) document id\n   */\n  private def checkPackageReadPermission(namespaces: Set[String], isOwner: Boolean, doc: DocId)(\n    implicit ec: ExecutionContext,\n    transid: TransactionId): Future[Boolean] = {\n\n    val right = Privilege.READ\n\n    WhiskPackage.get(entityStore, doc) flatMap {\n      case wp if wp.binding.isEmpty =>\n        val allowed = wp.publish || isOwner\n        logging.debug(this, s\"entitlement check on package, '$right' allowed?: $allowed\")\n        Future.successful(allowed)\n      case wp =>\n        if (isOwner) {\n          val binding = wp.binding.get\n          val pkgOwner = namespaces.contains(binding.namespace.asString)\n          val pkgDocid = binding.docid\n          logging.debug(this, s\"checking subject has privilege '$right' for bound package '$pkgDocid'\")\n          if (doc == pkgDocid) {\n            logging.error(this, s\"unexpected package binding refers to itself: $doc\")\n            Future.failed(\n              RejectRequest(\n                UnprocessableContent,\n                Messages.packageBindingCircularReference(binding.fullyQualifiedName.toString)))\n          } else {\n            checkPackageReadPermission(namespaces, pkgOwner, pkgDocid)\n          }\n        } else {\n          logging.debug(this, s\"entitlement check on package binding, '$right' allowed?: false\")\n          Future.successful(false)\n        }\n    } recoverWith {\n      case t: NoDocumentException =>\n        logging.debug(this, s\"the package does not exist (owner? $isOwner)\")\n        // if owner, reject with not found, otherwise fail the future to reject with\n        // unauthorized (this prevents information leaks about packages in other namespaces)\n        if (isOwner) {\n          Future.failed(RejectRequest(NotFound))\n        } else {\n          Future.successful(false)\n        }\n      case t: DocumentTypeMismatchException =>\n        logging.debug(this, s\"the requested binding is not a package (owner? $isOwner)\")\n        // if owner, reject with not found, otherwise fail the future to reject with\n        // unauthorized (this prevents information leaks about packages in other namespaces)\n        if (isOwner) {\n          Future.failed(RejectRequest(Conflict, Messages.conformanceMessage))\n        } else {\n          Future.successful(false)\n        }\n      case t: RejectRequest =>\n        logging.error(this, s\"entitlement check on package failed: $t\")\n        Future.failed(t)\n      case t =>\n        logging.error(this, s\"entitlement check on package failed: ${t.getMessage}\")\n        if (isOwner) {\n          Future.failed(RejectRequest(InternalServerError, Messages.corruptedEntity))\n        } else {\n          Future.successful(false)\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/RateThrottler.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entitlement\n\nimport scala.collection.concurrent.TrieMap\n\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.Identity\nimport org.apache.openwhisk.core.entity.UUID\nimport java.util.concurrent.atomic.AtomicInteger\n\n/**\n * A class tracking the rate of invocation (or any operation) by subject (any key really).\n *\n * For now, we throttle only at a 1-minute granularity.\n */\nclass RateThrottler(description: String, maxPerMinute: Identity => Int)(implicit logging: Logging) {\n\n  /**\n   * Maintains map of subject namespace to operations rates.\n   */\n  private val rateMap = new TrieMap[UUID, RateInfo]\n\n  /**\n   * Checks whether the operation should be allowed to proceed.\n   * Every `check` operation charges the subject namespace for one operation.\n   *\n   * @param user the identity to check\n   * @return true iff subject namespace is below allowed limit\n   */\n  def check(user: Identity)(implicit transid: TransactionId): RateLimit = {\n    val uuid = user.namespace.uuid // this is namespace identifier\n    val throttle = rateMap.getOrElseUpdate(uuid, new RateInfo)\n    val limit = maxPerMinute(user)\n    val rate = TimedRateLimit(throttle.update(limit), limit)\n    logging.debug(this, s\"namespace = ${uuid.asString} rate = ${rate.count}, limit = $limit\")\n    rate\n  }\n}\n\n/**\n * Tracks the activation rate of one subject at minute-granularity.\n */\nprivate class RateInfo {\n  @volatile var lastMin: Long = getCurrentMinute\n  val lastMinCount = new AtomicInteger()\n\n  /**\n   * Increments operation count in the current time window by\n   * one and checks if below allowed max rate.\n   *\n   * @param maxPerMinute the current maximum allowed requests\n   *                     per minute (might change over time)\n   * @return current count\n   */\n  def update(maxPerMinute: Int): Int = {\n    roll()\n    lastMinCount.incrementAndGet()\n  }\n\n  def roll(): Unit = {\n    val curMin = getCurrentMinute\n    if (curMin != lastMin) {\n      lastMin = curMin\n      lastMinCount.set(0)\n    }\n  }\n\n  private def getCurrentMinute = System.currentTimeMillis / (60 * 1000)\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/CommonLoadBalancer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer\n\nimport org.apache.pekko.actor.ActorRef\nimport java.nio.charset.StandardCharsets\nimport java.util.concurrent.atomic.LongAdder\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging.InfoLevel\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.common.LoggingMarkers._\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.controller.Controller\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\n\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future, Promise}\nimport scala.util.{Failure, Success}\n\n/**\n * Abstract class which provides common logic for all LoadBalancer implementations.\n */\nabstract class CommonLoadBalancer(config: WhiskConfig,\n                                  feedFactory: FeedFactory,\n                                  controllerInstance: ControllerInstanceId)(implicit\n                                                                            val actorSystem: ActorSystem,\n                                                                            logging: Logging,\n                                                                            messagingProvider: MessagingProvider)\n    extends LoadBalancer {\n\n  protected implicit val executionContext: ExecutionContext = actorSystem.dispatcher\n\n  val lbConfig: ShardingContainerPoolBalancerConfig =\n    loadConfigOrThrow[ShardingContainerPoolBalancerConfig](ConfigKeys.loadbalancer)\n  protected val invokerPool: ActorRef\n\n  /** State related to invocations and throttling */\n  protected[loadBalancer] val activationSlots = TrieMap[ActivationId, ActivationEntry]()\n  protected[loadBalancer] val activationPromises =\n    TrieMap[ActivationId, Promise[Either[ActivationId, WhiskActivation]]]()\n  protected val activationsPerNamespace = TrieMap[UUID, LongAdder]()\n  protected val activationsPerController = TrieMap[ControllerInstanceId, LongAdder]()\n  protected val activationsPerInvoker = TrieMap[InvokerInstanceId, LongAdder]()\n  protected val totalActivations = new LongAdder()\n  protected val totalBlackBoxActivationMemory = new LongAdder()\n  protected val totalManagedActivationMemory = new LongAdder()\n\n  protected def emitMetrics() = {\n    MetricEmitter.emitGaugeMetric(LOADBALANCER_ACTIVATIONS_INFLIGHT(controllerInstance), totalActivations.longValue)\n    MetricEmitter.emitGaugeMetric(\n      LOADBALANCER_MEMORY_INFLIGHT(controllerInstance, \"\"),\n      totalBlackBoxActivationMemory.longValue + totalManagedActivationMemory.longValue)\n    MetricEmitter.emitGaugeMetric(\n      LOADBALANCER_MEMORY_INFLIGHT(controllerInstance, \"Blackbox\"),\n      totalBlackBoxActivationMemory.longValue)\n    MetricEmitter.emitGaugeMetric(\n      LOADBALANCER_MEMORY_INFLIGHT(controllerInstance, \"Managed\"),\n      totalManagedActivationMemory.longValue)\n  }\n\n  actorSystem.scheduler.scheduleAtFixedRate(10.seconds, 10.seconds)(() => emitMetrics())\n\n  override def activeActivationsFor(namespace: UUID): Future[Int] =\n    Future.successful(activationsPerNamespace.get(namespace).map(_.intValue).getOrElse(0))\n  override def totalActiveActivations: Future[Int] = Future.successful(totalActivations.intValue)\n  override def activeActivationsByController(controller: String): Future[Int] =\n    Future.successful(activationsPerController.get(ControllerInstanceId(controller)).map(_.intValue()).getOrElse(0))\n  override def activeActivationsByController: Future[List[(String, String)]] =\n    Future.successful(\n      activationSlots.values.map(entry => (entry.id.asString, entry.fullyQualifiedEntityName.toString)).toList)\n  override def activeActivationsByInvoker(invoker: String): Future[Int] =\n    Future.successful(\n      activationsPerInvoker.get(InvokerInstanceId(invoker.toInt, userMemory = 0.MB)).map(_.intValue()).getOrElse(0))\n  override def close: Unit = activationFeed ! GracefulShutdown\n\n  /**\n   * Calculate the duration within which a completion ack must be received for an activation.\n   *\n   * Calculation is based on the passed action time limit. If the passed action time limit is shorter than\n   * the configured standard action time limit, the latter is used to avoid too tight timeouts.\n   *\n   * The base timeout is multiplied with a configurable timeout factor. This dilution controls how much slack you\n   * want to allow in your system before you start reporting failed activations. The default value of 2 bases\n   * on invoker behavior that a cold invocation's init duration may be as long as its run duration. Higher factors\n   * may account for additional wait times.\n   *\n   * Finally, a configurable duration is added to the diluted timeout to be lenient towards general delays / wait times.\n   *\n   * @param actionTimeLimit the action's time limit\n   * @return the calculated time duration within which a completion ack must be received\n   */\n  private def calculateCompletionAckTimeout(actionTimeLimit: FiniteDuration): FiniteDuration = {\n    (actionTimeLimit.max(TimeLimit.STD_DURATION) * lbConfig.timeoutFactor) + lbConfig.timeoutAddon\n  }\n\n  /**\n   * 2. Update local state with the activation to be executed scheduled.\n   *\n   * All activations are tracked in the activationSlots map. Additionally, blocking invokes\n   * are tracked in the activationPromises map. When a result is received via result ack, it\n   * will cause the result to be forwarded to the caller waiting on the result, and cancel\n   * the DB poll which is also trying to do the same.\n   * Once the completion ack arrives, activationSlots entry will be removed.\n   */\n  protected def setupActivation(msg: ActivationMessage,\n                                action: ExecutableWhiskActionMetaData,\n                                instance: InvokerInstanceId): Future[Either[ActivationId, WhiskActivation]] = {\n\n    // Needed for emitting metrics.\n    totalActivations.increment()\n    val isBlackboxInvocation = action.exec.pull\n    val totalActivationMemory =\n      if (isBlackboxInvocation) totalBlackBoxActivationMemory else totalManagedActivationMemory\n    totalActivationMemory.add(action.limits.memory.megabytes)\n\n    activationsPerNamespace.getOrElseUpdate(msg.user.namespace.uuid, new LongAdder()).increment()\n    activationsPerController.getOrElseUpdate(controllerInstance, new LongAdder()).increment()\n    activationsPerInvoker\n      .getOrElseUpdate(InvokerInstanceId(instance.instance, userMemory = 0.MB), new LongAdder())\n      .increment()\n\n    // Completion Ack must be received within the calculated time.\n    val completionAckTimeout = calculateCompletionAckTimeout(action.limits.timeout.duration)\n\n    // If activation is blocking, store a promise that we can mark successful later on once the result ack\n    // arrives. Return a Future representing the promise to caller.\n    // If activation is non-blocking, return a successfully completed Future to caller.\n    val resultPromise = if (msg.blocking) {\n      activationPromises.getOrElseUpdate(msg.activationId, Promise[Either[ActivationId, WhiskActivation]]()).future\n    } else Future.successful(Left(msg.activationId))\n\n    // Install a timeout handler for the catastrophic case where a completion ack is not received at all\n    // (because say an invoker is down completely, or the connection to the message bus is disrupted) or when\n    // the completion ack is significantly delayed (possibly dues to long queues but the subject should not be penalized);\n    // in this case, if the activation handler is still registered, remove it and update the books.\n    //\n    // Attention: a significantly delayed completion ack means that the invoker is still busy or will be busy in future\n    // with running the action. So the current strategy of freeing up the activation's memory in invoker\n    // book-keeping will allow the load balancer to send more activations to the invoker. This can lead to\n    // invoker overloads so that activations need to wait until other activations complete.\n    activationSlots.getOrElseUpdate(\n      msg.activationId, {\n        val timeoutHandler = actorSystem.scheduler.scheduleOnce(completionAckTimeout) {\n          processCompletion(msg.activationId, msg.transid, forced = true, isSystemError = false, instance = instance)\n        }\n\n        // please note: timeoutHandler.cancel must be called on all non-timeout paths, e.g. Success\n        ActivationEntry(\n          msg.activationId,\n          msg.user.namespace.uuid,\n          instance,\n          action.limits.memory.megabytes.MB,\n          action.limits.timeout.duration,\n          action.limits.concurrency.maxConcurrent,\n          action.fullyQualifiedName(true),\n          timeoutHandler,\n          isBlackboxInvocation,\n          msg.blocking,\n          controllerInstance)\n      })\n\n    resultPromise\n  }\n\n  protected val messageProducer =\n    messagingProvider.getProducer(config, Some(ActivationEntityLimit.MAX_ACTIVATION_LIMIT))\n\n  /** 3. Send the activation to the invoker */\n  protected def sendActivationToInvoker(producer: MessageProducer,\n                                        msg: ActivationMessage,\n                                        invoker: InvokerInstanceId): Future[ResultMetadata] = {\n    implicit val transid: TransactionId = msg.transid\n\n    val topic = s\"${Controller.topicPrefix}invoker${invoker.toInt}\"\n\n    MetricEmitter.emitCounterMetric(LoggingMarkers.LOADBALANCER_ACTIVATION_START)\n    val start = transid.started(\n      this,\n      LoggingMarkers.CONTROLLER_KAFKA,\n      s\"posting topic '$topic' with activation id '${msg.activationId}'\",\n      logLevel = InfoLevel)\n\n    producer.send(topic, msg).andThen {\n      case Success(status) =>\n        transid.finished(\n          this,\n          start,\n          s\"posted to ${status.topic}[${status.partition}][${status.offset}]\",\n          logLevel = InfoLevel)\n      case Failure(_) => transid.failed(this, start, s\"error on posting to topic $topic\")\n    }\n  }\n\n  /** Subscribes to ack messages from the invokers (result / completion) and registers a handler for these messages. */\n  private val activationFeed: ActorRef =\n    feedFactory.createFeed(actorSystem, messagingProvider, processAcknowledgement)\n\n  /** 4. Get the ack message and parse it */\n  protected[loadBalancer] def processAcknowledgement(bytes: Array[Byte]): Future[Unit] = Future {\n    val raw = new String(bytes, StandardCharsets.UTF_8)\n    AcknowledgementMessage.parse(raw) match {\n      case Success(acknowledgement) =>\n        acknowledgement.isSlotFree.foreach { instance =>\n          processCompletion(\n            acknowledgement.activationId,\n            acknowledgement.transid,\n            forced = false,\n            isSystemError = acknowledgement.isSystemError.getOrElse(false),\n            instance)\n        }\n\n        acknowledgement.result.foreach { response =>\n          processResult(acknowledgement.activationId, acknowledgement.transid, response)\n        }\n\n        activationFeed ! MessageFeed.Processed\n\n      case Failure(t) =>\n        activationFeed ! MessageFeed.Processed\n        logging.error(this, s\"failed processing message: $raw\")\n\n      case _ =>\n        activationFeed ! MessageFeed.Processed\n        logging.error(this, s\"Unexpected Acknowledgement message received by loadbalancer: $raw\")\n    }\n  }\n\n  /** 5. Process the result ack and return it to the user */\n  protected def processResult(aid: ActivationId,\n                              tid: TransactionId,\n                              response: Either[ActivationId, WhiskActivation]): Unit = {\n    // Resolve the promise to send the result back to the user.\n    // The activation will be removed from the activation slots later, when the completion message\n    // is received (because the slot in the invoker is not yet free for new activations).\n    activationPromises.remove(aid).foreach(_.trySuccess(response))\n    logging.info(this, s\"received result ack for '$aid'\")(tid)\n  }\n\n  protected def releaseInvoker(invoker: InvokerInstanceId, entry: ActivationEntry): Unit\n\n  // Singletons for counter metrics related to completion acks\n  protected val LOADBALANCER_COMPLETION_ACK_REGULAR =\n    LoggingMarkers.LOADBALANCER_COMPLETION_ACK(controllerInstance, RegularCompletionAck)\n  protected val LOADBALANCER_COMPLETION_ACK_FORCED =\n    LoggingMarkers.LOADBALANCER_COMPLETION_ACK(controllerInstance, ForcedCompletionAck)\n  protected val LOADBALANCER_COMPLETION_ACK_HEALTHCHECK =\n    LoggingMarkers.LOADBALANCER_COMPLETION_ACK(controllerInstance, HealthcheckCompletionAck)\n  protected val LOADBALANCER_COMPLETION_ACK_REGULAR_AFTER_FORCED =\n    LoggingMarkers.LOADBALANCER_COMPLETION_ACK(controllerInstance, RegularAfterForcedCompletionAck)\n  protected val LOADBALANCER_COMPLETION_ACK_FORCED_AFTER_REGULAR =\n    LoggingMarkers.LOADBALANCER_COMPLETION_ACK(controllerInstance, ForcedAfterRegularCompletionAck)\n\n  /** 6. Process the completion ack and update the state */\n  protected[loadBalancer] def processCompletion(aid: ActivationId,\n                                                tid: TransactionId,\n                                                forced: Boolean,\n                                                isSystemError: Boolean,\n                                                instance: InstanceId): Unit = {\n\n    val invoker = instance match {\n      case i: InvokerInstanceId => Some(i)\n      case _                    => None\n    }\n\n    val invocationResult = if (forced) {\n      InvocationFinishedResult.Timeout\n    } else {\n      // If the response contains a system error, report that, otherwise report Success\n      // Left generally is considered a Success, since that could be a message not fitting into Kafka\n      if (isSystemError) {\n        InvocationFinishedResult.SystemError\n      } else {\n        InvocationFinishedResult.Success\n      }\n    }\n\n    activationSlots.remove(aid) match {\n      case Some(entry) =>\n        totalActivations.decrement()\n        val totalActivationMemory =\n          if (entry.isBlackbox) totalBlackBoxActivationMemory else totalManagedActivationMemory\n        totalActivationMemory.add(entry.memoryLimit.toMB * (-1))\n        activationsPerNamespace.get(entry.namespaceId).foreach(_.decrement())\n        activationsPerController.get(entry.controllerId).foreach(_.decrement())\n        activationsPerInvoker\n          .get(InvokerInstanceId(entry.invokerName.instance, userMemory = 0.MB))\n          .foreach(_.decrement())\n\n        invoker.foreach(releaseInvoker(_, entry))\n\n        if (!forced) {\n          entry.timeoutHandler.cancel()\n          // notice here that the activationPromises is not touched, because the expectation is that\n          // the active ack is received as expected, and processing that message removed the promise\n          // from the corresponding map\n          logging.info(this, s\"received completion ack for '$aid', system error=$isSystemError\")(tid)\n\n          MetricEmitter.emitCounterMetric(LOADBALANCER_COMPLETION_ACK_REGULAR)\n\n        } else {\n          // the entry has timed out; if the active ack is still around, remove its entry also\n          // and complete the promise with a failure if necessary\n          activationPromises\n            .remove(aid)\n            .foreach(_.tryFailure(new Throwable(\"no completion or active ack received yet\")))\n          val actionType = if (entry.isBlackbox) \"blackbox\" else \"managed\"\n          val blockingType = if (entry.isBlocking) \"blocking\" else \"non-blocking\"\n          val completionAckTimeout = calculateCompletionAckTimeout(entry.timeLimit)\n          logging.warn(\n            this,\n            s\"forced completion ack for '$aid', action '${entry.fullyQualifiedEntityName}' ($actionType), $blockingType, mem limit ${entry.memoryLimit.toMB} MB, time limit ${entry.timeLimit.toMillis} ms, completion ack timeout $completionAckTimeout from $instance\")(\n            tid)\n\n          MetricEmitter.emitCounterMetric(LOADBALANCER_COMPLETION_ACK_FORCED)\n        }\n\n        // Completion acks that are received here are strictly from user actions - health actions are not part of\n        // the load balancer's activation map. Inform the invoker pool supervisor of the user action completion.\n        // guard this\n        invoker.foreach(invokerPool ! InvocationFinishedMessage(_, invocationResult))\n      case None if tid == TransactionId.invokerHealth =>\n        // Health actions do not have an ActivationEntry as they are written on the message bus directly. Their result\n        // is important to pass to the invokerPool because they are used to determine if the invoker can be considered\n        // healthy again.\n        logging.info(this, s\"received completion ack for health action on $instance\")(tid)\n\n        MetricEmitter.emitCounterMetric(LOADBALANCER_COMPLETION_ACK_HEALTHCHECK)\n\n        // guard this\n        invoker.foreach(invokerPool ! InvocationFinishedMessage(_, invocationResult))\n      case None if !forced =>\n        // Received a completion ack that has already been taken out of the state because of a timeout (forced ack).\n        // The result is ignored because a timeout has already been reported to the invokerPool per the force.\n        // Logging this condition as a warning because the invoker processed the activation and sent a completion\n        // message - but not in time.\n        logging.warn(\n          this,\n          s\"received completion ack for '$aid' from $instance which has no entry, system error=$isSystemError\")(tid)\n\n        MetricEmitter.emitCounterMetric(LOADBALANCER_COMPLETION_ACK_REGULAR_AFTER_FORCED)\n      case None =>\n        // The entry has already been removed by a completion ack. This part of the code is reached by the timeout and can\n        // happen if completion ack and timeout happen roughly at the same time (the timeout was triggered before the completion\n        // ack canceled the timer). As the completion ack is already processed we don't have to do anything here.\n        logging.debug(this, s\"forced completion ack for '$aid' which has no entry\")(tid)\n\n        MetricEmitter.emitCounterMetric(LOADBALANCER_COMPLETION_ACK_FORCED_AFTER_REGULAR)\n    }\n  }\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/FPCPoolBalancer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer\n\nimport java.nio.charset.StandardCharsets\nimport java.util.concurrent.ThreadLocalRandom\nimport java.util.concurrent.atomic.LongAdder\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, ActorSystem, Cancellable, Props}\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.util.Timeout\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Offline, Unhealthy}\nimport org.apache.openwhisk.common.LoggingMarkers._\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.controller.Controller\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.etcd.EtcdKV.{InvokerKeys, QueueKeys, SchedulerKeys, ThrottlingKeys}\nimport org.apache.openwhisk.core.etcd.EtcdType._\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdConfig}\nimport org.apache.openwhisk.core.scheduler.queue.{CreateQueue, CreateQueueResponse, QueueManager}\nimport org.apache.openwhisk.core.scheduler.{SchedulerEndpoints, SchedulerStates}\nimport org.apache.openwhisk.core.service._\nimport org.apache.openwhisk.core.{ConfigKeys, WarmUp, WhiskConfig}\nimport org.apache.openwhisk.spi.SpiLoader\nimport org.apache.openwhisk.utils.retry\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.collection.JavaConverters._\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future, Promise}\nimport scala.language.postfixOps\nimport scala.util.{Failure, Random, Success, Try}\n\ncase class FPCPoolBalancerConfig(usePerMinThrottle: Boolean)\n\nclass FPCPoolBalancer(config: WhiskConfig,\n                      controllerInstance: ControllerInstanceId,\n                      etcdClient: EtcdClient,\n                      private val feedFactory: FeedFactory,\n                      lbConfig: ShardingContainerPoolBalancerConfig =\n                        loadConfigOrThrow[ShardingContainerPoolBalancerConfig](ConfigKeys.loadbalancer),\n                      private val messagingProvider: MessagingProvider = SpiLoader.get[MessagingProvider])(\n  implicit val actorSystem: ActorSystem,\n  logging: Logging)\n    extends LoadBalancer {\n\n  private implicit val executionContext: ExecutionContext = actorSystem.dispatcher\n  // This value is given according to the total waiting time at QueueManager for a new queue to be created.\n  private implicit val requestTimeout: Timeout = Timeout(8.seconds)\n\n  private val entityStore = WhiskEntityStore.datastore()\n\n  private val clusterName = loadConfigOrThrow[String](ConfigKeys.whiskClusterName)\n\n  /** key: SchedulerEndpoints, value: SchedulerStates */\n  private val schedulerEndpoints = TrieMap[SchedulerEndpoints, SchedulerStates]()\n  private val messageProducer = messagingProvider.getProducer(config, Some(ActivationEntityLimit.MAX_ACTIVATION_LIMIT))\n\n  private val watcherName = \"distributed-pool-balancer\"\n  val watcherService: ActorRef = actorSystem.actorOf(WatcherService.props(etcdClient))\n\n  /** State related to invocations and throttling */\n  protected[loadBalancer] val activationSlots = TrieMap[ActivationId, DistributedActivationEntry]()\n  protected[loadBalancer] val activationPromises =\n    TrieMap[ActivationId, Promise[Either[ActivationId, WhiskActivation]]]()\n\n  /** key: queue/${invocationNs}/ns/action/leader, value: SchedulerEndpoints */\n  private val queueEndpoints = TrieMap[String, SchedulerEndpoints]()\n\n  private val activationsPerNamespace = TrieMap[UUID, LongAdder]()\n  private val activationsPerController = TrieMap[ControllerInstanceId, LongAdder]()\n  private val totalActivations = new LongAdder()\n  private val totalActivationMemory = new LongAdder()\n  private val throttlers = TrieMap[String, Boolean]()\n\n  /**\n   * Publishes activation message on internal bus for an invoker to pick up.\n   *\n   * @param action the action to invoke\n   * @param msg the activation message to publish on an invoker topic\n   * @param transid the transaction id for the request\n   * @return result a nested Future the outer indicating completion of publishing and\n   *         the inner the completion of the action (i.e., the result)\n   *         if it is ready before timeout (Right) otherwise the activation id (Left).\n   *         The future is guaranteed to complete within the declared action time limit\n   *         plus a grace period (see activeAckTimeoutGrace).\n   */\n  override def publish(action: ExecutableWhiskActionMetaData, msg: ActivationMessage)(\n    implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]] = {\n\n    val topicBaseName = if (schedulerEndpoints.isEmpty) {\n      logging.error(\n        this,\n        s\"Failed to invoke action ${action.fullyQualifiedName(false)}, error: no scheduler endpoint available\")\n      Future.failed(LoadBalancerException(\"No scheduler endpoint available\"))\n    } else {\n      val invocationNamespace = msg.user.namespace.name.asString\n      val key = QueueKeys.queue(invocationNamespace, action.fullyQualifiedName(false), true)\n\n      queueEndpoints.get(key) match {\n        case Some(endPoint) =>\n          Future.successful(\n            schedulerEndpoints.getOrElse(endPoint, Random.shuffle(schedulerEndpoints.toList).head._2).sid.toString)\n        case None =>\n          etcdClient\n            .get(key)\n            .map { res =>\n              res.getKvsList.asScala.headOption map { kv =>\n                val endPoint: String = kv.getValue\n                SchedulerEndpoints\n                  .parse(endPoint)\n                  .map { endPoint =>\n                    queueEndpoints.update(kv.getKey, endPoint)\n                    Some(\n                      schedulerEndpoints\n                        .getOrElse(endPoint, Random.shuffle(schedulerEndpoints.toList).head._2)\n                        .sid\n                        .toString)\n                  }\n                  .getOrElse {\n                    FPCPoolBalancer.schedule(schedulerEndpoints.values.toIndexedSeq).map { scheduler =>\n                      createQueue(invocationNamespace, action.toWhiskAction, msg.action, msg.revision, scheduler)\n                      scheduler.sid.toString\n                    }\n                  }\n              } getOrElse {\n                FPCPoolBalancer.schedule(schedulerEndpoints.values.toIndexedSeq).map { scheduler =>\n                  createQueue(invocationNamespace, action.toWhiskAction, msg.action, msg.revision, scheduler)\n                  scheduler.sid.toString\n                }\n              }\n            }\n            .map { _.get }\n            .recoverWith {\n              case _ =>\n                Future.failed(LoadBalancerException(\"No scheduler endpoint available\"))\n            }\n      }\n    }\n    topicBaseName.flatMap { baseName =>\n      val topicName = Controller.topicPrefix + baseName\n      val activationResult = setupActivation(msg, action)\n      sendActivationToKafka(messageProducer, msg, topicName).map(_ => activationResult)\n    }\n  }\n\n  private def createQueue(\n    invocationNamespace: String,\n    actionMetaData: WhiskActionMetaData,\n    fullyQualifiedEntityName: FullyQualifiedEntityName,\n    revision: DocRevision,\n    scheduler: SchedulerStates,\n    retryCount: Int = 5,\n    excludeSchedulers: Set[SchedulerInstanceId] = Set.empty)(implicit transid: TransactionId): Unit = {\n    if (retryCount >= 0)\n      scheduler\n        .getRemoteRef(QueueManager.actorName)\n        .ask(CreateQueue(invocationNamespace, fullyQualifiedEntityName, revision, actionMetaData))\n        .mapTo[CreateQueueResponse]\n        .onComplete {\n          case Success(_) =>\n            logging.info(\n              this,\n              s\"Created queue successfully for $invocationNamespace/$fullyQualifiedEntityName on ${scheduler.sid}\")\n          case Failure(t) =>\n            logging.error(\n              this,\n              s\"failed to get response from ${scheduler}, error is $t, will retry for ${retryCount} times\")\n            // try another scheduler\n            FPCPoolBalancer\n              .schedule(schedulerEndpoints.values.toIndexedSeq, excludeSchedulers + scheduler.sid)\n              .map { newScheduler =>\n                createQueue(\n                  invocationNamespace,\n                  actionMetaData,\n                  fullyQualifiedEntityName,\n                  revision,\n                  newScheduler,\n                  retryCount - 1,\n                  excludeSchedulers + scheduler.sid)\n              }\n              .getOrElse {\n                logging.error(\n                  this,\n                  s\"failed to create queue for $invocationNamespace/$fullyQualifiedEntityName, no scheduler endpoint available related activations may fail\")\n              }\n        } else\n      logging.error(\n        this,\n        s\"failed to create queue for $invocationNamespace/$fullyQualifiedEntityName, related activations may fail\")\n  }\n\n  /**\n   * 2. Update local state with the to be executed activation.\n   *\n   * All activations are tracked in the activationSlots map. Additionally, blocking invokes\n   * are tracked in the activation results map. When a result is received via activeack, it\n   * will cause the result to be forwarded to the caller waiting on the result, and cancel\n   * the DB poll which is also trying to do the same.\n   */\n  private def setupActivation(msg: ActivationMessage,\n                              action: ExecutableWhiskActionMetaData): Future[Either[ActivationId, WhiskActivation]] = {\n    val isBlackboxInvocation = action.exec.pull\n\n    totalActivations.increment()\n    totalActivationMemory.add(action.limits.memory.megabytes)\n    activationsPerNamespace.getOrElseUpdate(msg.user.namespace.uuid, new LongAdder()).increment()\n    activationsPerController.getOrElseUpdate(controllerInstance, new LongAdder()).increment()\n\n    // Timeout is a multiple of the configured maximum action duration. The minimum timeout is the configured standard\n    // value for action durations to avoid too tight timeouts.\n    // Timeouts in general are diluted by a configurable factor. In essence this factor controls how much slack you want\n    // to allow in your topics before you start reporting failed activations.\n    val timeout = (action.limits.timeout.duration.max(TimeLimit.STD_DURATION) * lbConfig.timeoutFactor) + 1.minute\n\n    val resultPromise = if (msg.blocking) {\n      activationPromises.getOrElseUpdate(msg.activationId, Promise[Either[ActivationId, WhiskActivation]]()).future\n    } else Future.successful(Left(msg.activationId))\n\n    // Install a timeout handler for the catastrophic case where an active ack is not received at all\n    // (because say an invoker is down completely, or the connection to the message bus is disrupted) or when\n    // the active ack is significantly delayed (possibly dues to long queues but the subject should not be penalized);\n    // in this case, if the activation handler is still registered, remove it and update the books.\n    activationSlots.getOrElseUpdate(\n      msg.activationId, {\n        val timeoutHandler = actorSystem.scheduler.scheduleOnce(timeout) {\n          processCompletion(msg.activationId, msg.transid, forced = true, isSystemError = false)\n        }\n\n        // please note: timeoutHandler.cancel must be called on all non-timeout paths, e.g. Success\n        DistributedActivationEntry(\n          msg.activationId,\n          msg.user.namespace.uuid,\n          msg.user.namespace.name.asString,\n          msg.revision,\n          msg.transid,\n          action.limits.memory.megabytes.MB,\n          action.limits.concurrency.maxConcurrent,\n          action.fullyQualifiedName(true),\n          timeoutHandler,\n          isBlackboxInvocation,\n          msg.blocking,\n          controllerInstance)\n      })\n\n    resultPromise\n  }\n\n  /** 3. Send the activation to the kafka */\n  private def sendActivationToKafka(producer: MessageProducer,\n                                    msg: ActivationMessage,\n                                    topic: String): Future[ResultMetadata] = {\n    implicit val transid: TransactionId = msg.transid\n\n    MetricEmitter.emitCounterMetric(LoggingMarkers.LOADBALANCER_ACTIVATION_START)\n    val start = transid.started(this, LoggingMarkers.CONTROLLER_KAFKA)\n\n    producer.send(topic, msg).andThen {\n      case Success(status) =>\n        transid.finished(\n          this,\n          start,\n          s\"posted to ${status.topic}[${status.partition}][${status.offset}]\",\n          logLevel = InfoLevel)\n      case Failure(_) => transid.failed(this, start, s\"error on posting to topic $topic\")\n    }\n  }\n\n  /**\n   * Subscribes to active acks (completion messages from the invokers), and\n   * registers a handler for received active acks from invokers.\n   */\n  private val activationFeed: ActorRef =\n    feedFactory.createFeed(actorSystem, messagingProvider, processAcknowledgement)\n\n  /** 4. Get the active-ack message and parse it */\n  protected[loadBalancer] def processAcknowledgement(bytes: Array[Byte]): Future[Unit] = Future {\n    val raw = new String(bytes, StandardCharsets.UTF_8)\n    AcknowledgementMessage.parse(raw) match {\n      case Success(acknowledgement) =>\n        acknowledgement.isSlotFree.foreach { invoker =>\n          processCompletion(\n            acknowledgement.activationId,\n            acknowledgement.transid,\n            forced = false,\n            isSystemError = acknowledgement.isSystemError.getOrElse(false))\n        }\n\n        acknowledgement.result.foreach { response =>\n          processResult(acknowledgement.activationId, acknowledgement.transid, response)\n        }\n\n        activationFeed ! MessageFeed.Processed\n      case Failure(t) =>\n        activationFeed ! MessageFeed.Processed\n        logging.error(this, s\"failed processing message: $raw\")\n\n      case _ =>\n        activationFeed ! MessageFeed.Processed\n        logging.warn(this, s\"Unexpected Acknowledgement message received by loadbalancer: $raw\")\n    }\n  }\n\n  private def renewTimeoutHandler(entry: DistributedActivationEntry,\n                                  msg: ActivationMessage,\n                                  isSystemError: Boolean): Unit = {\n    entry.timeoutHandler.cancel()\n\n    val timeout = (TimeLimit.MAX_DURATION * lbConfig.timeoutFactor) + 1.minute\n    val timeoutHandler = actorSystem.scheduler.scheduleOnce(timeout) {\n      processCompletion(msg.activationId, msg.transid, forced = true, isSystemError)\n    }\n    activationSlots.update(msg.activationId, entry.copy(timeoutHandler = timeoutHandler))\n  }\n\n  /** 5. Process the result ack and return it to the user */\n  private def processResult(aid: ActivationId,\n                            tid: TransactionId,\n                            response: Either[ActivationId, WhiskActivation]): Unit = {\n    // Resolve the promise to send the result back to the user.\n    // The activation will be removed from the activation slots later, when the completion message\n    // is received (because the slot in the invoker is not yet free for new activations).\n    activationPromises.remove(aid).foreach(_.trySuccess(response))\n    logging.info(this, s\"received result ack for '$aid'\")(tid)\n  }\n\n  private def deleteActivationSlot(aid: ActivationId, tid: TransactionId): Option[DistributedActivationEntry] = {\n    activationSlots.remove(aid) match {\n      case option =>\n        if (activationSlots.contains(aid)) {\n          logging.warn(this, s\"Failed to delete $aid from activation slots\")(tid)\n          throw new Exception(s\"Failed to delete $aid from activation slots\")\n        }\n        option\n    }\n  }\n\n  // Singletons for counter metrics related to completion acks\n  protected val LOADBALANCER_COMPLETION_ACK_REGULAR =\n    LoggingMarkers.LOADBALANCER_COMPLETION_ACK(controllerInstance, RegularCompletionAck)\n  protected val LOADBALANCER_COMPLETION_ACK_FORCED =\n    LoggingMarkers.LOADBALANCER_COMPLETION_ACK(controllerInstance, ForcedCompletionAck)\n  protected val LOADBALANCER_COMPLETION_ACK_REGULAR_AFTER_FORCED =\n    LoggingMarkers.LOADBALANCER_COMPLETION_ACK(controllerInstance, RegularAfterForcedCompletionAck)\n  protected val LOADBALANCER_COMPLETION_ACK_FORCED_AFTER_REGULAR =\n    LoggingMarkers.LOADBALANCER_COMPLETION_ACK(controllerInstance, ForcedAfterRegularCompletionAck)\n\n  /** Process the completion ack and update the state */\n  protected[loadBalancer] def processCompletion(aid: ActivationId,\n                                                tid: TransactionId,\n                                                forced: Boolean,\n                                                isSystemError: Boolean): Unit = {\n    implicit val transid = tid\n    activationSlots.remove(aid) match {\n      case Some(entry) =>\n        if (activationSlots.contains(aid))\n          Try { retry(deleteActivationSlot(aid, tid)) } recover {\n            case _ =>\n              logging.error(this, s\"Failed to delete $aid from activation slots\")\n          }\n        totalActivations.decrement()\n        totalActivationMemory.add(entry.memory.toMB * (-1))\n        activationsPerNamespace.get(entry.namespaceId).foreach(_.decrement())\n        activationsPerController.get(entry.controllerName).foreach(_.decrement())\n\n        if (!forced) {\n          entry.timeoutHandler.cancel()\n          // notice here that the activationPromises is not touched, because the expectation is that\n          // the active ack is received as expected, and processing that message removed the promise\n          // from the corresponding map\n          logging.info(this, s\"received completion ack for '$aid', system error=$isSystemError\")(tid)\n          MetricEmitter.emitCounterMetric(LOADBALANCER_COMPLETION_ACK_REGULAR)\n        } else {\n          logging.error(this, s\"Failed to invoke action ${aid.toString}, error: timeout waiting for the active ack\")\n          MetricEmitter.emitCounterMetric(LOADBALANCER_COMPLETION_ACK_FORCED)\n\n          // the entry has timed out; if the active ack is still around, remove its entry also\n          // and complete the promise with a failure if necessary\n          activationPromises\n            .remove(aid)\n            .foreach(\n              _.tryFailure(new Throwable(\"Activation entry has timed out, no completion or active ack received yet\")))\n        }\n\n      // Active acks that are received here are strictly from user actions - health actions are not part of\n      // the load balancer's activation map. Inform the invoker pool supervisor of the user action completion.\n      case None if !forced =>\n        // Received a completion ack that has already been taken out of the state because of a timeout (forced ack).\n        // The result is ignored because a timeout has already been reported to the invokerPool per the force.\n        // Logging this condition as a warning because the invoker processed the activation and sent a completion\n        // message - but not in time.\n        logging.warn(this, s\"received completion ack for '$aid' which has no entry, system error=$isSystemError\")(tid)\n        MetricEmitter.emitCounterMetric(LOADBALANCER_COMPLETION_ACK_REGULAR_AFTER_FORCED)\n      case None =>\n        // The entry has already been removed by a completion ack. This part of the code is reached by the timeout and can\n        // happen if completion ack and timeout happen roughly at the same time (the timeout was triggered before the completion\n        // ack canceled the timer). As the completion ack is already processed we don't have to do anything here.\n        logging.debug(this, s\"forced completion ack for '$aid' which has no entry\")(tid)\n        MetricEmitter.emitCounterMetric(LOADBALANCER_COMPLETION_ACK_FORCED_AFTER_REGULAR)\n    }\n  }\n\n  private val queueKey = QueueKeys.queuePrefix\n  private val schedulerKey = SchedulerKeys.prefix\n  private val throttlingKey = ThrottlingKeys.prefix\n  private val watchedKeys = Set(queueKey, schedulerKey, throttlingKey)\n\n  private val watcher = actorSystem.actorOf(Props(new Actor {\n    watchedKeys.foreach { key =>\n      watcherService ! WatchEndpoint(key, \"\", true, watcherName, Set(PutEvent, DeleteEvent))\n    }\n\n    override def receive: Receive = {\n      case WatchEndpointRemoved(watchKey, key, value, true) =>\n        watchKey match {\n          case `queueKey` =>\n            if (key.contains(\"leader\")) {\n              queueEndpoints.remove(key)\n              activationSlots.values\n                .find(entry =>\n                  //the leader key's value is queue/invocationNamespace/ns/pkg/act/leader\n                  QueueKeys\n                    .queue(entry.invocationNamespace, entry.fullyQualifiedEntityName.copy(version = None), true) == key)\n                .foreach {\n                  entry =>\n                    implicit val transid = entry.transactionId\n                    logging.warn(\n                      this,\n                      s\"The $key is deleted from ETCD, but there are still unhandled activations for this action, try to create a new queue\")\n                    WhiskActionMetaData\n                      .get(\n                        entityStore,\n                        entry.fullyQualifiedEntityName.toDocId,\n                        DocRevision.empty,\n                        entry.revision != DocRevision.empty)\n                      .map { actionMetaData =>\n                        FPCPoolBalancer\n                          .schedule(schedulerEndpoints.values.toIndexedSeq)\n                          .map { scheduler =>\n                            createQueue(\n                              entry.invocationNamespace,\n                              actionMetaData,\n                              entry.fullyQualifiedEntityName,\n                              entry.revision,\n                              scheduler)\n                          }\n                          .getOrElse {\n                            logging.error(\n                              this,\n                              s\"Failed to recreate queue for ${entry.fullyQualifiedEntityName}, no scheduler endpoint available\")\n                          }\n                      }\n                }\n            }\n          case `schedulerKey` =>\n            SchedulerStates\n              .parse(value)\n              .map { state =>\n                logging.info(this, s\"remove scheduler endpoint $state\")\n                schedulerEndpoints.remove(state.endpoints)\n              }\n              .recover {\n                case t =>\n                  logging.error(this, s\"Unexpected error$t\")\n              }\n\n          case `throttlingKey` =>\n            throttlers.remove(key)\n          case _ =>\n        }\n\n      case WatchEndpointInserted(watchKey, key, value, true) =>\n        watchKey match {\n          case `queueKey` =>\n            //ignore parse follower value, just parse leader value's normal value, e.g. on special case, leader key's value may be Removing\n            if (key.contains(\"leader\") && value.contains(\"host\")) {\n              SchedulerEndpoints\n                .parse(value)\n                .map { endpoints =>\n                  queueEndpoints.update(key, endpoints)\n                }\n                .recover {\n                  case t =>\n                    logging.error(this, s\"Unexpected error$t\")\n                }\n            }\n          case `schedulerKey` =>\n            SchedulerStates\n              .parse(value)\n              .map { state =>\n                // if this is a new scheduler, warm up it\n                if (!schedulerEndpoints.contains(state.endpoints))\n                  warmUpScheduler(key, state.endpoints)\n                schedulerEndpoints.update(state.endpoints, state)\n              }\n              .recover {\n                case t =>\n                  logging.error(this, s\"Unexpected error$t\")\n              }\n          case `throttlingKey` =>\n            val throttled = Try {\n              value.toBoolean\n            }.getOrElse(false)\n            throttlers.update(key, throttled)\n          case _ =>\n        }\n    }\n  }))\n\n  private[loadBalancer] def getSchedulerEndpoint() = {\n    schedulerEndpoints\n  }\n\n  private val warmUpQueueCreationRequest =\n    ExecManifest.runtimesManifest\n      .resolveDefaultRuntime(\"nodejs:default\")\n      .map { manifest =>\n        val metadata = ExecutableWhiskActionMetaData(\n          WarmUp.warmUpAction.path,\n          WarmUp.warmUpAction.name,\n          CodeExecMetaDataAsString(manifest, false, entryPoint = None))\n        CreateQueue(\n          WarmUp.warmUpActionIdentity.namespace.name.asString,\n          WarmUp.warmUpAction,\n          DocRevision.empty,\n          metadata.toWhiskAction)\n      }\n\n  private def warmUpScheduler(schedulerName: String, scheduler: SchedulerEndpoints): Unit = {\n    implicit val transId = TransactionId.warmUp\n    logging.info(this, s\"Warm up scheduler $scheduler\")\n    sendActivationToKafka(\n      messageProducer,\n      WarmUp.warmUpActivation(controllerInstance),\n      Controller.topicPrefix + schedulerName.replace(s\"$clusterName/\", \"\").replace(\"/\", \"\")) // warm up kafka\n\n    warmUpQueueCreationRequest.foreach { request =>\n      scheduler.getRemoteRef(QueueManager.actorName).ask(request).mapTo[CreateQueueResponse].onComplete {\n        case _ => logging.info(this, s\"Warmed up scheduler $scheduler\")\n      }\n    }\n  }\n\n  protected def loadSchedulerEndpoint(): Unit = {\n    etcdClient\n      .getPrefix(SchedulerKeys.prefix)\n      .map { res =>\n        res.getKvsList.asScala.map { kv =>\n          val schedulerStates: String = kv.getValue\n          SchedulerStates\n            .parse(schedulerStates)\n            .map { state =>\n              // if this is a new scheduler, warm up it\n              if (!schedulerEndpoints.contains(state.endpoints))\n                warmUpScheduler(kv.getKey, state.endpoints)\n              schedulerEndpoints.update(state.endpoints, state)\n            }\n            .recover {\n              case t =>\n                logging.error(this, s\"Unexpected error$t\")\n            }\n        }\n      }\n  }\n\n  loadSchedulerEndpoint()\n\n  override def close(): Unit = {\n    watchedKeys.foreach { key =>\n      watcherService ! UnwatchEndpoint(key, true, watcherName)\n    }\n    activationFeed ! GracefulShutdown\n  }\n\n  /**\n   * Returns a message indicating the health of the containers and/or container pool in general.\n   *\n   * @return a Future[IndexedSeq[InvokerHealth]] representing the health of the pools managed by the loadbalancer.\n   **/\n  override def invokerHealth(): Future[IndexedSeq[InvokerHealth]] = {\n    etcdClient.getPrefix(s\"${InvokerKeys.prefix}/\").map { res =>\n      val healthsFromEtcd = res.getKvsList.asScala.map { kv =>\n        val (memory, busyMemory, status, tags, dedicatedNamespaces) = InvokerResourceMessage\n          .parse(kv.getValue.toString(StandardCharsets.UTF_8))\n          .map { resourceMessage =>\n            val status = resourceMessage.status match {\n              case Healthy.asString   => Healthy\n              case Unhealthy.asString => Unhealthy\n              case Offline.asString   => Offline\n            }\n            (\n              resourceMessage.freeMemory.MB,\n              resourceMessage.busyMemory.MB,\n              status,\n              resourceMessage.tags,\n              resourceMessage.dedicatedNamespaces)\n          }\n          .get\n        val temporalId = InvokerKeys.getInstanceId(kv.getKey.toString(StandardCharsets.UTF_8))\n        val invoker = temporalId.copy(\n          userMemory = memory,\n          busyMemory = Some(busyMemory),\n          tags = tags,\n          dedicatedNamespaces = dedicatedNamespaces)\n\n        new InvokerHealth(invoker, status)\n      }.toIndexedSeq\n      val missingHealths =\n        if (healthsFromEtcd.isEmpty) Set.empty[InvokerHealth]\n        else\n          ((healthsFromEtcd\n            .minBy(_.id.toInt)\n            .id\n            .toInt to healthsFromEtcd.maxBy(_.id.toInt).id.toInt).toSet -- healthsFromEtcd.map(_.id.toInt))\n            .map(id => new InvokerHealth(InvokerInstanceId(id, Some(id.toString), userMemory = 0 MB), Offline))\n      (healthsFromEtcd ++ missingHealths) sortBy (_.id.toInt)\n    }\n  }\n\n  def emitMetrics() = {\n    invokerHealth().map(invokers => {\n      MetricEmitter.emitGaugeMetric(HEALTHY_INVOKERS, invokers.count(_.status == Healthy))\n      MetricEmitter.emitGaugeMetric(UNHEALTHY_INVOKERS, invokers.count(_.status == Unhealthy))\n      MetricEmitter.emitGaugeMetric(OFFLINE_INVOKERS, invokers.count(_.status == Offline))\n      // Add both user memory and busy memory because user memory represents free memory in this case\n      MetricEmitter.emitGaugeMetric(INVOKER_TOTALMEM, invokers.foldLeft(0L) { (total, curr) =>\n        if (curr.status.isUsable) {\n          curr.id.userMemory.toMB + curr.id.busyMemory.getOrElse(ByteSize(0, SizeUnits.BYTE)).toMB + total\n        } else {\n          total\n        }\n      })\n      MetricEmitter.emitGaugeMetric(LOADBALANCER_ACTIVATIONS_INFLIGHT(controllerInstance), totalActivations.longValue)\n      MetricEmitter\n        .emitGaugeMetric(LOADBALANCER_MEMORY_INFLIGHT(controllerInstance, \"\"), totalActivationMemory.longValue)\n    })\n  }\n\n  actorSystem.scheduler.scheduleAtFixedRate(10.seconds, 10.seconds)(() => emitMetrics())\n\n  /** Gets the number of in-flight activations for a specific user. */\n  override def activeActivationsFor(namespace: UUID): Future[Int] =\n    Future.successful(activationsPerNamespace.get(namespace).map(_.intValue()).getOrElse(0))\n\n  /** Gets the number of in-flight activations for a specific controller. */\n  override def activeActivationsByController(controller: String): Future[Int] =\n    Future.successful(activationsPerController.get(ControllerInstanceId(controller)).map(_.intValue()).getOrElse(0))\n\n  /** Gets the in-flight activations */\n  override def activeActivationsByController: Future[List[(String, String)]] =\n    Future.successful(\n      activationSlots.values.map(entry => (entry.id.asString, entry.fullyQualifiedEntityName.toString)).toList)\n\n  /** Gets the number of in-flight activations for a specific invoker. */\n  override def activeActivationsByInvoker(invoker: String): Future[Int] = Future.successful(0)\n\n  /** Gets the number of in-flight activations in the system. */\n  override def totalActiveActivations: Future[Int] = Future.successful(totalActivations.intValue())\n\n  /** Gets the throttling for given action. */\n  override def checkThrottle(namespace: EntityPath, fqn: String): Boolean = {\n\n    /**\n     * Note! The throttle key is assumed to exist unconditionally and is not limited to throttle if not present.\n     *\n     * Action Throttle true      -> 429\n     *                 false     -> Pass (wait 429, Container already exist)\n     *                 not exist -> Namespace Throttled true      -> 429 (Cannot create more containers)\n     *                                                  false     -> Pass\n     *                                                  not exist -> Pass\n     */\n    throttlers.getOrElse(\n      ThrottlingKeys.action(namespace.namespace, fqn),\n      throttlers.getOrElse(ThrottlingKeys.namespace(namespace.root), false))\n  }\n}\n\nobject FPCPoolBalancer extends LoadBalancerProvider {\n\n  override def instance(whiskConfig: WhiskConfig, instance: ControllerInstanceId)(implicit actorSystem: ActorSystem,\n                                                                                  logging: Logging): LoadBalancer = {\n\n    implicit val exe: ExecutionContextExecutor = actorSystem.dispatcher\n    val activeAckTopic = s\"${Controller.topicPrefix}completed${instance.asString}\"\n    val maxActiveAcksPerPoll = 128\n    val activeAckPollDuration = 1.second\n\n    val feedFactory = new FeedFactory {\n      def createFeed(f: ActorRefFactory, provider: MessagingProvider, acker: Array[Byte] => Future[Unit]): ActorRef = {\n        f.actorOf(Props {\n          new MessageFeed(\n            \"activeack\",\n            logging,\n            provider.getConsumer(whiskConfig, activeAckTopic, activeAckTopic, maxPeek = maxActiveAcksPerPoll),\n            maxActiveAcksPerPoll,\n            activeAckPollDuration,\n            acker)\n        })\n      }\n    }\n\n    val etcd = EtcdClient(loadConfigOrThrow[EtcdConfig](ConfigKeys.etcd))\n\n    new FPCPoolBalancer(whiskConfig, instance, etcd, feedFactory)\n  }\n\n  def requiredProperties: Map[String, String] = WhiskConfig.kafkaHosts\n\n  // TODO modularize rng algorithm\n  /**\n   * The rng algorithm is responsible for the invoker distribution, and the better the distribution, the smaller the number of rescheduling.\n   *\n   */\n  def rng(mod: Int): Int = ThreadLocalRandom.current().nextInt(mod)\n\n  /**\n   * Assign a scheduler to a message, return the scheduler which has least queues\n   *\n   * @param schedulers Scheduler pool\n   * @param excludeSchedulers schedulers which should not be chose\n   * @return Assigned a scheduler\n   */\n  def schedule(schedulers: IndexedSeq[SchedulerStates],\n               excludeSchedulers: Set[SchedulerInstanceId] = Set.empty): Option[SchedulerStates] = {\n    schedulers.filter(scheduler => !excludeSchedulers.contains(scheduler.sid)).sortBy(_.queueSize).headOption\n  }\n\n}\n\n/**\n * State kept for each activation slot until completion.\n *\n * @param id id of the activation\n * @param namespaceId namespace that invoked the action\n * @param timeoutHandler times out completion of this activation, should be canceled on good paths\n */\ncase class DistributedActivationEntry(id: ActivationId,\n                                      namespaceId: UUID,\n                                      invocationNamespace: String,\n                                      revision: DocRevision,\n                                      transactionId: TransactionId,\n                                      memory: ByteSize,\n                                      maxConcurrent: Int,\n                                      fullyQualifiedEntityName: FullyQualifiedEntityName,\n                                      timeoutHandler: Cancellable,\n                                      isBlackbox: Boolean,\n                                      isBlocking: Boolean,\n                                      controllerName: ControllerInstanceId = ControllerInstanceId(\"0\"))\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/FeedFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.pekko.actor.ActorRefFactory\nimport org.apache.openwhisk.core.connector.MessagingProvider\nimport scala.concurrent.Future\n\ntrait FeedFactory {\n  def createFeed(actorRefFactory: ActorRefFactory,\n                 messagingProvider: MessagingProvider,\n                 messageHandler: Array[Byte] => Future[Unit]): ActorRef\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/InvokerPoolFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.pekko.actor.ActorRefFactory\nimport org.apache.openwhisk.core.connector.{ActivationMessage, MessageProducer, MessagingProvider, ResultMetadata}\nimport org.apache.openwhisk.core.entity.InvokerInstanceId\n\nimport scala.concurrent.Future\n\ntrait InvokerPoolFactory {\n  def createInvokerPool(\n    actorRefFactory: ActorRefFactory,\n    messagingProvider: MessagingProvider,\n    messagingProducer: MessageProducer,\n    sendActivationToInvoker: (MessageProducer, ActivationMessage, InvokerInstanceId) => Future[ResultMetadata],\n    monitor: Option[ActorRef]): ActorRef\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/InvokerSupervision.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer\n\nimport java.nio.charset.StandardCharsets\n\nimport scala.collection.immutable\nimport scala.concurrent.{Await, ExecutionContext, Future}\nimport scala.concurrent.duration._\nimport scala.util.Failure\nimport scala.util.Success\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, FSM, Props}\nimport org.apache.pekko.actor.FSM.CurrentState\nimport org.apache.pekko.actor.FSM.SubscribeTransitionCallBack\nimport org.apache.pekko.actor.FSM.Transition\nimport org.apache.pekko.pattern.pipe\nimport org.apache.pekko.util.Timeout\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.database.NoDocumentException\nimport org.apache.openwhisk.core.entity.ActivationId.ActivationIdGenerator\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types.EntityStore\n\n// Received events\ncase object GetStatus\n\ncase object Tick\n\n// Possible answers of an activation\nsealed trait InvocationFinishedResult\nobject InvocationFinishedResult {\n  // The activation could be successfully executed from the system's point of view. That includes user- and application\n  // errors\n  case object Success extends InvocationFinishedResult\n  // The activation could not be executed because of a system error\n  case object SystemError extends InvocationFinishedResult\n  // The active-ack did not arrive before it timed out\n  case object Timeout extends InvocationFinishedResult\n}\n\ncase class ActivationRequest(msg: ActivationMessage, invoker: InvokerInstanceId)\ncase class InvocationFinishedMessage(invokerInstance: InvokerInstanceId, result: InvocationFinishedResult)\n\n// Sent to a monitor if the state changed\ncase class CurrentInvokerPoolState(newState: IndexedSeq[InvokerHealth])\n\n// Data stored in the Invoker\nfinal case class InvokerInfo(buffer: RingBuffer[InvocationFinishedResult])\n\n/**\n * Actor representing a pool of invokers\n *\n * The InvokerPool manages a Invokers through subactors. An new Invoker\n * is registered lazily by sending it a Ping event with the name of the\n * Invoker. Ping events are furthermore forwarded to the respective\n * Invoker for their respective State handling.\n *\n * Note: An Invoker that never sends an initial Ping will not be considered\n * by the InvokerPool and thus might not be caught by monitoring.\n */\nclass InvokerPool(childFactory: (ActorRefFactory, InvokerInstanceId) => ActorRef,\n                  sendActivationToInvoker: (ActivationMessage, InvokerInstanceId) => Future[ResultMetadata],\n                  pingConsumer: MessageConsumer,\n                  monitor: Option[ActorRef])\n    extends Actor {\n\n  import InvokerState._\n\n  implicit val transid: TransactionId = TransactionId.invokerHealth\n  implicit val logging: Logging = new PekkoLogging(context.system.log)\n  implicit val timeout: Timeout = Timeout(5.seconds)\n  implicit val ec: ExecutionContext = context.dispatcher\n\n  // State of the actor. Mutable vars with immutable collections prevents closures or messages\n  // from leaking the state for external mutation\n  var instanceToRef = immutable.Map.empty[Int, ActorRef]\n  var refToInstance = immutable.Map.empty[ActorRef, InvokerInstanceId]\n  var status = IndexedSeq[InvokerHealth]()\n\n  def receive: Receive = {\n    case p: PingMessage =>\n      val invoker = instanceToRef.getOrElse(p.instance.toInt, registerInvoker(p.instance))\n      instanceToRef = instanceToRef.updated(p.instance.toInt, invoker)\n\n      // For the case when the invoker was restarted and got a new displayed name\n      val oldHealth = status(p.instance.toInt)\n      if (oldHealth.id != p.instance) {\n        status = status.updated(p.instance.toInt, new InvokerHealth(p.instance, oldHealth.status))\n        refToInstance = refToInstance.updated(invoker, p.instance)\n      }\n\n      invoker.forward(p)\n\n    case GetStatus => sender() ! status\n\n    case msg: InvocationFinishedMessage =>\n      // Forward message to invoker, if InvokerActor exists\n      instanceToRef.get(msg.invokerInstance.toInt).foreach(_.forward(msg))\n\n    case CurrentState(invoker, currentState: InvokerState) =>\n      refToInstance.get(invoker).foreach { instance =>\n        status = status.updated(instance.toInt, new InvokerHealth(instance, currentState))\n      }\n      logStatus()\n\n    case Transition(invoker, oldState: InvokerState, newState: InvokerState) =>\n      refToInstance.get(invoker).foreach { instance =>\n        status = status.updated(instance.toInt, new InvokerHealth(instance, newState))\n      }\n      logStatus()\n\n    // this is only used for the internal test action which enabled an invoker to become healthy again\n    case msg: ActivationRequest => sendActivationToInvoker(msg.msg, msg.invoker).pipeTo(sender)\n  }\n\n  def logStatus(): Unit = {\n    monitor.foreach(_ ! CurrentInvokerPoolState(status))\n    val pretty = status.map(i => s\"${i.id.toInt} -> ${i.status}\")\n    logging.info(this, s\"invoker status changed to ${pretty.mkString(\", \")}\")\n  }\n\n  /** Receive Ping messages from invokers. */\n  val pingPollDuration: FiniteDuration = 1.second\n  val invokerPingFeed: ActorRef = context.system.actorOf(Props {\n    new MessageFeed(\n      \"ping\",\n      logging,\n      pingConsumer,\n      pingConsumer.maxPeek,\n      pingPollDuration,\n      processInvokerPing,\n      logHandoff = false)\n  })\n\n  def processInvokerPing(bytes: Array[Byte]): Future[Unit] = Future {\n    val raw = new String(bytes, StandardCharsets.UTF_8)\n    PingMessage.parse(raw) match {\n      case Success(p: PingMessage) =>\n        self ! p\n        invokerPingFeed ! MessageFeed.Processed\n\n      case Failure(t) =>\n        invokerPingFeed ! MessageFeed.Processed\n        logging.error(this, s\"failed processing message: $raw with $t\")\n    }\n  }\n\n  /** Pads a list to a given length using the given function to compute entries */\n  def padToIndexed[A](list: IndexedSeq[A], n: Int, f: (Int) => A): IndexedSeq[A] = list ++ (list.size until n).map(f)\n\n  // Register a new invoker\n  def registerInvoker(instanceId: InvokerInstanceId): ActorRef = {\n    logging.info(this, s\"registered a new invoker: invoker${instanceId.toInt}\")(TransactionId.invokerHealth)\n\n    // Grow the underlying status sequence to the size needed to contain the incoming ping. Dummy values are created\n    // to represent invokers, where ping messages haven't arrived yet\n    status = padToIndexed(\n      status,\n      instanceId.toInt + 1,\n      i => new InvokerHealth(InvokerInstanceId(i, userMemory = instanceId.userMemory), Offline))\n    status = status.updated(instanceId.toInt, new InvokerHealth(instanceId, Offline))\n\n    val ref = childFactory(context, instanceId)\n    ref ! SubscribeTransitionCallBack(self) // register for state change events\n    refToInstance = refToInstance.updated(ref, instanceId)\n\n    ref\n  }\n\n}\n\nobject InvokerPool {\n  private def createTestActionForInvokerHealth(db: EntityStore, action: WhiskAction): Future[Unit] = {\n    implicit val tid: TransactionId = TransactionId.loadbalancer\n    implicit val ec: ExecutionContext = db.executionContext\n    implicit val logging: Logging = db.logging\n\n    WhiskAction\n      .get(db, action.docid)\n      .flatMap { oldAction =>\n        WhiskAction.put(db, action.revision(oldAction.rev), Some(oldAction))(tid, notifier = None)\n      }\n      .recover {\n        case _: NoDocumentException => WhiskAction.put(db, action, old = None)(tid, notifier = None)\n      }\n      .map(_ => {})\n      .andThen {\n        case Success(_) => logging.info(this, \"test action for invoker health now exists\")\n        case Failure(e) => logging.error(this, s\"error creating test action for invoker health: $e\")\n      }\n  }\n\n  /**\n   * Prepares everything for the health protocol to work (i.e. creates a testaction)\n   *\n   * @param controllerInstance instance of the controller we run in\n   * @param entityStore store to write the action to\n   * @return throws an exception on failure to prepare\n   */\n  def prepare(controllerInstance: ControllerInstanceId, entityStore: EntityStore): Unit = {\n    InvokerPool\n      .healthAction(controllerInstance)\n      .map {\n        // Await the creation of the test action; on failure, this will abort the constructor which should\n        // in turn abort the startup of the controller.\n        a =>\n          Await.result(createTestActionForInvokerHealth(entityStore, a), 1.minute)\n      }\n      .orElse {\n        throw new IllegalStateException(\n          \"cannot create test action for invoker health because runtime manifest is not valid\")\n      }\n  }\n\n  def props(f: (ActorRefFactory, InvokerInstanceId) => ActorRef,\n            p: (ActivationMessage, InvokerInstanceId) => Future[ResultMetadata],\n            pc: MessageConsumer,\n            m: Option[ActorRef] = None): Props = {\n    Props(new InvokerPool(f, p, pc, m))\n  }\n\n  /** A stub identity for invoking the test action. This does not need to be a valid identity. */\n  val healthActionIdentity: Identity = {\n    val whiskSystem = \"whisk.system\"\n    val uuid = UUID()\n    Identity(Subject(whiskSystem), Namespace(EntityName(whiskSystem), uuid), BasicAuthenticationAuthKey(uuid, Secret()))\n  }\n\n  /** An action to use for monitoring invoker health. */\n  def healthAction(i: ControllerInstanceId): Option[WhiskAction] =\n    ExecManifest.runtimesManifest.resolveDefaultRuntime(\"nodejs:default\").map { manifest =>\n      new WhiskAction(\n        namespace = healthActionIdentity.namespace.name.toPath,\n        name = EntityName(s\"invokerHealthTestAction${i.asString}\"),\n        exec = CodeExecAsString(manifest, \"\"\"function main(params) { return params; }\"\"\", None),\n        limits = ActionLimits(memory = MemoryLimit(MemoryLimit.MIN_MEMORY)))\n    }\n}\n\n/**\n * Actor representing an Invoker\n *\n * This finite state-machine represents an Invoker in its possible\n * states \"Healthy\" and \"Offline\".\n */\nclass InvokerActor(invokerInstance: InvokerInstanceId, controllerInstance: ControllerInstanceId)\n    extends FSM[InvokerState, InvokerInfo] {\n\n  import InvokerState._\n\n  implicit val transid: TransactionId = TransactionId.invokerHealth\n  implicit val logging: Logging = new PekkoLogging(context.system.log)\n  val name = s\"invoker${invokerInstance.toInt}\"\n\n  val healthyTimeout: FiniteDuration = 10.seconds\n\n  // This is done at this point to not intermingle with the state-machine especially their timeouts.\n  def customReceive: Receive = {\n    case _: ResultMetadata => // Ignores the result of publishing test actions to MessageProducer.\n  }\n\n  override def receive: Receive = customReceive.orElse(super.receive)\n\n  // To be used for all states that should send test actions to reverify the invoker\n  val healthPingingState: StateFunction = {\n    case Event(ping: PingMessage, _) => goOfflineIfDisabled(ping)\n    case Event(StateTimeout, _)      => goto(Offline)\n    case Event(Tick, _) =>\n      invokeTestAction()\n      stay\n  }\n\n  // To be used for all states that should send test actions to reverify the invoker\n  def healthPingingTransitionHandler(state: InvokerState): TransitionHandler = {\n    case _ -> `state` =>\n      invokeTestAction()\n      startTimerAtFixedRate(InvokerActor.timerName, Tick, 1.minute)\n    case `state` -> _ => cancelTimer(InvokerActor.timerName)\n  }\n\n  /** Always start UnHealthy. Then the invoker receives some test activations and becomes Healthy. */\n  startWith(Unhealthy, InvokerInfo(new RingBuffer[InvocationFinishedResult](InvokerActor.bufferSize)))\n\n  /** An Offline invoker represents an existing but broken invoker. This means, that it does not send pings anymore. */\n  when(Offline) {\n    case Event(ping: PingMessage, _) => if (ping.invokerEnabled) goto(Unhealthy) else stay\n  }\n\n  /** An Unhealthy invoker represents an invoker that was not able to handle actions successfully. */\n  when(Unhealthy, stateTimeout = healthyTimeout)(healthPingingState)\n\n  /** An Unresponsive invoker represents an invoker that is not responding with active acks in a timely manner */\n  when(Unresponsive, stateTimeout = healthyTimeout)(healthPingingState)\n\n  /**\n   * A Healthy invoker is characterized by continuously getting pings.\n   * It will go offline if that state is not confirmed for 20 seconds.\n   */\n  when(Healthy, stateTimeout = healthyTimeout) {\n    case Event(ping: PingMessage, _) => goOfflineIfDisabled(ping)\n    case Event(StateTimeout, _)      => goto(Offline)\n  }\n\n  /** Handles the completion of an Activation in every state. */\n  whenUnhandled {\n    case Event(cm: InvocationFinishedMessage, info) => handleCompletionMessage(cm.result, info.buffer)\n  }\n\n  /** Logs transition changes. */\n  onTransition {\n    case _ -> newState if !newState.isUsable =>\n      transid.mark(\n        this,\n        LoggingMarkers.LOADBALANCER_INVOKER_STATUS_CHANGE(newState.asString),\n        s\"$name is ${newState.asString}\",\n        org.apache.pekko.event.Logging.WarningLevel)\n    case _ -> newState if newState.isUsable => logging.info(this, s\"$name is ${newState.asString}\")\n  }\n\n  onTransition(healthPingingTransitionHandler(Unhealthy))\n  onTransition(healthPingingTransitionHandler(Unresponsive))\n\n  initialize()\n\n  /**\n   * Handling for if a ping message from an invoker signals that it has been disabled to immediately transition to Offline.\n   *\n   * @param ping\n   * @return\n   */\n  private def goOfflineIfDisabled(ping: PingMessage) = {\n    if (ping.invokerEnabled) stay else goto(Offline)\n  }\n\n  /**\n   * Handling for active acks. This method saves the result (successful or unsuccessful)\n   * into an RingBuffer and checks, if the InvokerActor has to be changed to UnHealthy.\n   *\n   * @param result: result of Activation\n   * @param buffer to be used\n   */\n  private def handleCompletionMessage(result: InvocationFinishedResult,\n                                      buffer: RingBuffer[InvocationFinishedResult]) = {\n    buffer.add(result)\n\n    // If the action is successful, the Invoker is Healthy. We execute additional test actions\n    // immediately to clear the RingBuffer as fast as possible.\n    // The actions that arrive while the invoker is unhealthy are most likely health actions.\n    // It is possible they are normal user actions as well. This can happen if such actions were in the\n    // invoker queue or in progress while the invoker's status flipped to Unhealthy.\n    if (result == InvocationFinishedResult.Success && stateName == Unhealthy) {\n      invokeTestAction()\n    }\n\n    // Stay online if the activations was successful.\n    // Stay offline if an activeAck is received (a stale activation) but the invoker ceased pinging.\n    if ((stateName == Healthy && result == InvocationFinishedResult.Success) || stateName == Offline) {\n      stay\n    } else {\n      val entries = buffer.toList\n\n      // Goto Unhealthy or Unresponsive respectively if there are more errors than accepted in buffer at steady state.\n      // Otherwise transition to Healthy on successful activations only.\n      if (entries.count(_ == InvocationFinishedResult.SystemError) > InvokerActor.bufferErrorTolerance) {\n        // Note: The predicate is false if the ring buffer is still being primed\n        // (i.e., the entries.size <=  bufferErrorTolerance).\n        gotoIfNotThere(Unhealthy)\n      } else if (entries.count(_ == InvocationFinishedResult.Timeout) > InvokerActor.bufferErrorTolerance) {\n        // Note: The predicate is false if the ring buffer is still being primed\n        // (i.e., the entries.size <=  bufferErrorTolerance).\n        gotoIfNotThere(Unresponsive)\n      } else {\n        result match {\n          case InvocationFinishedResult.Success =>\n            // Eagerly transition to healthy, at steady state (there aren't sufficient contra-indications) or\n            // during priming of the ring buffer. In case of the latter, there is at least one additional test\n            // action in flight which can reverse the transition later.\n            gotoIfNotThere(Healthy)\n\n          case InvocationFinishedResult.SystemError if (entries.size <= InvokerActor.bufferErrorTolerance) =>\n            // The ring buffer is not fully primed yet, stay/goto Unhealthy.\n            gotoIfNotThere(Unhealthy)\n\n          case InvocationFinishedResult.Timeout if (entries.size <= InvokerActor.bufferErrorTolerance) =>\n            // The ring buffer is not fully primed yet, stay/goto Unresponsive.\n            gotoIfNotThere(Unresponsive)\n\n          case _ =>\n            // At steady state, the state of the buffer superceded and we hold the current state\n            // until enough events have occurred to transition to a new state.\n            stay\n        }\n      }\n    }\n  }\n\n  /**\n   * Creates an activation request with the given action and sends it to the InvokerPool.\n   * The InvokerPool redirects it to the invoker which is represented by this InvokerActor.\n   */\n  private def invokeTestAction() = {\n    InvokerPool.healthAction(controllerInstance).map { action =>\n      val activationMessage = ActivationMessage(\n        // Use the sid of the InvokerSupervisor as tid\n        transid = transid,\n        action = action.fullyQualifiedName(true),\n        // Use empty DocRevision to force the invoker to pull the action from db all the time\n        revision = DocRevision.empty,\n        user = InvokerPool.healthActionIdentity,\n        // Create a new Activation ID for this activation\n        activationId = new ActivationIdGenerator {}.make(),\n        rootControllerIndex = controllerInstance,\n        blocking = false,\n        content = None,\n        initArgs = Set.empty,\n        lockedArgs = Map.empty)\n\n      context.parent ! ActivationRequest(activationMessage, invokerInstance)\n    }\n  }\n\n  /**\n   * Only change the state if the currentState is not the newState.\n   *\n   * @param newState of the InvokerActor\n   */\n  private def gotoIfNotThere(newState: InvokerState) = {\n    if (stateName == newState) stay() else goto(newState)\n  }\n}\n\nobject InvokerActor {\n  def props(invokerInstance: InvokerInstanceId, controllerInstance: ControllerInstanceId) =\n    Props(new InvokerActor(invokerInstance, controllerInstance))\n\n  val bufferSize = 10\n  val bufferErrorTolerance = 3\n\n  val timerName = \"testActionTimer\"\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/LeanBalancer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer\n\nimport org.apache.pekko.actor.{ActorRef, ActorSystem, Props}\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.WhiskConfig._\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.containerpool.ContainerPoolConfig\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.invoker.InvokerProvider\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.spi.SpiLoader\nimport org.apache.openwhisk.utils.ExecutionContextFactory\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.core.entity.size._\n\nimport scala.concurrent.Future\n\n/**\n * Lean loadbalancer implemetation.\n *\n * Communicates with Invoker directly without Kafka in the middle. Invoker does not exist as a separate entity, it is built together with Controller\n * Uses LeanMessagingProvider to use in-memory queue instead of Kafka\n */\nclass LeanBalancer(config: WhiskConfig,\n                   feedFactory: FeedFactory,\n                   controllerInstance: ControllerInstanceId,\n                   implicit val messagingProvider: MessagingProvider = SpiLoader.get[MessagingProvider])(\n  implicit actorSystem: ActorSystem,\n  logging: Logging)\n    extends CommonLoadBalancer(config, feedFactory, controllerInstance) {\n\n  /** Loadbalancer interface methods */\n  override def invokerHealth(): Future[IndexedSeq[InvokerHealth]] = Future.successful(IndexedSeq.empty[InvokerHealth])\n  override def clusterSize: Int = 1\n\n  val poolConfig: ContainerPoolConfig = loadConfigOrThrow[ContainerPoolConfig](ConfigKeys.containerPool)\n\n  val invokerName = InvokerInstanceId(0, None, None, poolConfig.userMemory)\n\n  /** 1. Publish a message to the loadbalancer */\n  override def publish(action: ExecutableWhiskActionMetaData, msg: ActivationMessage)(\n    implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]] = {\n\n    /** 2. Update local state with the activation to be executed scheduled. */\n    val activationResult = setupActivation(msg, action, invokerName)\n    sendActivationToInvoker(messageProducer, msg, invokerName).map(_ => activationResult)\n  }\n\n  /** Creates an invoker for executing user actions. There is only one invoker in the lean model. */\n  private def makeALocalThreadedInvoker(): Unit = {\n    implicit val ec = ExecutionContextFactory.makeCachedThreadPoolExecutionContext()\n    val limitConfig: IntraConcurrencyLimitConfig =\n      loadConfigOrThrow[IntraConcurrencyLimitConfig](ConfigKeys.concurrencyLimit)\n    SpiLoader.get[InvokerProvider].instance(config, invokerName, messageProducer, poolConfig, limitConfig)\n  }\n\n  makeALocalThreadedInvoker()\n\n  override protected val invokerPool: ActorRef = actorSystem.actorOf(Props.empty)\n\n  override protected def releaseInvoker(invoker: InvokerInstanceId, entry: ActivationEntry) = {\n    // Currently do nothing\n  }\n\n  override protected def emitMetrics() = {\n    super.emitMetrics()\n  }\n}\n\nobject LeanBalancer extends LoadBalancerProvider {\n\n  override def instance(whiskConfig: WhiskConfig, instance: ControllerInstanceId)(implicit actorSystem: ActorSystem,\n                                                                                  logging: Logging): LoadBalancer = {\n\n    new LeanBalancer(whiskConfig, createFeedFactory(whiskConfig, instance), instance)\n  }\n\n  def requiredProperties =\n    ExecManifest.requiredProperties ++\n      wskApiHost\n}\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/LoadBalancer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer\n\nimport org.apache.pekko.actor.{ActorRefFactory, ActorSystem, Props}\nimport org.apache.openwhisk.common.{InvokerHealth, Logging, TransactionId}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.controller.Controller\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.spi.Spi\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\n\ntrait LoadBalancer {\n\n  /**\n   * Publishes activation message on internal bus for an invoker to pick up.\n   *\n   * @param action the action to invoke\n   * @param msg the activation message to publish on an invoker topic\n   * @param transid the transaction id for the request\n   * @return result a nested Future the outer indicating completion of publishing and\n   *         the inner the completion of the action (i.e., the result)\n   *         if it is ready before timeout (Right) otherwise the activation id (Left).\n   *         The future is guaranteed to complete within the declared action time limit\n   *         plus a grace period (see activeAckTimeoutGrace).\n   */\n  def publish(action: ExecutableWhiskActionMetaData, msg: ActivationMessage)(\n    implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]]\n\n  /**\n   * Returns a message indicating the health of the containers and/or container pool in general.\n   *\n   * @return a Future[IndexedSeq[InvokerHealth]] representing the health of the pools managed by the loadbalancer.\n   */\n  def invokerHealth(): Future[IndexedSeq[InvokerHealth]]\n\n  /** Gets the number of in-flight activations for a specific user. */\n  def activeActivationsFor(namespace: UUID): Future[Int]\n\n  /** Gets the number of in-flight activations for a specific controller. */\n  def activeActivationsByController(controller: String): Future[Int]\n\n  /** Gets the in-flight activations */\n  def activeActivationsByController: Future[List[(String, String)]]\n\n  /** Gets the number of in-flight activations for a specific invoker. */\n  def activeActivationsByInvoker(invoker: String): Future[Int]\n\n  /** Gets the number of in-flight activations in the system. */\n  def totalActiveActivations: Future[Int]\n\n  /** Gets the size of the cluster all loadbalancers are acting in */\n  def clusterSize: Int = 1\n\n  /** Gets the throttling for given action. */\n  def checkThrottle(namespace: EntityPath, action: String): Boolean = false\n\n  /** Close the load balancer */\n  def close: Unit = {}\n}\n\n/**\n * An Spi for providing load balancer implementations.\n */\ntrait LoadBalancerProvider extends Spi {\n  def requiredProperties: Map[String, String]\n\n  def instance(whiskConfig: WhiskConfig, instance: ControllerInstanceId)(implicit actorSystem: ActorSystem,\n                                                                         logging: Logging): LoadBalancer\n\n  /** Return default FeedFactory */\n  def createFeedFactory(whiskConfig: WhiskConfig, instance: ControllerInstanceId)(implicit actorSystem: ActorSystem,\n                                                                                  logging: Logging): FeedFactory = {\n\n    val activeAckTopic = s\"${Controller.topicPrefix}completed${instance.asString}\"\n    val maxActiveAcksPerPoll = 128\n    val activeAckPollDuration = 1.second\n\n    new FeedFactory {\n      def createFeed(f: ActorRefFactory, provider: MessagingProvider, acker: Array[Byte] => Future[Unit]) = {\n        f.actorOf(Props {\n          new MessageFeed(\n            \"activeack\",\n            logging,\n            provider.getConsumer(whiskConfig, activeAckTopic, activeAckTopic, maxPeek = maxActiveAcksPerPoll),\n            maxActiveAcksPerPoll,\n            activeAckPollDuration,\n            acker)\n        })\n      }\n    }\n  }\n}\n\n/** Exception thrown by the loadbalancer */\ncase class LoadBalancerException(msg: String) extends Throwable(msg)\n"
  },
  {
    "path": "core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/ShardingContainerPoolBalancer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer\n\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.pekko.actor.ActorRefFactory\nimport java.util.concurrent.ThreadLocalRandom\n\nimport org.apache.pekko.actor.{Actor, ActorSystem, Cancellable, Props}\nimport org.apache.pekko.cluster.ClusterEvent._\nimport org.apache.pekko.cluster.{Cluster, Member, MemberStatus}\nimport org.apache.pekko.management.scaladsl.PekkoManagement\nimport org.apache.pekko.management.cluster.bootstrap.ClusterBootstrap\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Offline, Unhealthy, Unresponsive}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.WhiskConfig._\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size.SizeLong\nimport org.apache.openwhisk.common.LoggingMarkers._\nimport org.apache.openwhisk.core.controller.Controller\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.spi.SpiLoader\n\nimport scala.annotation.tailrec\nimport scala.concurrent.Future\nimport scala.concurrent.duration.FiniteDuration\n\n/**\n * A loadbalancer that schedules workload based on a hashing-algorithm.\n *\n * ## Algorithm\n *\n * At first, for every namespace + action pair a hash is calculated and then an invoker is picked based on that hash\n * (`hash % numInvokers`). The determined index is the so called \"home-invoker\". This is the invoker where the following\n * progression will **always** start. If this invoker is healthy (see \"Invoker health checking\") and if there is\n * capacity on that invoker (see \"Capacity checking\"), the request is scheduled to it.\n *\n * If one of these prerequisites is not true, the index is incremented by a step-size. The step-sizes available are the\n * all coprime numbers smaller than the amount of invokers available (coprime, to minimize collisions while progressing\n * through the invokers). The step-size is picked by the same hash calculated above (`hash & numStepSizes`). The\n * home-invoker-index is now incremented by the step-size and the checks (healthy + capacity) are done on the invoker\n * we land on now.\n *\n * This procedure is repeated until all invokers have been checked at which point the \"overload\" strategy will be\n * employed, which is to choose a healthy invoker randomly. In a steadily running system, that overload means that there\n * is no capacity on any invoker left to schedule the current request to.\n *\n * If no invokers are available or if there are no healthy invokers in the system, the loadbalancer will return an error\n * stating that no invokers are available to take any work. Requests are not queued anywhere in this case.\n *\n * An example:\n * - availableInvokers: 10 (all healthy)\n * - hash: 13\n * - homeInvoker: hash % availableInvokers = 13 % 10 = 3\n * - stepSizes: 1, 3, 7 (note how 2 and 5 is not part of this because it's not coprime to 10)\n * - stepSizeIndex: hash % numStepSizes = 13 % 3 = 1 => stepSize = 3\n *\n * Progression to check the invokers: 3, 6, 9, 2, 5, 8, 1, 4, 7, 0 --> done\n *\n * This heuristic is based on the assumption, that the chance to get a warm container is the best on the home invoker\n * and degrades the more steps you make. The hashing makes sure that all loadbalancers in a cluster will always pick the\n * same home invoker and do the same progression for a given action.\n *\n * Known caveats:\n * - This assumption is not always true. For instance, two heavy workloads landing on the same invoker can override each\n *   other, which results in many cold starts due to all containers being evicted by the invoker to make space for the\n *   \"other\" workload respectively. Future work could be to keep a buffer of invokers last scheduled for each action and\n *   to prefer to pick that one. Then the second-last one and so forth.\n *\n * ## Capacity checking\n *\n * The maximum capacity per invoker is configured using `user-memory`, which is the maximum amount of memory of actions\n * running in parallel on that invoker.\n *\n * Spare capacity is determined by what the loadbalancer thinks it scheduled to each invoker. Upon scheduling, an entry\n * is made to update the books and a slot for each MB of the actions memory limit in a Semaphore is taken. These slots\n * are only released after the response from the invoker (active-ack) arrives **or** after the active-ack times out.\n * The Semaphore has as many slots as MBs are configured in `user-memory`.\n *\n * Known caveats:\n * - In an overload scenario, activations are queued directly to the invokers, which makes the active-ack timeout\n *   unpredictable. Timing out active-acks in that case can cause the loadbalancer to prematurely assign new load to an\n *   overloaded invoker, which can cause uneven queues.\n * - The same is true if an invoker is extraordinarily slow in processing activations. The queue on this invoker will\n *   slowly rise if it gets slow to the point of still sending pings, but handling the load so slowly, that the\n *   active-acks time out. The loadbalancer again will think there is capacity, when there is none.\n *\n * Both caveats could be solved in future work by not queueing to invoker topics on overload, but to queue on a\n * centralized overflow topic. Timing out an active-ack can then be seen as a system-error, as described in the\n * following.\n *\n * ## Invoker health checking\n *\n * Invoker health is determined via a kafka-based protocol, where each invoker pings the loadbalancer every second. If\n * no ping is seen for a defined amount of time, the invoker is considered \"Offline\".\n *\n * Moreover, results from all activations are inspected. If more than 3 out of the last 10 activations contained system\n * errors, the invoker is considered \"Unhealthy\". If an invoker is unhealthy, no user workload is sent to it, but\n * test-actions are sent by the loadbalancer to check if system errors are still happening. If the\n * system-error-threshold-count in the last 10 activations falls below 3, the invoker is considered \"Healthy\" again.\n *\n * To summarize:\n * - \"Offline\": Ping missing for > 10 seconds\n * - \"Unhealthy\": > 3 **system-errors** in the last 10 activations, pings arriving as usual\n * - \"Healthy\": < 3 **system-errors** in the last 10 activations, pings arriving as usual\n *\n * ## Horizontal sharding\n *\n * Sharding is employed to avoid both loadbalancers having to share any data, because the metrics used in scheduling\n * are very fast changing.\n *\n * Horizontal sharding means, that each invoker's capacity is evenly divided between the loadbalancers. If an invoker\n * has at most 16 slots available (invoker-busy-threshold = 16), those will be divided to 8 slots for each loadbalancer\n * (if there are 2).\n *\n * If concurrent activation processing is enabled (and concurrency limit is > 1), accounting of containers and\n * concurrency capacity per container will limit the number of concurrent activations routed to the particular\n * slot at an invoker. Default max concurrency is 1.\n *\n * Known caveats:\n * - If a loadbalancer leaves or joins the cluster, all state is removed and created from scratch. Those events should\n *   not happen often.\n * - If concurrent activation processing is enabled, it only accounts for the containers that the current loadbalancer knows.\n *   So the actual number of containers launched at the invoker may be less than is counted at the loadbalancer, since\n *   the invoker may skip container launch in case there is concurrent capacity available for a container launched via\n *   some other loadbalancer.\n */\nclass ShardingContainerPoolBalancer(\n  config: WhiskConfig,\n  controllerInstance: ControllerInstanceId,\n  feedFactory: FeedFactory,\n  val invokerPoolFactory: InvokerPoolFactory,\n  implicit val messagingProvider: MessagingProvider = SpiLoader.get[MessagingProvider])(\n  implicit actorSystem: ActorSystem,\n  logging: Logging)\n    extends CommonLoadBalancer(config, feedFactory, controllerInstance) {\n\n  /** Build a cluster of all loadbalancers */\n  private val cluster: Option[Cluster] = if (loadConfigOrThrow[ClusterConfig](ConfigKeys.cluster).useClusterBootstrap) {\n    PekkoManagement(actorSystem).start()\n    ClusterBootstrap(actorSystem).start()\n    Some(Cluster(actorSystem))\n  } else if (loadConfigOrThrow[Seq[String]](\"pekko.cluster.seed-nodes\").nonEmpty) {\n    Some(Cluster(actorSystem))\n  } else {\n    None\n  }\n\n  override protected def emitMetrics() = {\n    super.emitMetrics()\n    MetricEmitter.emitGaugeMetric(\n      INVOKER_TOTALMEM_BLACKBOX,\n      schedulingState.blackboxInvokers.foldLeft(0L) { (total, curr) =>\n        if (curr.status.isUsable) {\n          curr.id.userMemory.toMB + total\n        } else {\n          total\n        }\n      })\n    MetricEmitter.emitGaugeMetric(\n      INVOKER_TOTALMEM_MANAGED,\n      schedulingState.managedInvokers.foldLeft(0L) { (total, curr) =>\n        if (curr.status.isUsable) {\n          curr.id.userMemory.toMB + total\n        } else {\n          total\n        }\n      })\n    MetricEmitter.emitGaugeMetric(HEALTHY_INVOKER_MANAGED, schedulingState.managedInvokers.count(_.status == Healthy))\n    MetricEmitter.emitGaugeMetric(\n      UNHEALTHY_INVOKER_MANAGED,\n      schedulingState.managedInvokers.count(_.status == Unhealthy))\n    MetricEmitter.emitGaugeMetric(\n      UNRESPONSIVE_INVOKER_MANAGED,\n      schedulingState.managedInvokers.count(_.status == Unresponsive))\n    MetricEmitter.emitGaugeMetric(OFFLINE_INVOKER_MANAGED, schedulingState.managedInvokers.count(_.status == Offline))\n    MetricEmitter.emitGaugeMetric(HEALTHY_INVOKER_BLACKBOX, schedulingState.blackboxInvokers.count(_.status == Healthy))\n    MetricEmitter.emitGaugeMetric(\n      UNHEALTHY_INVOKER_BLACKBOX,\n      schedulingState.blackboxInvokers.count(_.status == Unhealthy))\n    MetricEmitter.emitGaugeMetric(\n      UNRESPONSIVE_INVOKER_BLACKBOX,\n      schedulingState.blackboxInvokers.count(_.status == Unresponsive))\n    MetricEmitter.emitGaugeMetric(OFFLINE_INVOKER_BLACKBOX, schedulingState.blackboxInvokers.count(_.status == Offline))\n  }\n\n  /** State needed for scheduling. */\n  val schedulingState = ShardingContainerPoolBalancerState()(lbConfig)\n\n  /**\n   * Monitors invoker supervision and the cluster to update the state sequentially\n   *\n   * All state updates should go through this actor to guarantee that\n   * [[ShardingContainerPoolBalancerState.updateInvokers]] and [[ShardingContainerPoolBalancerState.updateCluster]]\n   * are called exclusive of each other and not concurrently.\n   */\n  private val monitor = actorSystem.actorOf(Props(new Actor {\n    override def preStart(): Unit = {\n      cluster.foreach(_.subscribe(self, classOf[MemberEvent], classOf[ReachabilityEvent]))\n    }\n\n    // all members of the cluster that are available\n    var availableMembers = Set.empty[Member]\n\n    override def receive: Receive = {\n      case CurrentInvokerPoolState(newState) =>\n        schedulingState.updateInvokers(newState)\n\n      // State of the cluster as it is right now\n      case CurrentClusterState(members, _, _, _, _) =>\n        availableMembers = members.filter(_.status == MemberStatus.Up)\n        schedulingState.updateCluster(availableMembers.size)\n\n      // General lifecycle events and events concerning the reachability of members. Split-brain is not a huge concern\n      // in this case as only the invoker-threshold is adjusted according to the perceived cluster-size.\n      // Taking the unreachable member out of the cluster from that point-of-view results in a better experience\n      // even under split-brain-conditions, as that (in the worst-case) results in premature overloading of invokers vs.\n      // going into overflow mode prematurely.\n      case event: ClusterDomainEvent =>\n        availableMembers = event match {\n          case MemberUp(member)          => availableMembers + member\n          case ReachableMember(member)   => availableMembers + member\n          case MemberRemoved(member, _)  => availableMembers - member\n          case UnreachableMember(member) => availableMembers - member\n          case _                         => availableMembers\n        }\n\n        schedulingState.updateCluster(availableMembers.size)\n    }\n  }))\n\n  /** Loadbalancer interface methods */\n  override def invokerHealth(): Future[IndexedSeq[InvokerHealth]] = Future.successful(schedulingState.invokers)\n  override def clusterSize: Int = schedulingState.clusterSize\n\n  /** 1. Publish a message to the loadbalancer */\n  override def publish(action: ExecutableWhiskActionMetaData, msg: ActivationMessage)(\n    implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]] = {\n\n    val isBlackboxInvocation = action.exec.pull\n    val actionType = if (!isBlackboxInvocation) \"managed\" else \"blackbox\"\n    val (invokersToUse, stepSizes) =\n      if (!isBlackboxInvocation) (schedulingState.managedInvokers, schedulingState.managedStepSizes)\n      else (schedulingState.blackboxInvokers, schedulingState.blackboxStepSizes)\n    val chosen = if (invokersToUse.nonEmpty) {\n      val hash = ShardingContainerPoolBalancer.generateHash(msg.user.namespace.name, action.fullyQualifiedName(false))\n      val homeInvoker = hash % invokersToUse.size\n      val stepSize = stepSizes(hash % stepSizes.size)\n      val invoker: Option[(InvokerInstanceId, Boolean)] = ShardingContainerPoolBalancer.schedule(\n        action.limits.concurrency.maxConcurrent,\n        action.fullyQualifiedName(true),\n        invokersToUse,\n        schedulingState.invokerSlots,\n        action.limits.memory.megabytes,\n        homeInvoker,\n        stepSize)\n      invoker.foreach {\n        case (_, true) =>\n          val metric =\n            if (isBlackboxInvocation)\n              LoggingMarkers.BLACKBOX_SYSTEM_OVERLOAD\n            else\n              LoggingMarkers.MANAGED_SYSTEM_OVERLOAD\n          MetricEmitter.emitCounterMetric(metric)\n        case _ =>\n      }\n      invoker.map(_._1)\n    } else {\n      None\n    }\n\n    chosen\n      .map { invoker =>\n        // MemoryLimit() and TimeLimit() return singletons - they should be fast enough to be used here\n        val memoryLimit = action.limits.memory\n        val memoryLimitInfo = if (memoryLimit == MemoryLimit()) { \"std\" } else { \"non-std\" }\n        val timeLimit = action.limits.timeout\n        val timeLimitInfo = if (timeLimit == TimeLimit()) { \"std\" } else { \"non-std\" }\n        logging.info(\n          this,\n          s\"scheduled activation ${msg.activationId}, action '${msg.action.asString}' ($actionType), ns '${msg.user.namespace.name.asString}', mem limit ${memoryLimit.megabytes} MB (${memoryLimitInfo}), time limit ${timeLimit.duration.toMillis} ms (${timeLimitInfo}) to ${invoker}\")\n        val activationResult = setupActivation(msg, action, invoker)\n        sendActivationToInvoker(messageProducer, msg, invoker).map(_ => activationResult)\n      }\n      .getOrElse {\n        // report the state of all invokers\n        val invokerStates = invokersToUse.foldLeft(Map.empty[InvokerState, Int]) { (agg, curr) =>\n          val count = agg.getOrElse(curr.status, 0) + 1\n          agg + (curr.status -> count)\n        }\n\n        logging.error(\n          this,\n          s\"failed to schedule activation ${msg.activationId}, action '${msg.action.asString}' ($actionType), ns '${msg.user.namespace.name.asString}' - invokers to use: $invokerStates\")\n        Future.failed(LoadBalancerException(\"No invokers available\"))\n      }\n  }\n\n  override val invokerPool =\n    invokerPoolFactory.createInvokerPool(\n      actorSystem,\n      messagingProvider,\n      messageProducer,\n      sendActivationToInvoker,\n      Some(monitor))\n\n  override protected def releaseInvoker(invoker: InvokerInstanceId, entry: ActivationEntry) = {\n    schedulingState.invokerSlots\n      .lift(invoker.toInt)\n      .foreach(_.releaseConcurrent(entry.fullyQualifiedEntityName, entry.maxConcurrent, entry.memoryLimit.toMB.toInt))\n  }\n}\n\nobject ShardingContainerPoolBalancer extends LoadBalancerProvider {\n\n  override def instance(whiskConfig: WhiskConfig, instance: ControllerInstanceId)(implicit actorSystem: ActorSystem,\n                                                                                  logging: Logging): LoadBalancer = {\n\n    val invokerPoolFactory = new InvokerPoolFactory {\n      override def createInvokerPool(\n        actorRefFactory: ActorRefFactory,\n        messagingProvider: MessagingProvider,\n        messagingProducer: MessageProducer,\n        sendActivationToInvoker: (MessageProducer, ActivationMessage, InvokerInstanceId) => Future[ResultMetadata],\n        monitor: Option[ActorRef]): ActorRef = {\n\n        InvokerPool.prepare(instance, WhiskEntityStore.datastore())\n\n        actorRefFactory.actorOf(\n          InvokerPool.props(\n            (f, i) => f.actorOf(InvokerActor.props(i, instance)),\n            (m, i) => sendActivationToInvoker(messagingProducer, m, i),\n            messagingProvider.getConsumer(\n              whiskConfig,\n              s\"${Controller.topicPrefix}health${instance.asString}\",\n              s\"${Controller.topicPrefix}health\",\n              maxPeek = 128),\n            monitor))\n      }\n\n    }\n    new ShardingContainerPoolBalancer(\n      whiskConfig,\n      instance,\n      createFeedFactory(whiskConfig, instance),\n      invokerPoolFactory)\n  }\n\n  def requiredProperties: Map[String, String] = kafkaHosts\n\n  /** Generates a hash based on the string representation of namespace and action */\n  def generateHash(namespace: EntityName, action: FullyQualifiedEntityName): Int = {\n    (namespace.asString.hashCode() ^ action.asString.hashCode()).abs\n  }\n\n  /** Euclidean algorithm to determine the greatest-common-divisor */\n  @tailrec\n  def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)\n\n  /** Returns pairwise coprime numbers until x. Result is memoized. */\n  def pairwiseCoprimeNumbersUntil(x: Int): IndexedSeq[Int] =\n    (1 to x).foldLeft(IndexedSeq.empty[Int])((primes, cur) => {\n      if (gcd(cur, x) == 1 && primes.forall(i => gcd(i, cur) == 1)) {\n        primes :+ cur\n      } else primes\n    })\n\n  /**\n   * Scans through all invokers and searches for an invoker tries to get a free slot on an invoker. If no slot can be\n   * obtained, randomly picks a healthy invoker.\n   *\n   * @param maxConcurrent concurrency limit supported by this action\n   * @param invokers a list of available invokers to search in, including their state\n   * @param dispatched semaphores for each invoker to give the slots away from\n   * @param slots Number of slots, that need to be acquired (e.g. memory in MB)\n   * @param index the index to start from (initially should be the \"homeInvoker\"\n   * @param step stable identifier of the entity to be scheduled\n   * @return an invoker to schedule to or None of no invoker is available\n   */\n  @tailrec\n  def schedule(\n    maxConcurrent: Int,\n    fqn: FullyQualifiedEntityName,\n    invokers: IndexedSeq[InvokerHealth],\n    dispatched: IndexedSeq[NestedSemaphore[FullyQualifiedEntityName]],\n    slots: Int,\n    index: Int,\n    step: Int,\n    stepsDone: Int = 0)(implicit logging: Logging, transId: TransactionId): Option[(InvokerInstanceId, Boolean)] = {\n    val numInvokers = invokers.size\n\n    if (numInvokers > 0) {\n      val invoker = invokers(index)\n      //test this invoker - if this action supports concurrency, use the scheduleConcurrent function\n      if (invoker.status.isUsable && dispatched(invoker.id.toInt).tryAcquireConcurrent(fqn, maxConcurrent, slots)) {\n        Some(invoker.id, false)\n      } else {\n        // If we've gone through all invokers\n        if (stepsDone == numInvokers + 1) {\n          val healthyInvokers = invokers.filter(_.status.isUsable)\n          if (healthyInvokers.nonEmpty) {\n            // Choose a healthy invoker randomly\n            val random = healthyInvokers(ThreadLocalRandom.current().nextInt(healthyInvokers.size)).id\n            dispatched(random.toInt).forceAcquireConcurrent(fqn, maxConcurrent, slots)\n            logging.warn(this, s\"system is overloaded. Chose invoker${random.toInt} by random assignment.\")\n            Some(random, true)\n          } else {\n            None\n          }\n        } else {\n          val newIndex = (index + step) % numInvokers\n          schedule(maxConcurrent, fqn, invokers, dispatched, slots, newIndex, step, stepsDone + 1)\n        }\n      }\n    } else {\n      None\n    }\n  }\n}\n\n/**\n * Holds the state necessary for scheduling of actions.\n *\n * @param _invokers all of the known invokers in the system\n * @param _managedInvokers all invokers for managed runtimes\n * @param _blackboxInvokers all invokers for blackbox runtimes\n * @param _managedStepSizes the step-sizes possible for the current managed invoker count\n * @param _blackboxStepSizes the step-sizes possible for the current blackbox invoker count\n * @param _invokerSlots state of accessible slots of each invoker\n */\ncase class ShardingContainerPoolBalancerState(\n  private var _invokers: IndexedSeq[InvokerHealth] = IndexedSeq.empty[InvokerHealth],\n  private var _managedInvokers: IndexedSeq[InvokerHealth] = IndexedSeq.empty[InvokerHealth],\n  private var _blackboxInvokers: IndexedSeq[InvokerHealth] = IndexedSeq.empty[InvokerHealth],\n  private var _managedStepSizes: Seq[Int] = ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(0),\n  private var _blackboxStepSizes: Seq[Int] = ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(0),\n  protected[loadBalancer] var _invokerSlots: IndexedSeq[NestedSemaphore[FullyQualifiedEntityName]] =\n    IndexedSeq.empty[NestedSemaphore[FullyQualifiedEntityName]],\n  private var _clusterSize: Int = 1)(\n  lbConfig: ShardingContainerPoolBalancerConfig =\n    loadConfigOrThrow[ShardingContainerPoolBalancerConfig](ConfigKeys.loadbalancer))(implicit logging: Logging) {\n\n  // Managed fraction and blackbox fraction can be between 0.0 and 1.0. The sum of these two fractions has to be between\n  // 1.0 and 2.0.\n  // If the sum is 1.0 that means, that there is no overlap of blackbox and managed invokers. If the sum is 2.0, that\n  // means, that there is no differentiation between managed and blackbox invokers.\n  // If the sum is below 1.0 with the initial values from config, the blackbox fraction will be set higher than\n  // specified in config and adapted to the managed fraction.\n  private val managedFraction: Double = Math.max(0.0, Math.min(1.0, lbConfig.managedFraction))\n  private val blackboxFraction: Double = Math.max(1.0 - managedFraction, Math.min(1.0, lbConfig.blackboxFraction))\n  logging.info(this, s\"managedFraction = $managedFraction, blackboxFraction = $blackboxFraction\")(\n    TransactionId.loadbalancer)\n\n  /** Getters for the variables, setting from the outside is only allowed through the update methods below */\n  def invokers: IndexedSeq[InvokerHealth] = _invokers\n  def managedInvokers: IndexedSeq[InvokerHealth] = _managedInvokers\n  def blackboxInvokers: IndexedSeq[InvokerHealth] = _blackboxInvokers\n  def managedStepSizes: Seq[Int] = _managedStepSizes\n  def blackboxStepSizes: Seq[Int] = _blackboxStepSizes\n  def invokerSlots: IndexedSeq[NestedSemaphore[FullyQualifiedEntityName]] = _invokerSlots\n  def clusterSize: Int = _clusterSize\n\n  /**\n   * @param memory\n   * @return calculated invoker slot\n   */\n  private def getInvokerSlot(memory: ByteSize): ByteSize = {\n    val invokerShardMemorySize = memory / _clusterSize\n    val newTreshold = if (invokerShardMemorySize < MemoryLimit.MIN_MEMORY) {\n      logging.error(\n        this,\n        s\"registered controllers: calculated controller's invoker shard memory size falls below the min memory of one action. \"\n          + s\"Setting to min memory. Expect invoker overloads. Cluster size ${_clusterSize}, invoker user memory size ${memory.toMB.MB}, \"\n          + s\"min action memory size ${MemoryLimit.MIN_MEMORY.toMB.MB}, calculated shard size ${invokerShardMemorySize.toMB.MB}.\")(\n        TransactionId.loadbalancer)\n      MemoryLimit.MIN_MEMORY\n    } else {\n      invokerShardMemorySize\n    }\n    newTreshold\n  }\n\n  /**\n   * Updates the scheduling state with the new invokers.\n   *\n   * This is okay to not happen atomically since dirty reads of the values set are not dangerous. It is important though\n   * to update the \"invokers\" variables last, since they will determine the range of invokers to choose from.\n   *\n   * Handling a shrinking invokers list is not necessary, because InvokerPool won't shrink its own list but rather\n   * report the invoker as \"Offline\".\n   *\n   * It is important that this method does not run concurrently to itself and/or to [[updateCluster]]\n   */\n  def updateInvokers(newInvokers: IndexedSeq[InvokerHealth]): Unit = {\n    val oldSize = _invokers.size\n    val newSize = newInvokers.size\n\n    // for small N, allow the managed invokers to overlap with blackbox invokers, and\n    // further assume that blackbox invokers << managed invokers\n    val managed = Math.max(1, Math.ceil(newSize.toDouble * managedFraction).toInt)\n    val blackboxes = Math.max(1, Math.floor(newSize.toDouble * blackboxFraction).toInt)\n\n    _invokers = newInvokers\n    _managedInvokers = _invokers.take(managed)\n    _blackboxInvokers = _invokers.takeRight(blackboxes)\n\n    val logDetail = if (oldSize != newSize) {\n      _managedStepSizes = ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(managed)\n      _blackboxStepSizes = ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(blackboxes)\n\n      if (oldSize < newSize) {\n        // Keeps the existing state..\n        val onlyNewInvokers = _invokers.drop(_invokerSlots.length)\n        _invokerSlots = _invokerSlots ++ onlyNewInvokers.map { invoker =>\n          new NestedSemaphore[FullyQualifiedEntityName](getInvokerSlot(invoker.id.userMemory).toMB.toInt)\n        }\n        val newInvokerDetails = onlyNewInvokers\n          .map(i =>\n            s\"${i.id.toString}: ${i.status} / ${getInvokerSlot(i.id.userMemory).toMB.MB} of ${i.id.userMemory.toMB.MB}\")\n          .mkString(\", \")\n        s\"number of known invokers increased: new = $newSize, old = $oldSize. details: $newInvokerDetails.\"\n      } else {\n        s\"number of known invokers decreased: new = $newSize, old = $oldSize.\"\n      }\n    } else {\n      s\"no update required - number of known invokers unchanged: $newSize.\"\n    }\n\n    logging.info(\n      this,\n      s\"loadbalancer invoker status updated. managedInvokers = $managed blackboxInvokers = $blackboxes. $logDetail\")(\n      TransactionId.loadbalancer)\n  }\n\n  /**\n   * Updates the size of a cluster. Throws away all state for simplicity.\n   *\n   * This is okay to not happen atomically, since a dirty read of the values set are not dangerous. At worst the\n   * scheduler works on outdated invoker-load data which is acceptable.\n   *\n   * It is important that this method does not run concurrently to itself and/or to [[updateInvokers]]\n   */\n  def updateCluster(newSize: Int): Unit = {\n    val actualSize = newSize max 1 // if a cluster size < 1 is reported, falls back to a size of 1 (alone)\n    if (_clusterSize != actualSize) {\n      val oldSize = _clusterSize\n      _clusterSize = actualSize\n      _invokerSlots = _invokers.map { invoker =>\n        new NestedSemaphore[FullyQualifiedEntityName](getInvokerSlot(invoker.id.userMemory).toMB.toInt)\n      }\n      // Directly after startup, no invokers have registered yet. This needs to be handled gracefully.\n      val invokerCount = _invokers.size\n      val totalInvokerMemory =\n        _invokers.foldLeft(0L)((total, invoker) => total + getInvokerSlot(invoker.id.userMemory).toMB).MB\n      val averageInvokerMemory =\n        if (totalInvokerMemory.toMB > 0 && invokerCount > 0) {\n          (totalInvokerMemory / invokerCount).toMB.MB\n        } else {\n          0.MB\n        }\n      logging.info(\n        this,\n        s\"loadbalancer cluster size changed from $oldSize to $actualSize active nodes. ${invokerCount} invokers with ${averageInvokerMemory} average memory size - total invoker memory ${totalInvokerMemory}.\")(\n        TransactionId.loadbalancer)\n    }\n  }\n}\n\n/**\n * Configuration for the cluster created between loadbalancers.\n *\n * @param useClusterBootstrap Whether or not to use a bootstrap mechanism\n */\ncase class ClusterConfig(useClusterBootstrap: Boolean)\n\n/**\n * Configuration for the sharding container pool balancer.\n *\n * @param blackboxFraction the fraction of all invokers to use exclusively for blackboxes\n * @param timeoutFactor factor to influence the timeout period for forced active acks (time-limit.std * timeoutFactor + timeoutAddon)\n * @param timeoutAddon extra time to influence the timeout period for forced active acks (time-limit.std * timeoutFactor + timeoutAddon)\n */\ncase class ShardingContainerPoolBalancerConfig(managedFraction: Double,\n                                               blackboxFraction: Double,\n                                               timeoutFactor: Int,\n                                               timeoutAddon: FiniteDuration)\n\n/**\n * State kept for each activation slot until completion.\n *\n * @param id id of the activation\n * @param namespaceId namespace that invoked the action\n * @param invokerName invoker the action is scheduled to\n * @param memoryLimit memory limit of the invoked action\n * @param timeLimit time limit of the invoked action\n * @param maxConcurrent concurrency limit of the invoked action\n * @param fullyQualifiedEntityName fully qualified name of the invoked action\n * @param timeoutHandler times out completion of this activation, should be canceled on good paths\n * @param isBlackbox true if the invoked action is a blackbox action, otherwise false (managed action)\n * @param isBlocking true if the action is invoked in a blocking fashion, i.e. \"somebody\" waits for the result\n * @param controllerId id of the controller that this activation comes from\n */\ncase class ActivationEntry(id: ActivationId,\n                           namespaceId: UUID,\n                           invokerName: InvokerInstanceId,\n                           memoryLimit: ByteSize,\n                           timeLimit: FiniteDuration,\n                           maxConcurrent: Int,\n                           fullyQualifiedEntityName: FullyQualifiedEntityName,\n                           timeoutHandler: Cancellable,\n                           isBlackbox: Boolean,\n                           isBlocking: Boolean,\n                           controllerId: ControllerInstanceId = ControllerInstanceId(\"0\"))\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/.dockerignore",
    "content": "*\n!init.sh\n!build/distributions\n!build/tmp/docker-coverage\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nARG BASE=scala\nFROM ${BASE}\n\nENV UID=1001 \\\n    NOT_ROOT_USER=owuser\n\n\n# Copy app jars\nADD build/distributions/cache-invalidator.tar /\n\nCOPY init.sh /\nRUN chmod +x init.sh\n\nRUN useradd -m -u 1001 -d /home/${NOT_ROOT_USER} -s /bin/bash ${NOT_ROOT_USER}\nUSER ${NOT_ROOT_USER}\n\nEXPOSE 8080\nCMD [\"./init.sh\", \"0\"]\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/Dockerfile-debian",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM scala\n\nENV UID=1001 \\\n    NOT_ROOT_USER=owuser\n\n\n# Copy app jars\nADD build/distributions/cache-invalidator.tar /\n\nCOPY init.sh /\nRUN chmod +x init.sh\n\nRUN adduser --disabled-password --disabled-login --gecos '' --uid ${UID} --home /home/${NOT_ROOT_USER} ${NOT_ROOT_USER}\nUSER ${NOT_ROOT_USER}\n\nEXPOSE 8080\nCMD [\"./init.sh\", \"0\"]\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# OpenWhisk Cache Invalidator Service\n\nThis service performs cache invalidation in an OpenWhisk cluster to enable cache event propagation in multi region setups.\n\n## Design\n\nAn OpenWhisk cluster uses a Kafka topic `cacheInvalidation` to communicate changes to any cached entity. Messages on this\ntopic are of the form\n\n```json\n{\"instanceId\":\"controller0\",\"key\":{\"mainId\":\"guest/hello\"}}\n```\n\nWhen deploying OpenWhisk across multiple nodes which do not share a common Kafka instance, we need a way to propagate the\ncache-change events across the cluster. For CosmosDB based setups this can be done by using [CosmosDB ChangeFeed][1]\nsupport. It enables reading changes that are made to any specific collection.\n\nThis service makes use of [change feed processor][2] java library and listen to changes happening in `whisks` and `subject`\ncollections and then convert them into Kafka message events which can be sent to `cacheInvalidation` topic local to the cluster\n\n## Usage\n\nThe service needs following env variables to be set\n\n- `KAFKA_HOSTS` - For local env it can be set to `172.17.0.1:9093`. When using [OpenWhisk Devtools][3] based setup use `kafka`\n- `COSMOSDB_ENDPOINT` - Endpoint URL like https://<account>.documents.azure.com:443/\n- `COSMOSDB_KEY` - DB Key\n- `COSMOSDB_NAME` - DB name\n\nUpon startup it would create a collection to manage the lease data with name `cache-invalidator-lease`. For events sent by\nthis service `instanceId` are sent to `cache-invalidator`\n\n## Local Run\n\nSetup the OpenWhisk cluster using [devtools][3] but have it connect to CosmosDB. This would also start\nthe [Kafka Topic UI][4] at port `8001`. Then when changes are made to the database, you should see events sent to the Kafka\ntopic. For example, if a a package is created with wsk package create test-package using the guest account, the following\nevent is generated:\n\n```json\n {\"instanceId\":\"cache-invalidator\",\"key\":{\"mainId\":\"guest/test-package\"}}\n```\n\n\n[1]: https://docs.microsoft.com/en-us/azure/cosmos-db/change-feed\n[2]: https://github.com/Azure/azure-documentdb-changefeedprocessor-java\n[3]: https://github.com/apache/openwhisk-devtools/tree/master/docker-compose\n[4]: https://github.com/Landoop/kafka-topics-ui\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'application'\n    id 'eclipse'\n    id 'maven-publish'\n    id 'org.scoverage'\n    id 'scala'\n}\n\next.dockerImageName = 'cache-invalidator-cosmosdb'\napply from: \"../../../gradle/docker.gradle\"\ndistDocker.dependsOn ':common:scala:distDocker', 'distTar'\n\nproject.archivesBaseName = \"openwhisk-cache-invalidator-cosmosdb\"\n\nscoverage {\n    scoverageVersion.set(\"${gradle.scala.scoverageVersion}\")\n    scoverageScalaVersion.set(\"${gradle.scala.scoverageScalaVersion}\")\n}\n\ndependencies {\n    implementation \"org.scala-lang:scala-library:${gradle.scala.version}\"\n    implementation (project(':common:scala')) {\n        exclude group: 'com.microsoft.azure', module:'azure-cosmosdb'\n    }\n    implementation \"com.microsoft.azure:azure-cosmos:3.7.6\"\n    implementation \"org.apache.pekko:pekko-connectors-kafka_${gradle.scala.depVersion}:${gradle.pekko_kafka.version}\"\n}\n\nmainClassName = \"org.apache.openwhisk.core.database.cosmosdb.cache.Main\"\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/docker-compose.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n---\nversion: '3'\n\nnetworks:\n  default:\n    external:\n      # Match the network name used by devtool docker compose setup\n      name: openwhisk_default\n\nservices:\n  cache-invalidator:\n    image: whisk/cache-invalidator-cosmosdb\n    ports:\n      - \"8080:8080\"\n    environment:\n      - KAFKA_HOSTS=172.17.0.1:9093\n      - COSMOSDB_ENDPOINT=${COSMOSDB_ENDPOINT}\n      - COSMOSDB_KEY=${COSMOSDB_KEY}\n      - COSMOSDB_NAME=${COSMOSDB_NAME}\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/init.sh",
    "content": "#!/bin/bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n./copyJMXFiles.sh\n\nexport CACHE_INVALIDATOR_OPTS\nCACHE_INVALIDATOR_OPTS=\"$CACHE_INVALIDATOR_OPTS $(./transformEnvironment.sh)\"\n\nexec cache-invalidator/bin/cache-invalidator \"$@\"\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/src/main/resources/application.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\npekko.kafka.producer {\n  # Properties defined by org.apache.kafka.clients.consumer.ConsumerConfig\n  # can be defined in this configuration section.\n  kafka-clients {\n    bootstrap.servers = ${?KAFKA_HOSTS}\n    //change the default producer timeout so that it will quickly fail when kafka topic cannot be written\n    max.block.ms = 2000\n  }\n}\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/src/main/resources/reference.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nwhisk {\n  cache-invalidator {\n    cosmosdb {\n      # Endpoint URL like https://<account>.documents.azure.com:443/\n      endpoint            = ${?COSMOSDB_ENDPOINT}\n      # Access key\n      key                 = ${?COSMOSDB_KEY}\n      # Database name\n      db                  = ${?COSMOSDB_NAME}\n\n      # ConnectionMode. Can be one of\n      # - GATEWAY\n      # - DIRECT\n      connection-mode     = \"GATEWAY\"\n\n      # Consistency level used for DB operations\n      consistency-level   = \"SESSION\"\n\n      # Default throughput used while creating a new lease collection\n      throughput        = 400\n\n      # Name of collection in which lease related data is stored\n      lease-collection    = \"cache-invalidator-lease\"\n\n      # Feed processor host name\n      # If multiple instance running then set it to some unique name\n      hostname            = \"cache-invalidator\"\n\n      # Sets a value indicating whether change feed in the Azure Cosmos DB service should start from beginning\n      # This is only used when\n      #  1. Lease store is not initialized and is ignored if a lease for partition exists and has continuation token\n      #  2. StartContinuation is not specified\n      #  3. StartTime is not specified\n      # This is mostly meant for test purpose where we create both db for first time and without this test at times fail\n      start-from-beginning  = false\n\n      collections {\n        # Provide collection specific connection info here\n        # This can be used if lease collection is to be placed in a separate endpoint/db\n        # - whisks\n        # - subjects\n      }\n    }\n    # HTTP Server port\n    port = 8080\n\n    # Current clusterId - If configured then changes which are done by current cluster would be ignored\n    # i.e. no cache invalidation event message would be generated for those changes\n    # cluster-id =\n\n    event-producer {\n      # Queue size in KafkaEventProducer to hold cache invalidation message bfore flushing them to Kafka\n      buffer-size = 100\n    }\n  }\n}\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/cache/CacheInvalidator.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.openwhisk.core.database.cosmosdb.cache\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.{ActorSystem, CoordinatedShutdown}\nimport org.apache.pekko.kafka.ProducerSettings\nimport com.typesafe.config.Config\nimport org.apache.kafka.common.serialization.StringSerializer\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.database.RemoteCacheInvalidation.cacheInvalidationTopic\n\nimport scala.concurrent.{ExecutionContext, Future, Promise}\nimport scala.util.Success\n\nclass CacheInvalidator(globalConfig: Config)(implicit system: ActorSystem, log: Logging) {\n  import CacheInvalidator._\n  val instanceId = \"cache-invalidator\"\n  val whisksCollection = \"whisks\"\n  implicit val ec: ExecutionContext = system.dispatcher\n\n  val config = CacheInvalidatorConfig(globalConfig)\n  val producer =\n    KafkaEventProducer(\n      kafkaProducerSettings(defaultProducerConfig(globalConfig)),\n      cacheInvalidationTopic,\n      config.eventProducerConfig)\n  val observer = new WhiskChangeEventObserver(config.invalidatorConfig, producer)\n  val feedConsumer: ChangeFeedConsumer = new ChangeFeedConsumer(whisksCollection, config, observer)\n  val running = Promise[Done]\n\n  def start(): (Future[Done], Future[Done]) = {\n    //If there is a failure at feedConsumer.start, stop everything\n    val startFuture = feedConsumer.start\n    startFuture\n      .map { _ =>\n        registerShutdownTasks(system, feedConsumer, producer)\n        log.info(this, s\"Started the Cache invalidator service. ClusterId [${config.invalidatorConfig.clusterId}]\")\n      }\n      .recover {\n        case t: Throwable =>\n          log.error(this, s\"Shutdown after failure to start invalidator: ${t}\")\n          stop(Some(t))\n      }\n\n    //If the producer stream fails, stop everything.\n    producer\n      .getStreamFuture()\n      .map(_ => log.info(this, \"Successfully completed producer\"))\n      .recover {\n        case t: Throwable =>\n          log.error(this, s\"Shutdown after producer failure: ${t}\")\n          stop(Some(t))\n      }\n\n    (startFuture, running.future)\n  }\n  def stop(error: Option[Throwable])(implicit system: ActorSystem, ec: ExecutionContext, log: Logging): Future[Done] = {\n    feedConsumer\n      .close()\n      .andThen {\n        case _ =>\n          producer.close().andThen {\n            case _ =>\n              terminate(error)\n          }\n      }\n  }\n  def terminate(error: Option[Throwable]): Unit = {\n    //make sure that the tracking future is only completed once, even though it may be called for various types of failures\n    synchronized {\n      if (!running.isCompleted) {\n        error.map(running.failure).getOrElse(running.success(Done))\n      }\n    }\n  }\n  private def registerShutdownTasks(system: ActorSystem,\n                                    feedConsumer: ChangeFeedConsumer,\n                                    producer: KafkaEventProducer)(implicit ec: ExecutionContext, log: Logging): Unit = {\n    CoordinatedShutdown(system).addTask(CoordinatedShutdown.PhaseBeforeServiceUnbind, \"closeFeedListeners\") { () =>\n      feedConsumer\n        .close()\n        .flatMap { _ =>\n          producer.close().andThen {\n            case Success(_) =>\n              log.info(this, \"Kafka producer successfully shutdown\")\n          }\n        }\n    }\n  }\n}\nobject CacheInvalidator {\n  def kafkaProducerSettings(config: Config): ProducerSettings[String, String] =\n    ProducerSettings(config, new StringSerializer, new StringSerializer)\n\n  def defaultProducerConfig(globalConfig: Config): Config = globalConfig.getConfig(\"pekko.kafka.producer\")\n\n}\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/cache/CacheInvalidatorConfig.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb.cache\n\nimport com.azure.data.cosmos.{ConnectionMode, ConsistencyLevel}\nimport com.typesafe.config.Config\nimport com.typesafe.config.ConfigUtil.joinPath\nimport pureconfig._\nimport pureconfig.generic.auto._\n\ncase class ConnectionInfo(endpoint: String,\n                          key: String,\n                          db: String,\n                          throughput: Int,\n                          connectionMode: ConnectionMode,\n                          consistencyLevel: ConsistencyLevel)\n\ncase class FeedConfig(hostname: String, leaseCollection: String, startFromBeginning: Boolean)\n\ncase class EventProducerConfig(bufferSize: Int)\n\ncase class InvalidatorConfig(port: Int, clusterId: Option[String])\n\ncase class CacheInvalidatorConfig(globalConfig: Config) {\n  val configRoot = \"whisk.cache-invalidator\"\n  val cosmosConfigRoot = s\"$configRoot.cosmosdb\"\n  val eventConfigRoot = s\"$configRoot.event-producer\"\n  val connections = \"collections\"\n  val feedConfig: FeedConfig = loadConfigOrThrow[FeedConfig](globalConfig.getConfig(cosmosConfigRoot))\n  val eventProducerConfig: EventProducerConfig =\n    loadConfigOrThrow[EventProducerConfig](globalConfig.getConfig(eventConfigRoot))\n  val invalidatorConfig: InvalidatorConfig = loadConfigOrThrow[InvalidatorConfig](globalConfig.getConfig(configRoot))\n\n  def getCollectionInfo(name: String): ConnectionInfo = {\n    val config = globalConfig.getConfig(cosmosConfigRoot)\n    val specificConfigPath = joinPath(connections, name)\n\n    //Merge config specific to entity with common config\n    val entityConfig = if (config.hasPath(specificConfigPath)) {\n      config.getConfig(specificConfigPath).withFallback(config)\n    } else {\n      config\n    }\n\n    loadConfigOrThrow[ConnectionInfo](entityConfig)\n  }\n}\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/cache/ChangeFeedConsumer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb.cache\n\nimport java.util\n\nimport org.apache.pekko.Done\nimport com.azure.data.cosmos.internal.changefeed.implementation.ChangeFeedProcessorBuilderImpl\nimport com.azure.data.cosmos.internal.changefeed.{ChangeFeedObserverCloseReason, ChangeFeedObserverContext}\nimport com.azure.data.cosmos.{\n  ChangeFeedProcessor,\n  ChangeFeedProcessorOptions,\n  ConnectionPolicy,\n  CosmosClient,\n  CosmosContainer,\n  CosmosItemProperties\n}\nimport org.apache.openwhisk.common.Logging\nimport reactor.core.publisher.Mono\n\nimport scala.collection.JavaConverters._\nimport scala.collection.immutable.Seq\nimport scala.compat.java8.FutureConverters._\nimport scala.concurrent.{ExecutionContext, Future}\n\ntrait ChangeFeedObserver {\n  def process(context: ChangeFeedObserverContext, docs: Seq[CosmosItemProperties]): Future[Done]\n}\n\nclass ChangeFeedConsumer(collName: String, config: CacheInvalidatorConfig, observer: ChangeFeedObserver)(\n  implicit ec: ExecutionContext,\n  log: Logging) {\n  import ChangeFeedConsumer._\n\n  log.info(this, s\"Watching changes in $collName with lease managed in ${config.feedConfig.leaseCollection}\")\n  val clients = scala.collection.mutable.Map[ConnectionInfo, CosmosClient]().withDefault(createCosmosClient)\n\n  var processor: Option[ChangeFeedProcessor] = None\n  def start: Future[Done] = {\n\n    def getContainer(name: String, createIfNotExist: Boolean = false): CosmosContainer = {\n      val info = config.getCollectionInfo(name)\n      val client = clients(info)\n      val db = client.getDatabase(info.db)\n      val container = db.getContainer(name)\n\n      val resp = if (createIfNotExist) {\n        db.createContainerIfNotExists(name, \"/id\", info.throughput)\n      } else container.read()\n\n      resp.block().container()\n    }\n    try {\n      val targetContainer = getContainer(collName)\n      val leaseContainer = getContainer(config.feedConfig.leaseCollection, createIfNotExist = true)\n\n      val clusterId = config.invalidatorConfig.clusterId\n      val prefix = clusterId.map(id => s\"$id-$collName\").getOrElse(collName)\n\n      val feedOpts = new ChangeFeedProcessorOptions\n      feedOpts.leasePrefix(prefix)\n      feedOpts.startFromBeginning(config.feedConfig.startFromBeginning)\n\n      val builder = ChangeFeedProcessor.Builder\n        .hostName(config.feedConfig.hostname)\n        .feedContainer(targetContainer)\n        .leaseContainer(leaseContainer)\n        .options(feedOpts)\n        .asInstanceOf[ChangeFeedProcessorBuilderImpl] //observerFactory is not exposed hence need to cast to impl\n\n      builder.observerFactory(() => ObserverBridge)\n      val p = builder.build()\n\n      processor = Some(p)\n      p.start().toFuture.toScala.map(_ => Done)\n    } catch {\n      case t: Throwable => Future.failed(t)\n    }\n\n  }\n\n  def close(): Future[Done] = {\n\n    processor\n      .map { p =>\n        // be careful about exceptions thrown during ChangeFeedProcessor.stop()\n        // e.g. calling stop() before start() completed, etc will throw exceptions\n        try {\n          p.stop().toFuture.toScala.map(_ => Done)\n        } catch {\n          case t: Throwable =>\n            log.warn(this, s\"Failed to stop processor ${t}\")\n            Future.failed(t)\n        }\n      }\n      .getOrElse(Future.successful(Done))\n      .andThen {\n        case _ =>\n          log.info(this, \"Closing cosmos clients.\")\n          clients.values.foreach(c => c.close())\n          Future.successful(Done)\n      }\n\n  }\n\n  private object ObserverBridge extends com.azure.data.cosmos.internal.changefeed.ChangeFeedObserver {\n    override def open(context: ChangeFeedObserverContext): Unit = {}\n    override def close(context: ChangeFeedObserverContext, reason: ChangeFeedObserverCloseReason): Unit = {}\n    override def processChanges(context: ChangeFeedObserverContext,\n                                docs: util.List[CosmosItemProperties]): Mono[Void] = {\n      val f = observer.process(context, docs.asScala.toList).map(_ => null).toJava.toCompletableFuture\n      Mono.fromFuture(f)\n    }\n  }\n}\n\nobject ChangeFeedConsumer {\n  def createCosmosClient(conInfo: ConnectionInfo): CosmosClient = {\n    val policy = ConnectionPolicy.defaultPolicy.connectionMode(conInfo.connectionMode)\n    CosmosClient.builder\n      .endpoint(conInfo.endpoint)\n      .key(conInfo.key)\n      .connectionPolicy(policy)\n      .consistencyLevel(conInfo.consistencyLevel)\n      .build\n  }\n}\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/cache/KafkaEventProducer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb.cache\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.kafka.scaladsl.Producer\nimport org.apache.pekko.kafka.{ProducerMessage, ProducerSettings}\nimport org.apache.pekko.stream.scaladsl.{Keep, Sink, Source}\nimport org.apache.pekko.stream._\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.connector.kafka.KamonMetricsReporter\n\nimport scala.collection.immutable.Seq\nimport scala.concurrent.{ExecutionContext, Future, Promise}\n\ncase class KafkaEventProducer(settings: ProducerSettings[String, String],\n                              topic: String,\n                              eventProducerConfig: EventProducerConfig)(implicit system: ActorSystem, log: Logging)\n    extends EventProducer {\n  private implicit val executionContext: ExecutionContext = system.dispatcher\n\n  private val (queue, stream) = Source\n    .queue[(Seq[String], Promise[Done])](eventProducerConfig.bufferSize, OverflowStrategy.fail) //TODO Use backpressure\n    .map {\n      case (msgs, p) =>\n        log.info(this, s\"Sending ${msgs.size} messages to kafka.\")\n        ProducerMessage.multi(msgs.map(newRecord), p)\n    }\n    .via(Producer.flexiFlow(producerSettings))\n    .map {\n      case ProducerMessage.MultiResult(r, passThrough) =>\n        log.info(this, s\"Produced ${r.size} messages.\")\n        passThrough.success(Done)\n      case _ =>\n      //As we use multi mode only other modes need not be handled\n    }\n    .recover {\n      case t: Throwable =>\n        //this will happen in case of shutdown while items are still queued, i.e. if producer cannot connect\n        throw (t)\n    }\n    .toMat(Sink.ignore)(Keep.both)\n    .run()\n  def getStreamFuture() = stream\n\n  override def send(msg: Seq[String]): Future[Done] = {\n    val promise = Promise[Done]\n    queue.offer(msg -> promise).flatMap {\n      case QueueOfferResult.Enqueued    => promise.future\n      case QueueOfferResult.Dropped     => Future.failed(new Exception(\"Kafka request queue is full.\"))\n      case QueueOfferResult.QueueClosed => Future.failed(new Exception(\"Kafka request queue was closed.\"))\n      case QueueOfferResult.Failure(f)  => Future.failed(f)\n    }\n  }\n\n  def close(): Future[Done] = {\n    log.info(this, \"Closing kafka producer.\")\n    queue.complete()\n    queue.watchCompletion()\n  }\n\n  private def newRecord(msg: String) = new ProducerRecord[String, String](topic, \"messages\", msg)\n\n  private def producerSettings =\n    settings.withProperty(ConsumerConfig.METRIC_REPORTER_CLASSES_CONFIG, KamonMetricsReporter.name)\n}\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/cache/Main.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb.cache\n\nimport org.apache.pekko.actor.ActorSystem\nimport kamon.Kamon\nimport org.apache.openwhisk.common.{ConfigMXBean, Logging, PekkoLogging}\nimport org.apache.openwhisk.http.{BasicHttpService, BasicRasService}\n\nimport scala.concurrent.ExecutionContext\n\nobject Main {\n  def main(args: Array[String]): Unit = {\n    implicit val system: ActorSystem = ActorSystem(\"cache-invalidator-actor-system\")\n    implicit val log: Logging = new PekkoLogging(org.apache.pekko.event.Logging.getLogger(system, this))\n    implicit val ec: ExecutionContext = system.dispatcher\n    ConfigMXBean.register()\n    Kamon.init()\n    val port = CacheInvalidatorConfig(system.settings.config).invalidatorConfig.port\n    BasicHttpService.startHttpService(new BasicRasService {}.route, port, None)\n    val (start, finish) = new CacheInvalidator(system.settings.config).start()\n    start\n      .map(_ => log.info(this, s\"Started the server at http://localhost:$port\"))\n    finish\n      .andThen {\n        case _ =>\n          Kamon.stopModules().andThen {\n            case _ =>\n              system.terminate().andThen {\n                case _ =>\n                  //it is possible that the cosmos sdk reactor system does not cleanly shut down, so we will explicitly terminate jvm here.\n                  log.info(this, \"Exiting\")\n                  sys.exit(0)\n              }\n          }\n      }\n  }\n}\n"
  },
  {
    "path": "core/cosmosdb/cache-invalidator/src/main/scala/org/apache/openwhisk/core/database/cosmosdb/cache/WhiskChangeEventObserver.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb.cache\n\nimport org.apache.pekko.Done\nimport com.azure.data.cosmos.CosmosItemProperties\nimport com.azure.data.cosmos.internal.changefeed.ChangeFeedObserverContext\nimport com.google.common.base.Throwables\nimport kamon.metric.MeasurementUnit\nimport org.apache.openwhisk.common.{LogMarkerToken, Logging, MetricEmitter}\nimport org.apache.openwhisk.core.database.CacheInvalidationMessage\nimport org.apache.openwhisk.core.database.cosmosdb.CosmosDBConstants\nimport org.apache.openwhisk.core.database.cosmosdb.CosmosDBUtil.unescapeId\nimport org.apache.openwhisk.core.entity.CacheKey\n\nimport scala.collection.concurrent.TrieMap\nimport scala.collection.immutable.Seq\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success}\n\nclass WhiskChangeEventObserver(config: InvalidatorConfig, eventProducer: EventProducer)(implicit ec: ExecutionContext,\n                                                                                        log: Logging)\n    extends ChangeFeedObserver {\n  import WhiskChangeEventObserver._\n\n  override def process(context: ChangeFeedObserverContext, docs: Seq[CosmosItemProperties]): Future[Done] = {\n    //Each observer is called from a pool managed by CosmosDB ChangeFeedProcessor\n    //So its fine to have a blocking wait. If this fails then batch would be reread and\n    //retried thus ensuring at-least-once semantics\n    val f = eventProducer.send(processDocs(docs, config))\n    f.andThen {\n      case Success(_) =>\n        MetricEmitter.emitCounterMetric(feedCounter, docs.size)\n        recordLag(context, docs.last)\n      case Failure(t) =>\n        log.warn(this, \"Error occurred while sending cache invalidation message \" + Throwables.getStackTraceAsString(t))\n    }\n  }\n}\n\ntrait EventProducer {\n  def send(msg: Seq[String]): Future[Done]\n}\n\nobject WhiskChangeEventObserver {\n  val instanceId = \"cache-invalidator\"\n  private val feedCounter =\n    LogMarkerToken(\"cosmosdb\", \"change_feed\", \"count\", tags = Map(\"collection\" -> \"whisks\"))(MeasurementUnit.none)\n  private val lags = new TrieMap[String, LogMarkerToken]\n\n  /**\n   * Records the current lag on per partition basis. In ideal cases the lag should not continue to increase\n   */\n  def recordLag(context: ChangeFeedObserverContext, lastDoc: CosmosItemProperties): Unit = {\n    val sessionToken = context.getFeedResponse.sessionToken()\n    val lsnRef = lastDoc.get(\"_lsn\")\n    require(lsnRef != null, s\"Non lsn defined in document $lastDoc\")\n\n    val lsn = lsnRef.toString.toLong\n    val sessionLsn = getSessionLsn(sessionToken)\n    val lag = sessionLsn - lsn\n    val partitionKey = context.getPartitionKeyRangeId\n    val gaugeToken = lags.getOrElseUpdate(partitionKey, createLagToken(partitionKey))\n    MetricEmitter.emitGaugeMetric(gaugeToken, lag)\n  }\n\n  private def createLagToken(partitionKey: String) = {\n    LogMarkerToken(\"cosmosdb\", \"change_feed\", \"lag\", tags = Map(\"collection\" -> \"whisks\", \"pk\" -> partitionKey))(\n      MeasurementUnit.none)\n  }\n\n  def getSessionLsn(token: String): Long = {\n    // Session Token can be in two formats. Either {PartitionKeyRangeId}:{LSN}\n    // or {PartitionKeyRangeId}:{Version}#{GlobalLSN}\n    // See https://github.com/Azure/azure-documentdb-changefeedprocessor-dotnet/pull/113/files#diff-54cbd8ddcc33cab4120c8af04869f881\n    val parsedSessionToken = token.substring(token.indexOf(\":\") + 1)\n    val segments = parsedSessionToken.split(\"#\")\n    val lsn = if (segments.size < 2) segments(0) else segments(1)\n    lsn.toLong\n  }\n\n  def processDocs(docs: Seq[CosmosItemProperties], config: InvalidatorConfig)(implicit log: Logging): Seq[String] = {\n    docs\n      .filter { doc =>\n        val cid = Option(doc.getString(CosmosDBConstants.clusterId))\n        val currentCid = config.clusterId\n\n        //only if current clusterId is configured do a check\n        currentCid match {\n          case Some(_) => cid != currentCid\n          case None    => true\n        }\n      }\n      .map { doc =>\n        val id = unescapeId(doc.id())\n        log.info(this, s\"Changed doc [$id]\")\n        val event = CacheInvalidationMessage(CacheKey(id), instanceId)\n        event.serialize\n      }\n  }\n\n}\n"
  },
  {
    "path": "core/invoker/.dockerignore",
    "content": "*\n!init.sh\n!build/distributions\n!build/tmp/docker-coverage\n"
  },
  {
    "path": "core/invoker/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nARG BASE=scala\nFROM ${BASE}\n\nENV UID=1001 \\\n    NOT_ROOT_USER=owuser \\\n    DOCKER_VERSION=23.0.6\n# If you change the docker version here, it has implications on invoker runc support.\n# Docker server version and the invoker docker version must be the same to enable runc usage.\n# If this cannot be guaranteed, set `invoker_use_runc: false` in the ansible env.\n\n# Uncomment to fetch latest version of docker instead: RUN wget -qO- https://get.docker.com | sh\n# Install docker client\nRUN curl -sSL -o docker-${DOCKER_VERSION}.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz && \\\n    tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/docker && \\\n    tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/runc && \\\n    rm -f docker-${DOCKER_VERSION}.tgz && \\\n    chmod +x /usr/bin/docker && \\\n    chmod +x /usr/bin/runc\n\nADD build/distributions/invoker.tar ./\n\nCOPY init.sh /\nRUN chmod +x init.sh\n\n# When running the invoker as a non-root user this has implications on the standard directory where runc stores its data.\n# The non-root user should have access on the directory and corresponding permission to make changes on it.\nRUN useradd -m -u 1001 -d /home/${NOT_ROOT_USER} -s /bin/bash ${NOT_ROOT_USER}\n\nEXPOSE 8080\nCMD [\"./init.sh\", \"0\"]\n"
  },
  {
    "path": "core/invoker/Dockerfile-debian",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM scala\n\nENV UID=1001 \\\n    NOT_ROOT_USER=owuser\n    ENV DOCKER_VERSION=23.0.6\n# If you change the docker version here, it has implications on invoker runc support.\n# Docker server version and the invoker docker version must be the same to enable runc usage.\n# If this cannot be guaranteed, set `invoker_use_runc: false` in the ansible env.\n\n\nRUN apt-get -y install openssl\n\n# Uncomment to fetch latest version of docker instead: RUN wget -qO- https://get.docker.com | sh\n# Install docker client\nRUN curl -sSL -o docker-${DOCKER_VERSION}.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz && \\\n    tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/docker && \\\n    tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/runc && \\\n    rm -f docker-${DOCKER_VERSION}.tgz && \\\n    chmod +x /usr/bin/docker && \\\n    chmod +x /usr/bin/runc\n\nADD build/distributions/invoker.tar ./\n\nCOPY init.sh /\nRUN chmod +x init.sh\n\n# When running the invoker as a non-root user this has implications on the standard directory where runc stores its data.\n# The non-root user should have access on the directory and corresponding permission to make changes on it.\nRUN adduser --disabled-password --disabled-login --gecos '' --uid ${UID} --home /home/${NOT_ROOT_USER} ${NOT_ROOT_USER}\n\nEXPOSE 8080\nCMD [\"./init.sh\", \"0\"]\n"
  },
  {
    "path": "core/invoker/Dockerfile.cov",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM invoker\n\nARG OW_ROOT_DIR\n\nRUN mkdir -p /coverage/common && \\\n    mkdir -p /coverage/invoker && \\\n    mkdir -p \"${OW_ROOT_DIR}/common/scala/build\" && \\\n    mkdir -p \"${OW_ROOT_DIR}/core/invoker/build\" && \\\n    ln -s /coverage/common \"${OW_ROOT_DIR}/common/scala/build/scoverage\" && \\\n    ln -s /coverage/invoker \"${OW_ROOT_DIR}/core/invoker/build/scoverage\"\n\nCOPY build/tmp/docker-coverage /invoker/\n"
  },
  {
    "path": "core/invoker/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'application'\n    id 'eclipse'\n    id 'maven-publish'\n    id 'org.scoverage'\n    id 'scala'\n}\n\next.dockerImageName = 'invoker'\napply from: '../../gradle/docker.gradle'\ndistDocker.dependsOn ':common:scala:distDocker', 'distTar'\n\nproject.archivesBaseName = \"openwhisk-invoker\"\n\nscoverage {\n    scoverageVersion.set(\"${gradle.scala.scoverageVersion}\")\n    scoverageScalaVersion.set(\"${gradle.scala.scoverageScalaVersion}\")\n}\n\next.coverageDirs = [\n    \"${buildDir}/classes/scala/scoverage\",\n    \"${project(':common:scala').buildDir.absolutePath}/classes/scala/scoverage\"\n]\ndistDockerCoverage.dependsOn ':common:scala:scoverageClasses', 'scoverageClasses'\n\ndependencies {\n    implementation \"org.scala-lang:scala-library:${gradle.scala.version}\"\n    implementation project(':common:scala')\n    implementation project(':core:scheduler')\n\n    implementation (\"org.apache.curator:curator-recipes:${gradle.curator.version}\") {\n        exclude group: 'org.apache.zookeeper', module:'zookeeper'\n    }\n    implementation (\"org.apache.zookeeper:zookeeper:3.4.14\") {\n        exclude group: 'org.slf4j'\n        exclude group: 'log4j'\n        exclude group: 'jline'\n    }\n}\n\nmainClassName = \"org.apache.openwhisk.core.invoker.Invoker\"\n"
  },
  {
    "path": "core/invoker/init.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n./copyJMXFiles.sh\n\nexport INVOKER_OPTS\nINVOKER_OPTS=\"$INVOKER_OPTS $(./transformEnvironment.sh)\"\n\nexec invoker/bin/invoker \"$@\"\n"
  },
  {
    "path": "core/invoker/src/main/resources/application.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# common logging configuration see common scala\ninclude \"logging\"\ninclude \"pekko-http-version\"\n\nwhisk {\n  blacklist {\n    poll-interval: 5 minutes\n  }\n\n  docker.client {\n    # Docker < 1.13.1 has a known problem: if more than 10 containers are created (docker run)\n    # concurrently, there is a good chance that some of them will fail.\n    # See https://github.com/moby/moby/issues/29369\n    # Use a semaphore to make sure that at most 10 `docker run` commands are active\n    # the same time.\n    # 0 means that there are infinite parallel runs.\n    parallel-runs: 10\n\n    # hide args passed into docker run command when logging docker run command\n    mask-docker-run-args: false\n\n    # Timeouts for docker commands. Set to \"Inf\" to disable timeout.\n    timeouts {\n      run: 1 minute\n      rm: 1 minute\n      pull: 10 minutes\n      ps: 1 minute\n      inspect: 1 minute\n      pause: 10 seconds\n      unpause: 10 seconds\n      version: 10 seconds\n    }\n  }\n\n  docker.container-factory {\n    # Use runc for pause/resume functionality in DockerContainerFactory\n    use-runc: true\n  }\n\n  docker.standalone.container-factory {\n    #If enabled then pull would also be attempted for standard OpenWhisk images under`openwhisk` prefix\n    pull-standard-images: false\n  }\n\n  container-pool {\n    user-memory: 1024 m\n    concurrent-peek-factor: 0.5 #factor used to limit message peeking: 0 < factor <= 1.0 - larger number improves concurrent processing, but increases risk of message loss during invoker crash\n    pekko-client:  false # if true, use PoolingContainerClient for HTTP from invoker to action container (otherwise use ApacheBlockingContainerClient)\n    prewarm-expiration-check-init-delay: 10 minute # the init delay time for the first check\n    prewarm-expiration-check-interval: 10 minute # period to check for prewarm expiration\n    prewarm-expiration-check-interval-variance: 10 seconds # varies expiration across invokers to avoid many concurrent expirations\n    prewarm-expiration-limit: 100 # number of prewarms to expire in one expiration cycle (remaining expired will be considered for expiration in next cycle)\n    prewarm-max-retry-limit: 5 # max subsequent retry limit to create prewarm containers\n    prewarm-promotion: false # if true, action can take prewarm container which has bigger memory\n    memory-sync-interval: 1 second # period to sync memory info to etcd\n    batch-deletion-size: 10 # batch size for removing containers when disable invoker, too big value may cause docker/k8s overload\n    # optional setting to specify the total allocatable cpus for all action containers, each container will get a fraction of this proportional to its allocated memory to limit the cpu\n    # user-cpus: 1\n  }\n\n  kubernetes {\n    # Timeouts for k8s commands. Set to \"Inf\" to disable timeout.\n    timeouts {\n      run: 1 minute\n      logs: 1 minute\n    }\n    user-pod-node-affinity {\n      enabled: true\n      key: \"openwhisk-role\"\n      value: \"invoker\"\n    }\n    # Enables forwarding to remote port via a local random port. This mode is mostly useful\n    # for development via Standalone mode\n    port-forwarding-enabled = false\n\n    # Pod template used as base for Action Pods created. It can be either\n    #  1. Reference to file `file:/path/to/template.yml`\n    #  2. OR yaml formatted multi line string. See multi line config support https://github.com/lightbend/config/blob/master/HOCON.md#multi-line-strings\n    #\n    #pod-template =\n\n    # Set this optional string to be the namespace that the invoker should target for adding pods. This allows the invoker to run in a namespace it doesn't have API access to but add pods to another namespace. See also https://github.com/apache/openwhisk/issues/4711\n    # When not set the underlying client may pickup the namespace from the kubeconfig or via system property setting.\n    # See https://github.com/fabric8io/kubernetes-client#configuring-the-client for more information.\n    # action-namespace = \"ns-actions\"\n\n    #scale milliCPU config per segment of memory: 100 milliCPU == .1 vcpu per https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/\n    #code will append the \"m\" after calculating the number of milliCPU\n    #if missing, the pod will be created without cpu request/limit (and use the default for that namespace/cluster)\n    #if specified, the pod will be created with cpu request+limit set as (action memory limit / cpu-scaling.memory) * cpu-scaling.millicpus; with max of cpu-scaling.max-millicpus and min of cpu-scaling.millicpus\n    #cpu-scaling {\n    #  millicpus = 100\n    #  memory = 256 m\n    #  max-millicpus = 4000\n    #}\n\n    # Action pods can be injected with pod data using field refs to the pod spec (aka The Downward API):\n    # https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-pod-fields-as-values-for-environment-variables\n    #field-ref-environment: {\n    #  \"POD_NAMESPACE\":\"metadata.namespace\",\n    #  \"POD_NAME\":\"metadata.name\",\n    #  \"POD_UID\": \"metadata.uid\"\n    #}\n\n    #if missing, the pod will be created without ephermal disk request/limit\n    #if specified, the pod will be created with ephemeral-storage request+limit set or using the scale factor\n    #as a multiple of the request memory. If the scaled value exceeds the limit, the limit will be used so, it's good\n    #practice to set the limit if you plan on using the scale-factor.\n    #See: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#local-ephemeral-storage\n    #ephemeral-storage {\n    #  limit = 2 g\n    #  scale-factor = 2.0\n    #}\n\n    #enable PodDisruptionBudget creation for pods? (will include same labels as pods, and specify minAvailable=1 to prevent termination of action pods during maintenance)\n    pdb-enabled = false\n  }\n\n  # Timeouts for runc commands. Set to \"Inf\" to disable timeout.\n  runc.timeouts {\n    pause: 10 seconds\n    resume: 10 seconds\n  }\n\n  # args for 'docker run' to use\n  container-factory {\n    container-args {\n      network: bridge\n      # See https://docs.docker.com/config/containers/container-networking/#dns-services for documentation of dns-*\n      dns-servers: []\n      dns-search: []\n      dns-options: []\n      extra-env-vars: [] # sequence of `key` and/or `key=value` bindings to add to all user action container environments\n      extra-args: {}   # to pass additional args to 'docker run'; format is `{key1: [v1, v2], key2: [v1, v2]}`\n    }\n    runtimes-registry {\n      url: \"\"\n    }\n    user-images-registry {\n      url: \"\"\n    }\n  }\n\n  container-proxy {\n    timeouts {\n      # The \"unusedTimeout\" in the ContainerProxy,\n      #aka 'How long should a container sit idle until we kill it?'\n      idle-container = 10 minutes\n      pause-grace = 10 seconds\n      keeping-duration = 10 minutes\n    }\n    action-health-check {\n      enabled = false # if true, prewarm containers will be pinged periodically and warm containers will be pinged once after resumed\n      check-period = 3 seconds # how often should prewarm containers be pinged (tcp connection attempt)\n      max-fails = 3 # prewarm containers that fail this number of times will be destroyed and replaced\n    }\n\n    log-activation-errors {\n      application-errors = false\n      developer-errors = false\n      whisk-errors = true\n    }\n  }\n\n  # tracing configuration\n  tracing {\n    component = \"Invoker\"\n  }\n\n  invoker {\n    username: \"invoker.user\"\n    password: \"invoker.pass\"\n    protocol: http\n\n    resource {\n      tags: \"\"\n    }\n    dedicated {\n      namespaces: \"\"\n    }\n  }\n  runtime.delete.timeout = \"30 seconds\"\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerPool.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool\n\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, Props}\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, MetricEmitter, TransactionId}\nimport org.apache.openwhisk.core.connector.MessageFeed\nimport org.apache.openwhisk.core.entity.ExecManifest.ReactivePrewarmingConfig\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\n\nimport scala.annotation.tailrec\nimport scala.collection.immutable\nimport scala.concurrent.duration._\nimport scala.util.{Random, Try}\n\ncase class ColdStartKey(kind: String, memory: ByteSize)\n\ncase object EmitMetrics\n\ncase object AdjustPrewarmedContainer\n\n/**\n * A pool managing containers to run actions on.\n *\n * This pool fulfills the other half of the ContainerProxy contract. Only\n * one job (either Start or Run) is sent to a child-actor at any given\n * time. The pool then waits for a response of that container, indicating\n * the container is done with the job. Only then will the pool send another\n * request to that container.\n *\n * Upon actor creation, the pool will start to prewarm containers according\n * to the provided prewarmConfig, iff set. Those containers will **not** be\n * part of the poolsize calculation, which is capped by the poolSize parameter.\n * Prewarm containers are only used, if they have matching arguments\n * (kind, memory) and there is space in the pool.\n *\n * @param childFactory method to create new container proxy actor\n * @param feed actor to request more work from\n * @param prewarmConfig optional settings for container prewarming\n * @param poolConfig config for the ContainerPool\n */\nclass ContainerPool(childFactory: ActorRefFactory => ActorRef,\n                    feed: ActorRef,\n                    prewarmConfig: List[PrewarmingConfig] = List.empty,\n                    poolConfig: ContainerPoolConfig)(implicit val logging: Logging)\n    extends Actor {\n  import ContainerPool.memoryConsumptionOf\n\n  implicit val ec = context.dispatcher\n\n  var freePool = immutable.Map.empty[ActorRef, ContainerData]\n  var busyPool = immutable.Map.empty[ActorRef, ContainerData]\n  var prewarmedPool = immutable.Map.empty[ActorRef, PreWarmedData]\n  var prewarmStartingPool = immutable.Map.empty[ActorRef, (String, ByteSize)]\n  // If all memory slots are occupied and if there is currently no container to be removed, than the actions will be\n  // buffered here to keep order of computation.\n  // Otherwise actions with small memory-limits could block actions with large memory limits.\n  var runBuffer = immutable.Queue.empty[Run]\n  // Track the resent buffer head - so that we don't resend buffer head multiple times\n  var resent: Option[Run] = None\n  val logMessageInterval = 10.seconds\n  //periodically emit metrics (don't need to do this for each message!)\n  context.system.scheduler.scheduleAtFixedRate(30.seconds, 10.seconds, self, EmitMetrics)\n\n  // Key is ColdStartKey, value is the number of cold Start in minute\n  var coldStartCount = immutable.Map.empty[ColdStartKey, Int]\n\n  adjustPrewarmedContainer(true, false)\n\n  // check periodically, adjust prewarmed container(delete if unused for some time and create some increment containers)\n  // add some random amount to this schedule to avoid a herd of container removal + creation\n  val interval = poolConfig.prewarmExpirationCheckInterval + poolConfig.prewarmExpirationCheckIntervalVariance\n    .map(v =>\n      Random\n        .nextInt(v.toSeconds.toInt))\n    .getOrElse(0)\n    .seconds\n  if (prewarmConfig.exists(!_.reactive.isEmpty)) {\n    context.system.scheduler.scheduleAtFixedRate(\n      poolConfig.prewarmExpirationCheckInitDelay,\n      interval,\n      self,\n      AdjustPrewarmedContainer)\n  }\n\n  def logContainerStart(r: Run, containerState: String, activeActivations: Int, container: Option[Container]): Unit = {\n    val namespaceName = r.msg.user.namespace.name.asString\n    val actionName = r.action.name.name\n    val actionNamespace = r.action.namespace.namespace\n    val maxConcurrent = r.action.limits.concurrency.maxConcurrent\n    val activationId = r.msg.activationId.toString\n\n    r.msg.transid.mark(\n      this,\n      LoggingMarkers.INVOKER_CONTAINER_START(containerState, namespaceName, actionNamespace, actionName),\n      s\"containerStart containerState: $containerState container: $container activations: $activeActivations of max $maxConcurrent action: $actionName namespace: $namespaceName activationId: $activationId\",\n      org.apache.pekko.event.Logging.InfoLevel)\n  }\n\n  def receive: Receive = {\n    // A job to run on a container\n    //\n    // Run messages are received either via the feed or from child containers which cannot process\n    // their requests and send them back to the pool for rescheduling (this may happen if \"docker\" operations\n    // fail for example, or a container has aged and was destroying itself when a new request was assigned)\n    case r: Run =>\n      // Check if the message is resent from the buffer. Only the first message on the buffer can be resent.\n      val isResentFromBuffer = runBuffer.nonEmpty && runBuffer.dequeueOption.exists(_._1.msg == r.msg)\n\n      // Only process request, if there are no other requests waiting for free slots, or if the current request is the\n      // next request to process\n      // It is guaranteed, that only the first message on the buffer is resent.\n      if (runBuffer.isEmpty || isResentFromBuffer) {\n        if (isResentFromBuffer) {\n          //remove from resent tracking - it may get resent again, or get processed\n          resent = None\n        }\n        val kind = r.action.exec.kind\n        val memory = r.action.limits.memory.megabytes.MB\n\n        val createdContainer =\n          // Schedule a job to a warm container\n          ContainerPool\n            .schedule(r.action, r.msg.user.namespace.name, freePool)\n            .map(container => (container, container._2.initingState)) //warmed, warming, and warmingCold always know their state\n            .orElse(\n              // There was no warm/warming/warmingCold container. Try to take a prewarm container or a cold container.\n              // When take prewarm container, has no need to judge whether user memory is enough\n              takePrewarmContainer(r.action)\n                .map(container => (container, \"prewarmed\"))\n                .orElse {\n                  // Is there enough space to create a new container or do other containers have to be removed?\n                  if (hasPoolSpaceFor(busyPool ++ freePool ++ prewarmedPool, prewarmStartingPool, memory)) {\n                    val container = Some(createContainer(memory), \"cold\")\n                    incrementColdStartCount(kind, memory)\n                    container\n                  } else None\n                })\n            .orElse(\n              // Remove a container and create a new one for the given job\n              ContainerPool\n              // Only free up the amount, that is really needed to free up\n                .remove(freePool, Math.min(r.action.limits.memory.megabytes, memoryConsumptionOf(freePool)).MB)\n                .map(removeContainer)\n                // If the list had at least one entry, enough containers were removed to start the new container. After\n                // removing the containers, we are not interested anymore in the containers that have been removed.\n                .headOption\n                .map(_ =>\n                  takePrewarmContainer(r.action)\n                    .map(container => (container, \"recreatedPrewarm\"))\n                    .getOrElse {\n                      val container = (createContainer(memory), \"recreated\")\n                      incrementColdStartCount(kind, memory)\n                      container\n                  }))\n\n        createdContainer match {\n          case Some(((actor, data), containerState)) =>\n            //increment active count before storing in pool map\n            val newData = data.nextRun(r)\n            val container = newData.getContainer\n\n            if (newData.activeActivationCount < 1) {\n              logging.error(this, s\"invalid activation count < 1 ${newData}\")\n            }\n\n            //only move to busyPool if max reached\n            if (!newData.hasCapacity()) {\n              if (r.action.limits.concurrency.maxConcurrent > 1) {\n                logging.info(\n                  this,\n                  s\"container ${container} is now busy with ${newData.activeActivationCount} activations\")\n              }\n              busyPool = busyPool + (actor -> newData)\n              freePool = freePool - actor\n            } else {\n              //update freePool to track counts\n              freePool = freePool + (actor -> newData)\n            }\n            // Remove the action that was just executed from the buffer and execute the next one in the queue.\n            if (isResentFromBuffer) {\n              // It is guaranteed that the currently executed messages is the head of the queue, if the message comes\n              // from the buffer\n              val (_, newBuffer) = runBuffer.dequeue\n              runBuffer = newBuffer\n              // Try to process the next item in buffer (or get another message from feed, if buffer is now empty)\n              processBufferOrFeed()\n            }\n            actor ! r // forwards the run request to the container\n            logContainerStart(r, containerState, newData.activeActivationCount, container)\n          case None =>\n            // this can also happen if createContainer fails to start a new container, or\n            // if a job is rescheduled but the container it was allocated to has not yet destroyed itself\n            // (and a new container would over commit the pool)\n            val isErrorLogged = r.retryLogDeadline.map(_.isOverdue).getOrElse(true)\n            val retryLogDeadline = if (isErrorLogged) {\n              logging.warn(\n                this,\n                s\"Rescheduling Run message, too many message in the pool, \" +\n                  s\"freePoolSize: ${freePool.size} containers and ${memoryConsumptionOf(freePool)} MB, \" +\n                  s\"busyPoolSize: ${busyPool.size} containers and ${memoryConsumptionOf(busyPool)} MB, \" +\n                  s\"maxContainersMemory ${poolConfig.userMemory.toMB} MB, \" +\n                  s\"userNamespace: ${r.msg.user.namespace.name}, action: ${r.action}, \" +\n                  s\"needed memory: ${r.action.limits.memory.megabytes} MB, \" +\n                  s\"waiting messages: ${runBuffer.size}\")(r.msg.transid)\n              MetricEmitter.emitCounterMetric(LoggingMarkers.CONTAINER_POOL_RESCHEDULED_ACTIVATION)\n              Some(logMessageInterval.fromNow)\n            } else {\n              r.retryLogDeadline\n            }\n            if (!isResentFromBuffer) {\n              // Add this request to the buffer, as it is not there yet.\n              runBuffer = runBuffer.enqueue(Run(r.action, r.msg, retryLogDeadline))\n            }\n          //buffered items will be processed via processBufferOrFeed()\n        }\n      } else {\n        // There are currently actions waiting to be executed before this action gets executed.\n        // These waiting actions were not able to free up enough memory.\n        runBuffer = runBuffer.enqueue(r)\n      }\n\n    // Container is free to take more work\n    case NeedWork(warmData: WarmedData) =>\n      val oldData = freePool.get(sender()).getOrElse(busyPool(sender()))\n      val newData =\n        warmData.copy(lastUsed = oldData.lastUsed, activeActivationCount = oldData.activeActivationCount - 1)\n      if (newData.activeActivationCount < 0) {\n        logging.error(this, s\"invalid activation count after warming < 1 ${newData}\")\n      }\n      if (newData.hasCapacity()) {\n        //remove from busy pool (may already not be there), put back into free pool (to update activation counts)\n        freePool = freePool + (sender() -> newData)\n        if (busyPool.contains(sender())) {\n          busyPool = busyPool - sender()\n          if (newData.action.limits.concurrency.maxConcurrent > 1) {\n            logging.info(\n              this,\n              s\"concurrent container ${newData.container} is no longer busy with ${newData.activeActivationCount} activations\")\n          }\n        }\n      } else {\n        busyPool = busyPool + (sender() -> newData)\n        freePool = freePool - sender()\n      }\n      processBufferOrFeed()\n    // Container is prewarmed and ready to take work\n    case NeedWork(data: PreWarmedData) =>\n      prewarmStartingPool = prewarmStartingPool - sender()\n      prewarmedPool = prewarmedPool + (sender() -> data)\n\n    // Container got removed\n    case ContainerRemoved(replacePrewarm) =>\n      // if container was in free pool, it may have been processing (but under capacity),\n      // so there is capacity to accept another job request\n      freePool.get(sender()).foreach { f =>\n        freePool = freePool - sender()\n      }\n\n      // container was busy (busy indicates at full capacity), so there is capacity to accept another job request\n      busyPool.get(sender()).foreach { _ =>\n        busyPool = busyPool - sender()\n      }\n      processBufferOrFeed()\n\n      // in case this was a prewarm\n      prewarmedPool.get(sender()).foreach { data =>\n        prewarmedPool = prewarmedPool - sender()\n      }\n\n      // in case this was a starting prewarm\n      prewarmStartingPool.get(sender()).foreach { _ =>\n        logging.info(this, \"failed starting prewarm, removed\")\n        prewarmStartingPool = prewarmStartingPool - sender()\n      }\n\n      //backfill prewarms on every ContainerRemoved(replacePrewarm = true), just in case\n      if (replacePrewarm) {\n        adjustPrewarmedContainer(false, false) //in case a prewarm is removed due to health failure or crash\n      }\n\n    // This message is received for one of these reasons:\n    // 1. Container errored while resuming a warm container, could not process the job, and sent the job back\n    // 2. The container aged, is destroying itself, and was assigned a job which it had to send back\n    // 3. The container aged and is destroying itself\n    // Update the free/busy lists but no message is sent to the feed since there is no change in capacity yet\n    case RescheduleJob =>\n      freePool = freePool - sender()\n      busyPool = busyPool - sender()\n    case EmitMetrics =>\n      emitMetrics()\n\n    case AdjustPrewarmedContainer =>\n      adjustPrewarmedContainer(false, true)\n  }\n\n  /** Resend next item in the buffer, or trigger next item in the feed, if no items in the buffer. */\n  def processBufferOrFeed() = {\n    // If buffer has more items, and head has not already been resent, send next one, otherwise get next from feed.\n    runBuffer.dequeueOption match {\n      case Some((run, _)) => //run the first from buffer\n        implicit val tid = run.msg.transid\n        //avoid sending dupes\n        if (resent.isEmpty) {\n          logging.info(this, s\"re-processing from buffer (${runBuffer.length} items in buffer)\")\n          resent = Some(run)\n          self ! run\n        } else {\n          //do not resend the buffer head multiple times (may reach this point from multiple messages, before the buffer head is re-processed)\n        }\n      case None => //feed me!\n        feed ! MessageFeed.Processed\n    }\n  }\n\n  /** adjust prewarm containers up to the configured requirements for each kind/memory combination. */\n  def adjustPrewarmedContainer(init: Boolean, scheduled: Boolean): Unit = {\n    if (scheduled) {\n      //on scheduled time, remove expired prewarms\n      ContainerPool.removeExpired(poolConfig, prewarmConfig, prewarmedPool).foreach { p =>\n        prewarmedPool = prewarmedPool - p\n        p ! Remove\n      }\n      //on scheduled time, emit cold start counter metric with memory + kind\n      coldStartCount foreach { coldStart =>\n        val coldStartKey = coldStart._1\n        MetricEmitter.emitCounterMetric(\n          LoggingMarkers.CONTAINER_POOL_PREWARM_COLDSTART(coldStartKey.memory.toString, coldStartKey.kind))\n      }\n    }\n    //fill in missing prewarms (replaces any deletes)\n    ContainerPool\n      .increasePrewarms(init, scheduled, coldStartCount, prewarmConfig, prewarmedPool, prewarmStartingPool)\n      .foreach { c =>\n        val config = c._1\n        val currentCount = c._2._1\n        val desiredCount = c._2._2\n        if (currentCount < desiredCount) {\n          (currentCount until desiredCount).foreach { _ =>\n            prewarmContainer(config.exec, config.memoryLimit, config.reactive.map(_.ttl))\n          }\n        }\n      }\n    if (scheduled) {\n      //   lastly, clear coldStartCounts each time scheduled event is processed to reset counts\n      coldStartCount = immutable.Map.empty[ColdStartKey, Int]\n    }\n  }\n\n  /** Creates a new container and updates state accordingly. */\n  def createContainer(memoryLimit: ByteSize): (ActorRef, ContainerData) = {\n    val ref = childFactory(context)\n    val data = MemoryData(memoryLimit)\n    freePool = freePool + (ref -> data)\n    ref -> data\n  }\n\n  /** Creates a new prewarmed container */\n  def prewarmContainer(exec: CodeExec[_], memoryLimit: ByteSize, ttl: Option[FiniteDuration]): Unit = {\n    if (hasPoolSpaceFor(busyPool ++ freePool ++ prewarmedPool, prewarmStartingPool, memoryLimit)) {\n      val newContainer = childFactory(context)\n      prewarmStartingPool = prewarmStartingPool + (newContainer -> (exec.kind, memoryLimit))\n      newContainer ! Start(exec, memoryLimit, ttl)\n    } else {\n      logging.warn(\n        this,\n        s\"Cannot create prewarm container due to reach the invoker memory limit: ${poolConfig.userMemory.toMB}\")\n    }\n  }\n\n  /** this is only for cold start statistics of prewarm configs, e.g. not blackbox or other configs. */\n  def incrementColdStartCount(kind: String, memoryLimit: ByteSize): Unit = {\n    prewarmConfig\n      .filter { config =>\n        kind == config.exec.kind && memoryLimit == config.memoryLimit\n      }\n      .foreach { _ =>\n        val coldStartKey = ColdStartKey(kind, memoryLimit)\n        coldStartCount.get(coldStartKey) match {\n          case Some(value) => coldStartCount = coldStartCount + (coldStartKey -> (value + 1))\n          case None        => coldStartCount = coldStartCount + (coldStartKey -> 1)\n        }\n      }\n  }\n\n  /**\n   * Takes a prewarm container out of the prewarmed pool\n   * iff a container with a matching kind and memory is found.\n   *\n   * @param action the action that holds the kind and the required memory.\n   * @return the container iff found\n   */\n  def takePrewarmContainer(action: ExecutableWhiskAction): Option[(ActorRef, ContainerData)] = {\n    val kind = action.exec.kind\n    val memory = action.limits.memory.megabytes.MB\n    val now = Deadline.now\n    prewarmedPool.toSeq\n      .sortBy(_._2.expires.getOrElse(now))\n      .find {\n        case (_, PreWarmedData(_, `kind`, `memory`, _, _)) => true\n        case _                                             => false\n      }\n      .map {\n        case (ref, data) =>\n          // Move the container to the usual pool\n          freePool = freePool + (ref -> data)\n          prewarmedPool = prewarmedPool - ref\n          // Create a new prewarm container\n          // NOTE: prewarming ignores the action code in exec, but this is dangerous as the field is accessible to the\n          // factory\n\n          //get the appropriate ttl from prewarm configs\n          val ttl =\n            prewarmConfig.find(pc => pc.memoryLimit == memory && pc.exec.kind == kind).flatMap(_.reactive.map(_.ttl))\n          prewarmContainer(action.exec, memory, ttl)\n          (ref, data)\n      }\n  }\n\n  /** Removes a container and updates state accordingly. */\n  def removeContainer(toDelete: ActorRef) = {\n    toDelete ! Remove\n    freePool = freePool - toDelete\n    busyPool = busyPool - toDelete\n  }\n\n  /**\n   * Calculate if there is enough free memory within a given pool.\n   *\n   * @param pool The pool, that has to be checked, if there is enough free memory.\n   * @param memory The amount of memory to check.\n   * @return true, if there is enough space for the given amount of memory.\n   */\n  def hasPoolSpaceFor[A](pool: Map[A, ContainerData],\n                         prewarmStartingPool: Map[A, (String, ByteSize)],\n                         memory: ByteSize): Boolean = {\n    memoryConsumptionOf(pool) + prewarmStartingPool.map(_._2._2.toMB).sum + memory.toMB <= poolConfig.userMemory.toMB\n  }\n\n  /**\n   * Log metrics about pool state (buffer size, buffer memory requirements, active number, active memory, prewarm number, prewarm memory)\n   */\n  private def emitMetrics() = {\n    MetricEmitter.emitGaugeMetric(LoggingMarkers.CONTAINER_POOL_RUNBUFFER_COUNT, runBuffer.size)\n    MetricEmitter.emitGaugeMetric(\n      LoggingMarkers.CONTAINER_POOL_RUNBUFFER_SIZE,\n      runBuffer.map(_.action.limits.memory.megabytes).sum)\n    val containersInUse = freePool.filter(_._2.activeActivationCount > 0) ++ busyPool\n    MetricEmitter.emitGaugeMetric(LoggingMarkers.CONTAINER_POOL_ACTIVE_COUNT, containersInUse.size)\n    MetricEmitter.emitGaugeMetric(\n      LoggingMarkers.CONTAINER_POOL_ACTIVE_SIZE,\n      containersInUse.map(_._2.memoryLimit.toMB).sum)\n    MetricEmitter.emitGaugeMetric(\n      LoggingMarkers.CONTAINER_POOL_PREWARM_COUNT,\n      prewarmedPool.size + prewarmStartingPool.size)\n    MetricEmitter.emitGaugeMetric(\n      LoggingMarkers.CONTAINER_POOL_PREWARM_SIZE,\n      prewarmedPool.map(_._2.memoryLimit.toMB).sum + prewarmStartingPool.map(_._2._2.toMB).sum)\n    val unused = freePool.filter(_._2.activeActivationCount == 0)\n    val unusedMB = unused.map(_._2.memoryLimit.toMB).sum\n    MetricEmitter.emitGaugeMetric(LoggingMarkers.CONTAINER_POOL_IDLES_COUNT, unused.size)\n    MetricEmitter.emitGaugeMetric(LoggingMarkers.CONTAINER_POOL_IDLES_SIZE, unusedMB)\n  }\n}\n\nobject ContainerPool {\n\n  /**\n   * Calculate the memory of a given pool.\n   *\n   * @param pool The pool with the containers.\n   * @return The memory consumption of all containers in the pool in Megabytes.\n   */\n  protected[containerpool] def memoryConsumptionOf[A](pool: Map[A, ContainerData]): Long = {\n    pool.map(_._2.memoryLimit.toMB).sum\n  }\n\n  /**\n   * Finds the best container for a given job to run on.\n   *\n   * Selects an arbitrary warm container from the passed pool of idle containers\n   * that matches the action and the invocation namespace. The implementation uses\n   * matching such that structural equality of action and the invocation namespace\n   * is required.\n   * Returns None iff no matching container is in the idle pool.\n   * Does not consider pre-warmed containers.\n   *\n   * @param action the action to run\n   * @param invocationNamespace the namespace, that wants to run the action\n   * @param idles a map of idle containers, awaiting work\n   * @return a container if one found\n   */\n  protected[containerpool] def schedule[A](action: ExecutableWhiskAction,\n                                           invocationNamespace: EntityName,\n                                           idles: Map[A, ContainerData]): Option[(A, ContainerData)] = {\n    idles\n      .find {\n        case (_, c @ WarmedData(_, `invocationNamespace`, `action`, _, _, _)) if c.hasCapacity() => true\n        case _                                                                                   => false\n      }\n      .orElse {\n        idles.find {\n          case (_, c @ WarmingData(_, `invocationNamespace`, `action`, _, _)) if c.hasCapacity() => true\n          case _                                                                                 => false\n        }\n      }\n      .orElse {\n        idles.find {\n          case (_, c @ WarmingColdData(`invocationNamespace`, `action`, _, _)) if c.hasCapacity() => true\n          case _                                                                                  => false\n        }\n      }\n  }\n\n  /**\n   * Finds the oldest previously used container to remove to make space for the job passed to run.\n   * Depending on the space that has to be allocated, several containers might be removed.\n   *\n   * NOTE: This method is never called to remove an action that is in the pool already,\n   * since this would be picked up earlier in the scheduler and the container reused.\n   *\n   * @param pool a map of all free containers in the pool\n   * @param memory the amount of memory that has to be freed up\n   * @return a list of containers to be removed iff found\n   */\n  @tailrec\n  protected[containerpool] def remove[A](pool: Map[A, ContainerData],\n                                         memory: ByteSize,\n                                         toRemove: List[A] = List.empty): List[A] = {\n    // Try to find a Free container that does NOT have any active activations AND is initialized with any OTHER action\n    val freeContainers = pool.collect {\n      // Only warm containers will be removed. Prewarmed containers will stay always.\n      case (ref, w: WarmedData) if w.activeActivationCount == 0 =>\n        ref -> w\n    }\n\n    if (memory > 0.B && freeContainers.nonEmpty && memoryConsumptionOf(freeContainers) >= memory.toMB) {\n      // Remove the oldest container if:\n      // - there is more memory required\n      // - there are still containers that can be removed\n      // - there are enough free containers that can be removed\n      val (ref, data) = freeContainers.minBy(_._2.lastUsed)\n      // Catch exception if remaining memory will be negative\n      val remainingMemory = Try(memory - data.memoryLimit).getOrElse(0.B)\n      remove(freeContainers - ref, remainingMemory, toRemove ++ List(ref))\n    } else {\n      // If this is the first call: All containers are in use currently, or there is more memory needed than\n      // containers can be removed.\n      // Or, if this is one of the recursions: Enough containers are found to get the memory, that is\n      // necessary. -> Abort recursion\n      toRemove\n    }\n  }\n\n  /**\n   * Find the expired actor in prewarmedPool\n   *\n   * @param poolConfig\n   * @param prewarmConfig\n   * @param prewarmedPool\n   * @param logging\n   * @return a list of expired actor\n   */\n  def removeExpired[A](poolConfig: ContainerPoolConfig,\n                       prewarmConfig: List[PrewarmingConfig],\n                       prewarmedPool: Map[A, PreWarmedData])(implicit logging: Logging): List[A] = {\n    val now = Deadline.now\n    val expireds = prewarmConfig\n      .flatMap { config =>\n        val kind = config.exec.kind\n        val memory = config.memoryLimit\n        config.reactive\n          .map { c =>\n            val expiredPrewarmedContainer = prewarmedPool.toSeq\n              .filter { warmInfo =>\n                warmInfo match {\n                  case (_, p @ PreWarmedData(_, `kind`, `memory`, _, _)) if p.isExpired() => true\n                  case _                                                                  => false\n                }\n              }\n              .sortBy(_._2.expires.getOrElse(now))\n\n            if (expiredPrewarmedContainer.nonEmpty) {\n              // emit expired container counter metric with memory + kind\n              MetricEmitter.emitCounterMetric(LoggingMarkers.CONTAINER_POOL_PREWARM_EXPIRED(memory.toString, kind))\n              logging.info(\n                this,\n                s\"[kind: ${kind} memory: ${memory.toString}] ${expiredPrewarmedContainer.size} expired prewarmed containers\")\n            }\n            expiredPrewarmedContainer.map(e => (e._1, e._2.expires.getOrElse(now)))\n          }\n          .getOrElse(List.empty)\n      }\n      .sortBy(_._2) //need to sort these so that if the results are limited, we take the oldest\n      .map(_._1)\n    if (expireds.nonEmpty) {\n      logging.info(this, s\"removing up to ${poolConfig.prewarmExpirationLimit} of ${expireds.size} expired containers\")\n      expireds.take(poolConfig.prewarmExpirationLimit).foreach { e =>\n        prewarmedPool.get(e).map { d =>\n          logging.info(this, s\"removing expired prewarm of kind ${d.kind} with container ${d.container} \")\n        }\n      }\n    }\n    expireds.take(poolConfig.prewarmExpirationLimit)\n  }\n\n  /**\n   * Find the increased number for the prewarmed kind\n   *\n   * @param init\n   * @param scheduled\n   * @param coldStartCount\n   * @param prewarmConfig\n   * @param prewarmedPool\n   * @param prewarmStartingPool\n   * @param logging\n   * @return the current number and increased number for the kind in the Map\n   */\n  def increasePrewarms(init: Boolean,\n                       scheduled: Boolean,\n                       coldStartCount: Map[ColdStartKey, Int],\n                       prewarmConfig: List[PrewarmingConfig],\n                       prewarmedPool: Map[ActorRef, PreWarmedData],\n                       prewarmStartingPool: Map[ActorRef, (String, ByteSize)])(\n    implicit logging: Logging): Map[PrewarmingConfig, (Int, Int)] = {\n    prewarmConfig.map { config =>\n      val kind = config.exec.kind\n      val memory = config.memoryLimit\n\n      val runningCount = prewarmedPool.count {\n        // done starting (include expired, since they may not have been removed yet)\n        case (_, p @ PreWarmedData(_, `kind`, `memory`, _, _)) => true\n        // started but not finished starting (or expired)\n        case _ => false\n      }\n      val startingCount = prewarmStartingPool.count(p => p._2._1 == kind && p._2._2 == memory)\n      val currentCount = runningCount + startingCount\n\n      // determine how many are needed\n      val desiredCount: Int =\n        if (init) config.initialCount\n        else {\n          if (scheduled) {\n            // scheduled/reactive config backfill\n            config.reactive\n              .map(c => getReactiveCold(coldStartCount, c, kind, memory).getOrElse(c.minCount)) //reactive -> desired is either cold start driven, or minCount\n              .getOrElse(config.initialCount) //not reactive -> desired is always initial count\n          } else {\n            // normal backfill after removal - make sure at least minCount or initialCount is started\n            config.reactive.map(_.minCount).getOrElse(config.initialCount)\n          }\n        }\n\n      if (currentCount < desiredCount) {\n        logging.info(\n          this,\n          s\"found ${currentCount} started and ${startingCount} starting; ${if (init) \"initing\" else \"backfilling\"} ${desiredCount - currentCount} pre-warms to desired count: ${desiredCount} for kind:${config.exec.kind} mem:${config.memoryLimit.toString}\")(\n          TransactionId.invokerWarmup)\n      }\n      (config, (currentCount, desiredCount))\n    }.toMap\n  }\n\n  /**\n   * Get the required prewarmed container number according to the cold start happened in previous minute\n   *\n   * @param coldStartCount\n   * @param config\n   * @param kind\n   * @param memory\n   * @return the required prewarmed container number\n   */\n  def getReactiveCold(coldStartCount: Map[ColdStartKey, Int],\n                      config: ReactivePrewarmingConfig,\n                      kind: String,\n                      memory: ByteSize): Option[Int] = {\n    coldStartCount.get(ColdStartKey(kind, memory)).map { value =>\n      // Let's assume that threshold is `2`, increment is `1` in runtimes.json\n      // if cold start number in previous minute is `2`, requireCount is `2/2 * 1 = 1`\n      // if cold start number in previous minute is `4`, requireCount is `4/2 * 1 = 2`\n      math.min(math.max(config.minCount, (value / config.threshold) * config.increment), config.maxCount)\n    }\n  }\n\n  def props(factory: ActorRefFactory => ActorRef,\n            poolConfig: ContainerPoolConfig,\n            feed: ActorRef,\n            prewarmConfig: List[PrewarmingConfig] = List.empty)(implicit logging: Logging) =\n    Props(new ContainerPool(factory, feed, prewarmConfig, poolConfig))\n}\n\n/** Contains settings needed to perform container prewarming. */\ncase class PrewarmingConfig(initialCount: Int,\n                            exec: CodeExec[_],\n                            memoryLimit: ByteSize,\n                            reactive: Option[ReactivePrewarmingConfig] = None)\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool\n\nimport org.apache.pekko.actor.Actor\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.pekko.actor.Cancellable\nimport java.time.Instant\n\nimport org.apache.pekko.actor.Status.{Failure => FailureMessage}\nimport org.apache.pekko.actor.{FSM, Props, Stash}\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.pekko.io.IO\nimport org.apache.pekko.io.Tcp\nimport org.apache.pekko.io.Tcp.Close\nimport org.apache.pekko.io.Tcp.CommandFailed\nimport org.apache.pekko.io.Tcp.Connect\nimport org.apache.pekko.io.Tcp.Connected\nimport org.apache.pekko.pattern.pipe\nimport pureconfig.loadConfigOrThrow\nimport pureconfig.generic.auto._\nimport java.net.InetSocketAddress\nimport java.net.SocketException\n\nimport org.apache.openwhisk.common.MetricEmitter\nimport org.apache.openwhisk.common.TransactionId.systemPrefix\n\nimport scala.collection.immutable\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.{Counter, LoggingMarkers, PekkoLogging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.ack.ActiveAck\nimport org.apache.openwhisk.core.connector.{\n  ActivationMessage,\n  CombinedCompletionAndResultMessage,\n  CompletionMessage,\n  ResultMessage\n}\nimport org.apache.openwhisk.core.containerpool.logging.LogCollectingException\nimport org.apache.openwhisk.core.database.UserContext\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.invoker.Invoker.LogsCollector\nimport org.apache.openwhisk.http.Messages\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.util.{Failure, Success}\n\n// States\nsealed trait ContainerState\ncase object Uninitialized extends ContainerState\ncase object Starting extends ContainerState\ncase object Started extends ContainerState\ncase object Running extends ContainerState\ncase object Ready extends ContainerState\ncase object Pausing extends ContainerState\ncase object Paused extends ContainerState\ncase object Removing extends ContainerState\n\n// Data\n/** Base data type */\nsealed abstract class ContainerData(val lastUsed: Instant, val memoryLimit: ByteSize, val activeActivationCount: Int) {\n\n  /** When ContainerProxy in this state is scheduled, it may result in a new state (ContainerData)*/\n  def nextRun(r: Run): ContainerData\n\n  /**\n   *  Return Some(container) (for ContainerStarted instances) or None(for ContainerNotStarted instances)\n   *  Useful for cases where all ContainerData instances are handled, vs cases where only ContainerStarted\n   *  instances are handled */\n  def getContainer: Option[Container]\n\n  /** String to indicate the state of this container after scheduling */\n  val initingState: String\n\n  /** Inidicates whether this container can service additional activations */\n  def hasCapacity(): Boolean\n}\n\n/** abstract type to indicate an unstarted container */\nsealed abstract class ContainerNotStarted(override val lastUsed: Instant,\n                                          override val memoryLimit: ByteSize,\n                                          override val activeActivationCount: Int)\n    extends ContainerData(lastUsed, memoryLimit, activeActivationCount) {\n  override def getContainer = None\n  override val initingState = \"cold\"\n}\n\n/** abstract type to indicate a started container */\nsealed abstract class ContainerStarted(val container: Container,\n                                       override val lastUsed: Instant,\n                                       override val memoryLimit: ByteSize,\n                                       override val activeActivationCount: Int)\n    extends ContainerData(lastUsed, memoryLimit, activeActivationCount) {\n  override def getContainer = Some(container)\n}\n\n/** trait representing a container that is in use and (potentially) usable by subsequent or concurrent activations */\nsealed abstract trait ContainerInUse {\n  val activeActivationCount: Int\n  val action: ExecutableWhiskAction\n  def hasCapacity() =\n    activeActivationCount < action.limits.concurrency.maxConcurrent\n}\n\n/** trait representing a container that is NOT in use and is usable by subsequent activation(s) */\nsealed abstract trait ContainerNotInUse {\n  def hasCapacity() = true\n}\n\n/** type representing a cold (not running) container */\ncase class NoData(override val activeActivationCount: Int = 0)\n    extends ContainerNotStarted(Instant.EPOCH, 0.B, activeActivationCount)\n    with ContainerNotInUse {\n  override def nextRun(r: Run) = WarmingColdData(r.msg.user.namespace.name, r.action, Instant.now, 1)\n}\n\n/** type representing a cold (not running) container with specific memory allocation */\ncase class MemoryData(override val memoryLimit: ByteSize, override val activeActivationCount: Int = 0)\n    extends ContainerNotStarted(Instant.EPOCH, memoryLimit, activeActivationCount)\n    with ContainerNotInUse {\n  override def nextRun(r: Run) = WarmingColdData(r.msg.user.namespace.name, r.action, Instant.now, 1)\n}\n\n/** type representing a prewarmed (running, but unused) container (with a specific memory allocation) */\ncase class PreWarmedData(override val container: Container,\n                         kind: String,\n                         override val memoryLimit: ByteSize,\n                         override val activeActivationCount: Int = 0,\n                         expires: Option[Deadline] = None)\n    extends ContainerStarted(container, Instant.EPOCH, memoryLimit, activeActivationCount)\n    with ContainerNotInUse {\n  override val initingState = \"prewarmed\"\n  override def nextRun(r: Run) =\n    WarmingData(container, r.msg.user.namespace.name, r.action, Instant.now, 1)\n  def isExpired(): Boolean = expires.exists(_.isOverdue())\n}\n\n/** type representing a prewarm (running, but not used) container that is being initialized (for a specific action + invocation namespace) */\ncase class WarmingData(override val container: Container,\n                       invocationNamespace: EntityName,\n                       action: ExecutableWhiskAction,\n                       override val lastUsed: Instant,\n                       override val activeActivationCount: Int = 0)\n    extends ContainerStarted(container, lastUsed, action.limits.memory.megabytes.MB, activeActivationCount)\n    with ContainerInUse {\n  override val initingState = \"warming\"\n  override def nextRun(r: Run) = copy(lastUsed = Instant.now, activeActivationCount = activeActivationCount + 1)\n}\n\n/** type representing a cold (not yet running) container that is being initialized (for a specific action + invocation namespace) */\ncase class WarmingColdData(invocationNamespace: EntityName,\n                           action: ExecutableWhiskAction,\n                           override val lastUsed: Instant,\n                           override val activeActivationCount: Int = 0)\n    extends ContainerNotStarted(lastUsed, action.limits.memory.megabytes.MB, activeActivationCount)\n    with ContainerInUse {\n  override val initingState = \"warmingCold\"\n  override def nextRun(r: Run) = copy(lastUsed = Instant.now, activeActivationCount = activeActivationCount + 1)\n}\n\n/** type representing a warm container that has already been in use (for a specific action + invocation namespace) */\ncase class WarmedData(override val container: Container,\n                      invocationNamespace: EntityName,\n                      action: ExecutableWhiskAction,\n                      override val lastUsed: Instant,\n                      override val activeActivationCount: Int = 0,\n                      resumeRun: Option[Run] = None)\n    extends ContainerStarted(container, lastUsed, action.limits.memory.megabytes.MB, activeActivationCount)\n    with ContainerInUse {\n  override val initingState = \"warmed\"\n  override def nextRun(r: Run) = copy(lastUsed = Instant.now, activeActivationCount = activeActivationCount + 1)\n  //track the resuming run for easily referring to the action being resumed (it may fail and be resent)\n  def withoutResumeRun() = this.copy(resumeRun = None)\n  def withResumeRun(job: Run) = this.copy(resumeRun = Some(job))\n}\n\n// Events received by the actor\ncase class Start(exec: CodeExec[_], memoryLimit: ByteSize, ttl: Option[FiniteDuration] = None)\ncase class Run(action: ExecutableWhiskAction, msg: ActivationMessage, retryLogDeadline: Option[Deadline] = None)\ncase object Remove\ncase class HealthPingEnabled(enabled: Boolean)\n\n// Events sent by the actor\ncase class NeedWork(data: ContainerData)\ncase object ContainerPaused\ncase class ContainerRemoved(replacePrewarm: Boolean) // when container is destroyed\ncase object RescheduleJob // job is sent back to parent and could not be processed because container is being destroyed\ncase class PreWarmCompleted(data: PreWarmedData)\ncase class InitCompleted(data: WarmedData)\ncase object RunCompleted\n\n/**\n * A proxy that wraps a Container. It is used to keep track of the lifecycle\n * of a container and to guarantee a contract between the client of the container\n * and the container itself.\n *\n * The contract is as follows:\n * 1. If action.limits.concurrency.maxConcurrent == 1:\n *    Only one job is to be sent to the ContainerProxy at one time. ContainerProxy\n *    will delay all further jobs until a previous job has finished.\n *\n *    1a. The next job can be sent to the ContainerProxy after it indicates available\n *       capacity by sending NeedWork to its parent.\n *\n * 2. If action.limits.concurrency.maxConcurrent > 1:\n *    Parent must coordinate with ContainerProxy to attempt to send only data.action.limits.concurrency.maxConcurrent\n *    jobs for concurrent processing.\n *\n *    Since the current job count is only periodically sent to parent, the number of jobs\n *    sent to ContainerProxy may exceed data.action.limits.concurrency.maxConcurrent,\n *    in which case jobs are buffered, so that only a max of action.limits.concurrency.maxConcurrent\n *    are ever sent into the container concurrently. Parent will NOT be signalled to send more jobs until\n *    buffered jobs are completed, but their order is not guaranteed.\n *\n *    2a. The next job can be sent to the ContainerProxy after ContainerProxy has \"concurrent capacity\",\n *        indicated by sending NeedWork to its parent.\n *\n * 3. A Remove message can be sent at any point in time. Like multiple jobs though,\n *    it will be delayed until the currently running job finishes.\n *\n * @constructor\n * @param factory a function generating a Container\n * @param sendActiveAck a function sending the activation via active ack\n * @param storeActivation a function storing the activation in a persistent store\n * @param unusedTimeout time after which the container is automatically thrown away\n * @param pauseGrace time to wait for new work before pausing the container\n */\nclass ContainerProxy(factory: (TransactionId,\n                               String,\n                               ImageName,\n                               Boolean,\n                               ByteSize,\n                               Int,\n                               Option[Double],\n                               Option[ExecutableWhiskAction]) => Future[Container],\n                     sendActiveAck: ActiveAck,\n                     storeActivation: (TransactionId, WhiskActivation, Boolean, UserContext) => Future[Any],\n                     collectLogs: LogsCollector,\n                     instance: InvokerInstanceId,\n                     poolConfig: ContainerPoolConfig,\n                     healtCheckConfig: ContainerProxyHealthCheckConfig,\n                     activationErrorLoggingConfig: ContainerProxyActivationErrorLogConfig,\n                     unusedTimeout: FiniteDuration,\n                     pauseGrace: FiniteDuration,\n                     testTcp: Option[ActorRef])\n    extends FSM[ContainerState, ContainerData]\n    with Stash {\n  implicit val ec = context.system.dispatcher\n  implicit val logging = new PekkoLogging(context.system.log)\n  implicit val ac = context.system\n  var rescheduleJob = false // true iff actor receives a job but cannot process it because actor will destroy itself\n  var runBuffer = immutable.Queue.empty[Run] //does not retain order, but does manage jobs that would have pushed past action concurrency limit\n  //track buffer processing state to avoid extra transitions near end of buffer - this provides a pseudo-state between Running and Ready\n  var bufferProcessing = false\n\n  //keep a separate count to avoid confusion with ContainerState.activeActivationCount that is tracked/modified only in ContainerPool\n  var activeCount = 0;\n  var healthPingActor: Option[ActorRef] = None //setup after prewarm starts\n  val tcp: ActorRef = testTcp.getOrElse(IO(Tcp)) //allows to testing interaction with Tcp extension\n\n  startWith(Uninitialized, NoData())\n\n  when(Uninitialized) {\n    // pre warm a container (creates a stem cell container)\n    case Event(job: Start, _) =>\n      factory(\n        TransactionId.invokerWarmup,\n        ContainerProxy.containerName(instance, \"prewarm\", job.exec.kind),\n        job.exec.image,\n        job.exec.pull,\n        job.memoryLimit,\n        poolConfig.cpuShare(job.memoryLimit),\n        poolConfig.cpuLimit(job.memoryLimit),\n        None)\n        .map(container =>\n          PreWarmCompleted(PreWarmedData(container, job.exec.kind, job.memoryLimit, expires = job.ttl.map(_.fromNow))))\n        .pipeTo(self)\n\n      goto(Starting)\n\n    // cold start (no container to reuse or available stem cell container)\n    case Event(job: Run, _) =>\n      implicit val transid = job.msg.transid\n      activeCount += 1\n      // create a new container\n      val container = factory(\n        job.msg.transid,\n        ContainerProxy.containerName(instance, job.msg.user.namespace.name.asString, job.action.name.asString),\n        job.action.exec.image,\n        job.action.exec.pull,\n        job.action.limits.memory.megabytes.MB,\n        poolConfig.cpuShare(job.action.limits.memory.megabytes.MB),\n        poolConfig.cpuLimit(job.action.limits.memory.megabytes.MB),\n        Some(job.action))\n\n      // container factory will either yield a new container ready to execute the action, or\n      // starting up the container failed; for the latter, it's either an internal error starting\n      // a container or a docker action that is not conforming to the required action API\n      container\n        .andThen {\n          case Success(container) =>\n            // the container is ready to accept an activation; register it as PreWarmed; this\n            // normalizes the life cycle for containers and their cleanup when activations fail\n            self ! PreWarmCompleted(\n              PreWarmedData(container, job.action.exec.kind, job.action.limits.memory.megabytes.MB, 1, expires = None))\n\n          case Failure(t) =>\n            // the container did not come up cleanly, so disambiguate the failure mode and then cleanup\n            // the failure is either the system fault, or for docker actions, the application/developer fault\n            val response = t match {\n              case WhiskContainerStartupError(msg) => ActivationResponse.whiskError(msg)\n              case BlackboxStartupError(msg)       => ActivationResponse.developerError(msg)\n              case _                               => ActivationResponse.whiskError(Messages.resourceProvisionError)\n            }\n            val context = UserContext(job.msg.user)\n            // construct an appropriate activation and record it in the datastore,\n            // also update the feed and active ack; the container cleanup is queued\n            // implicitly via a FailureMessage which will be processed later when the state\n            // transitions to Running\n            val activation = ContainerProxy.constructWhiskActivation(job, None, Interval.zero, false, response)\n            sendActiveAck(\n              transid,\n              activation,\n              job.msg.blocking,\n              job.msg.rootControllerIndex,\n              job.msg.user.namespace.uuid,\n              CombinedCompletionAndResultMessage(transid, activation, instance))\n            storeActivation(transid, activation, job.msg.blocking, context)\n        }\n        .flatMap { container =>\n          // now attempt to inject the user code and run the action\n          initializeAndRun(container, job)\n            .map(_ => RunCompleted)\n        }\n        .pipeTo(self)\n\n      goto(Running)\n  }\n\n  when(Starting) {\n    // container was successfully obtained\n    case Event(completed: PreWarmCompleted, _) =>\n      context.parent ! NeedWork(completed.data)\n      goto(Started) using completed.data\n\n    // container creation failed\n    case Event(_: FailureMessage, _) =>\n      context.parent ! ContainerRemoved(true)\n      stop()\n\n    case _ => delay\n  }\n\n  when(Started) {\n    case Event(job: Run, data: PreWarmedData) =>\n      implicit val transid = job.msg.transid\n      activeCount += 1\n      initializeAndRun(data.container, job)\n        .map(_ => RunCompleted)\n        .pipeTo(self)\n      goto(Running) using PreWarmedData(data.container, data.kind, data.memoryLimit, 1, data.expires)\n\n    case Event(Remove, data: PreWarmedData) => destroyContainer(data, false)\n\n    // prewarm container failed\n    case Event(_: FailureMessage, data: PreWarmedData) =>\n      MetricEmitter.emitCounterMetric(LoggingMarkers.INVOKER_CONTAINER_HEALTH_FAILED_PREWARM)\n      destroyContainer(data, true)\n  }\n\n  when(Running) {\n    // Intermediate state, we were able to start a container\n    // and we keep it in case we need to destroy it.\n    case Event(completed: PreWarmCompleted, _) => stay using completed.data\n\n    // Run during prewarm init (for concurrent > 1)\n    case Event(job: Run, data: PreWarmedData) =>\n      implicit val transid = job.msg.transid\n      logging.info(this, s\"buffering for warming container ${data.container}; ${activeCount} activations in flight\")\n      runBuffer = runBuffer.enqueue(job)\n      stay()\n\n    // Run during cold init (for concurrent > 1)\n    case Event(job: Run, _: NoData) =>\n      implicit val transid = job.msg.transid\n      logging.info(this, s\"buffering for cold warming container ${activeCount} activations in flight\")\n      runBuffer = runBuffer.enqueue(job)\n      stay()\n\n    // Init was successful\n    case Event(completed: InitCompleted, _: PreWarmedData) =>\n      processBuffer(completed.data.action, completed.data)\n      stay using completed.data\n\n    // Init was successful\n    case Event(data: WarmedData, _: PreWarmedData) =>\n      //in case concurrency supported, multiple runs can begin as soon as init is complete\n      context.parent ! NeedWork(data)\n      stay using data\n\n    // Run was successful\n    case Event(RunCompleted, data: WarmedData) =>\n      activeCount -= 1\n      val newData = data.withoutResumeRun()\n      //if there are items in runbuffer, process them if there is capacity, and stay; otherwise if we have any pending activations, also stay\n      if (requestWork(data) || activeCount > 0) {\n        stay using newData\n      } else {\n        goto(Ready) using newData\n      }\n    case Event(job: Run, data: WarmedData)\n        if activeCount >= data.action.limits.concurrency.maxConcurrent && !rescheduleJob => //if we are over concurrency limit, and not a failure on resume\n      implicit val transid = job.msg.transid\n      logging.warn(this, s\"buffering for maxed warm container ${data.container}; ${activeCount} activations in flight\")\n      runBuffer = runBuffer.enqueue(job)\n      stay()\n    case Event(job: Run, data: WarmedData)\n        if activeCount < data.action.limits.concurrency.maxConcurrent && !rescheduleJob => //if there was a delay, and not a failure on resume, skip the run\n      activeCount += 1\n      implicit val transid = job.msg.transid\n      bufferProcessing = false //reset buffer processing state\n      initializeAndRun(data.container, job)\n        .map(_ => RunCompleted)\n        .pipeTo(self)\n      stay() using data\n\n    //ContainerHealthError should cause rescheduling of the job\n    case Event(FailureMessage(e: ContainerHealthError), data: WarmedData) =>\n      implicit val tid = e.tid\n      MetricEmitter.emitCounterMetric(LoggingMarkers.INVOKER_CONTAINER_HEALTH_FAILED_WARM)\n      //resend to self will send to parent once we get to Removing state\n      val newData = data.resumeRun\n        .map { run =>\n          logging.warn(this, \"Ready warm container unhealthy, will retry activation.\")\n          self ! run\n          data.withoutResumeRun()\n        }\n        .getOrElse(data)\n      rescheduleJob = true\n      rejectBuffered()\n      destroyContainer(newData, true)\n\n    // Failed after /init (the first run failed) on prewarmed or cold start\n    // - container will be destroyed\n    // - buffered will be aborted (if init fails, we assume it will always fail)\n    case Event(f: FailureMessage, data: PreWarmedData) =>\n      logging.error(\n        this,\n        s\"Failed during init of cold container ${data.getContainer}, queued activations will be aborted.\")\n\n      activeCount -= 1\n      //reuse an existing init failure for any buffered activations that will be aborted\n      val r = f.cause match {\n        case ActivationUnsuccessfulError(r) => Some(r.response)\n        case _                              => None\n      }\n      destroyContainer(data, true, true, r)\n\n    // Failed for a subsequent /run\n    // - container will be destroyed\n    // - buffered will be resent (at least 1 has completed, so others are given a chance to complete)\n    case Event(_: FailureMessage, data: WarmedData) =>\n      logging.error(\n        this,\n        s\"Failed during use of warm container ${data.getContainer}, queued activations will be resent.\")\n      activeCount -= 1\n      if (activeCount == 0) {\n        destroyContainer(data, true)\n      } else {\n        //signal that this container is going away (but don't remove it yet...)\n        rescheduleJob = true\n        goto(Removing)\n      }\n\n    // Failed at getting a container for a cold-start run\n    // - container will be destroyed\n    // - buffered will be aborted (if cold start container fails to start, we assume it will continue to fail)\n    case Event(_: FailureMessage, _) =>\n      logging.error(this, \"Failed to start cold container, queued activations will be aborted.\")\n      activeCount -= 1\n      context.parent ! ContainerRemoved(true)\n      abortBuffered()\n      rescheduleJob = true\n      goto(Removing)\n\n    case _ => delay\n  }\n\n  when(Ready, stateTimeout = pauseGrace) {\n    case Event(job: Run, data: WarmedData) =>\n      implicit val transid = job.msg.transid\n      activeCount += 1\n      val newData = data.withResumeRun(job)\n      initializeAndRun(data.container, job, true)\n        .map(_ => RunCompleted)\n        .pipeTo(self)\n\n      goto(Running) using newData\n\n    // pause grace timed out\n    case Event(StateTimeout, data: WarmedData) =>\n      data.container.suspend()(TransactionId.invokerNanny).map(_ => ContainerPaused).pipeTo(self)\n      goto(Pausing)\n\n    case Event(Remove, data: WarmedData) => destroyContainer(data, true)\n\n    // warm container failed\n    case Event(_: FailureMessage, data: WarmedData) =>\n      destroyContainer(data, true)\n  }\n\n  when(Pausing) {\n    case Event(ContainerPaused, data: WarmedData)   => goto(Paused)\n    case Event(_: FailureMessage, data: WarmedData) => destroyContainer(data, true)\n    case _                                          => delay\n  }\n\n  when(Paused, stateTimeout = unusedTimeout) {\n    case Event(job: Run, data: WarmedData) =>\n      implicit val transid = job.msg.transid\n      activeCount += 1\n      val newData = data.withResumeRun(job)\n      data.container\n        .resume()\n        .andThen {\n          // Sending the message to self on a failure will cause the message\n          // to ultimately be sent back to the parent (which will retry it)\n          // when container removal is done.\n          case Failure(_) =>\n            rescheduleJob = true\n            self ! job\n        }\n        .flatMap(_ => initializeAndRun(data.container, job, true))\n        .map(_ => RunCompleted)\n        .pipeTo(self)\n      goto(Running) using newData\n\n    // container is reclaimed by the pool or it has become too old\n    case Event(StateTimeout | Remove, data: WarmedData) =>\n      rescheduleJob = true // to suppress sending message to the pool and not double count\n      destroyContainer(data, true)\n  }\n\n  when(Removing) {\n    case Event(job: Run, _) =>\n      // Send the job back to the pool to be rescheduled\n      context.parent ! job\n      stay\n    // Run was successful, after another failed concurrent Run\n    case Event(RunCompleted, data: WarmedData) =>\n      activeCount -= 1\n      val newData = data.withoutResumeRun()\n      //if there are items in runbuffer, process them if there is capacity, and stay; otherwise if we have any pending activations, also stay\n      if (activeCount == 0) {\n        destroyContainer(newData, true)\n      } else {\n        stay using newData\n      }\n    case Event(ContainerRemoved(_), _) =>\n      stop()\n    // Run failed, after another failed concurrent Run\n    case Event(_: FailureMessage, data: WarmedData) =>\n      activeCount -= 1\n      val newData = data.withoutResumeRun()\n      if (activeCount == 0) {\n        destroyContainer(newData, true)\n      } else {\n        stay using newData\n      }\n  }\n\n  // Unstash all messages stashed while in intermediate state\n  onTransition {\n    case _ -> Started =>\n      if (healtCheckConfig.enabled) {\n        logging.debug(this, \"enabling health ping on Started\")\n        nextStateData.getContainer.foreach { c =>\n          enableHealthPing(c)\n        }\n      }\n      unstashAll()\n    case _ -> Running =>\n      if (healtCheckConfig.enabled && healthPingActor.isDefined) {\n        logging.debug(this, \"disabling health ping on Running\")\n        disableHealthPing()\n      }\n    case _ -> Ready =>\n      unstashAll()\n    case _ -> Paused =>\n      unstashAll()\n    case _ -> Removing =>\n      unstashAll()\n  }\n\n  initialize()\n\n  /** Either process runbuffer or signal parent to send work; return true if runbuffer is being processed */\n  def requestWork(newData: WarmedData): Boolean = {\n    //if there is concurrency capacity, process runbuffer, signal NeedWork, or both\n    if (activeCount < newData.action.limits.concurrency.maxConcurrent) {\n      if (runBuffer.nonEmpty) {\n        //only request work once, if available larger than runbuffer\n        val available = newData.action.limits.concurrency.maxConcurrent - activeCount\n        val needWork: Boolean = available > runBuffer.size\n        processBuffer(newData.action, newData)\n        if (needWork) {\n          //after buffer processing, then send NeedWork\n          context.parent ! NeedWork(newData)\n        }\n        true\n      } else {\n        context.parent ! NeedWork(newData)\n        bufferProcessing //true in case buffer is still in process\n      }\n    } else {\n      false\n    }\n  }\n\n  /** Process buffered items up to the capacity of action concurrency config */\n  def processBuffer(action: ExecutableWhiskAction, newData: ContainerData) = {\n    //send as many buffered as possible\n    val available = action.limits.concurrency.maxConcurrent - activeCount\n    logging.info(this, s\"resending up to ${available} from ${runBuffer.length} buffered jobs\")\n    1 to available foreach { _ =>\n      runBuffer.dequeueOption match {\n        case Some((run, q)) =>\n          self ! run\n          bufferProcessing = true\n          runBuffer = q\n        case _ =>\n      }\n    }\n  }\n\n  /** Delays all incoming messages until unstashAll() is called */\n  def delay = {\n    stash()\n    stay\n  }\n\n  /**\n   * Destroys the container after unpausing it if needed. Can be used\n   * as a state progression as it goes to Removing.\n   *\n   * @param newData the ContainerStarted which container will be destroyed\n   */\n  def destroyContainer(newData: ContainerStarted,\n                       replacePrewarm: Boolean,\n                       abort: Boolean = false,\n                       abortResponse: Option[ActivationResponse] = None) = {\n    val container = newData.container\n    if (!rescheduleJob) {\n      context.parent ! ContainerRemoved(replacePrewarm)\n    } else {\n      context.parent ! RescheduleJob\n    }\n    val abortProcess = if (abort && runBuffer.nonEmpty) {\n      abortBuffered(abortResponse)\n    } else {\n      rejectBuffered()\n      Future.successful(())\n    }\n\n    val unpause = stateName match {\n      case Paused => container.resume()(TransactionId.invokerNanny)\n      case _      => Future.successful(())\n    }\n\n    unpause\n      .flatMap(_ => container.destroy()(TransactionId.invokerNanny))\n      .flatMap(_ => abortProcess)\n      .map(_ => ContainerRemoved(replacePrewarm))\n      .pipeTo(self)\n    if (stateName != Removing) {\n      goto(Removing) using newData\n    } else {\n      stay using newData\n    }\n  }\n\n  def abortBuffered(abortResponse: Option[ActivationResponse] = None): Future[Any] = {\n    logging.info(this, s\"aborting ${runBuffer.length} queued activations after failed init or failed cold start\")\n    val f = runBuffer.flatMap { job =>\n      implicit val tid = job.msg.transid\n      logging.info(\n        this,\n        s\"aborting activation ${job.msg.activationId} after failed init or cold start with ${abortResponse}\")\n      val result = ContainerProxy.constructWhiskActivation(\n        job,\n        None,\n        Interval.zero,\n        false,\n        abortResponse.getOrElse(ActivationResponse.whiskError(Messages.abnormalRun)))\n      val context = UserContext(job.msg.user)\n      val msg = if (job.msg.blocking) {\n        CombinedCompletionAndResultMessage(tid, result, instance)\n      } else {\n        CompletionMessage(tid, result, instance)\n      }\n      val ack =\n        sendActiveAck(tid, result, job.msg.blocking, job.msg.rootControllerIndex, job.msg.user.namespace.uuid, msg)\n          .andThen {\n            case Failure(e) => logging.error(this, s\"failed to send abort ack $e\")\n          }\n      val store = storeActivation(tid, result, job.msg.blocking, context)\n        .andThen {\n          case Failure(e) => logging.error(this, s\"failed to store aborted activation $e\")\n        }\n      //return both futures\n      Seq(ack, store)\n    }\n    Future.sequence(f)\n  }\n\n  /**\n   * Return any buffered jobs to parent, in case buffer is not empty at removal/error time.\n   */\n  def rejectBuffered() = {\n    //resend any buffered items on container removal\n    if (runBuffer.nonEmpty) {\n      logging.info(this, s\"resending ${runBuffer.size} buffered jobs to parent on container removal\")\n      runBuffer.foreach(context.parent ! _)\n      runBuffer = immutable.Queue.empty[Run]\n    }\n  }\n\n  private def enableHealthPing(c: Container) = {\n    val hpa = healthPingActor.getOrElse {\n      logging.info(this, s\"creating health ping actor for ${c.addr.asString()}\")\n      val hp = context.actorOf(\n        TCPPingClient\n          .props(tcp, c.toString(), healtCheckConfig, new InetSocketAddress(c.addr.host, c.addr.port)))\n      healthPingActor = Some(hp)\n      hp\n    }\n    hpa ! HealthPingEnabled(true)\n  }\n\n  private def disableHealthPing() = {\n    healthPingActor.foreach(_ ! HealthPingEnabled(false))\n  }\n\n  /**\n   * Runs the job, initialize first if necessary.\n   * Completes the job by:\n   * 1. sending an activate ack,\n   * 2. fetching the logs for the run,\n   * 3. indicating the resource is free to the parent pool,\n   * 4. recording the result to the data store\n   *\n   * @param container the container to run the job on\n   * @param job       the job to run\n   * @return a future completing after logs have been collected and\n   *         added to the WhiskActivation\n   */\n  def initializeAndRun(container: Container, job: Run, reschedule: Boolean = false)(\n    implicit tid: TransactionId): Future[WhiskActivation] = {\n    val actionTimeout = job.action.limits.timeout.duration\n    val unlockedArgs =\n      ContainerProxy.unlockArguments(job.msg.content, job.msg.lockedArgs, ParameterEncryption.singleton)\n\n    val (env, parameters) = ContainerProxy.partitionArguments(unlockedArgs, job.msg.initArgs)\n\n    val environment = Map(\n      \"namespace\" -> job.msg.user.namespace.name.toJson,\n      \"action_name\" -> job.msg.action.qualifiedNameWithLeadingSlash.toJson,\n      \"action_version\" -> job.msg.action.version.toJson,\n      \"activation_id\" -> job.msg.activationId.toString.toJson,\n      \"transaction_id\" -> job.msg.transid.id.toJson)\n\n    // if the action requests the api key to be injected into the action context, add it here;\n    // treat a missing annotation as requesting the api key for backward compatibility\n    val authEnvironment = {\n      if (job.action.annotations.isTruthy(Annotations.ProvideApiKeyAnnotationName, valueForNonExistent = true)) {\n        job.msg.user.authkey.toEnvironment.fields\n      } else Map.empty\n    }\n\n    // Only initialize iff we haven't yet warmed the container\n    val initialize = stateData match {\n      case data: WarmedData =>\n        Future.successful(None)\n      case _ =>\n        val owEnv = (authEnvironment ++ environment ++ Map(\n          \"deadline\" -> (Instant.now.toEpochMilli + actionTimeout.toMillis).toString.toJson)) map {\n          case (key, value) => \"__OW_\" + key.toUpperCase -> value\n        }\n\n        container\n          .initialize(\n            job.action.containerInitializer(env ++ owEnv),\n            actionTimeout,\n            job.action.limits.concurrency.maxConcurrent,\n            Some(job.action.toWhiskAction))\n          .map(Some(_))\n    }\n\n    val activation: Future[WhiskActivation] = initialize\n      .flatMap { initInterval =>\n        //immediately setup warmedData for use (before first execution) so that concurrent actions can use it asap\n        if (initInterval.isDefined) {\n          self ! InitCompleted(WarmedData(container, job.msg.user.namespace.name, job.action, Instant.now, 1))\n        }\n\n        val env = authEnvironment ++ environment ++ Map(\n          // compute deadline on invoker side avoids discrepancies inside container\n          // but potentially under-estimates actual deadline\n          \"deadline\" -> (Instant.now.toEpochMilli + actionTimeout.toMillis).toString.toJson)\n\n        container\n          .run(\n            parameters,\n            env.toJson.asJsObject,\n            actionTimeout,\n            job.action.limits.concurrency.maxConcurrent,\n            job.msg.user.limits.allowedMaxPayloadSize,\n            job.msg.user.limits.allowedTruncationSize,\n            reschedule)(job.msg.transid)\n          .map {\n            case (runInterval, response) =>\n              val initRunInterval = initInterval\n                .map(i => Interval(runInterval.start.minusMillis(i.duration.toMillis), runInterval.end))\n                .getOrElse(runInterval)\n              ContainerProxy.constructWhiskActivation(\n                job,\n                initInterval,\n                initRunInterval,\n                runInterval.duration >= actionTimeout,\n                response)\n          }\n      }\n      .recoverWith {\n        case h: ContainerHealthError =>\n          Future.failed(h)\n        case InitializationError(interval, response) =>\n          Future.successful(\n            ContainerProxy\n              .constructWhiskActivation(job, Some(interval), interval, interval.duration >= actionTimeout, response))\n        case t =>\n          // Actually, this should never happen - but we want to make sure to not miss a problem\n          logging.error(this, s\"caught unexpected error while running activation: ${t}\")\n          Future.successful(\n            ContainerProxy.constructWhiskActivation(\n              job,\n              None,\n              Interval.zero,\n              false,\n              ActivationResponse.whiskError(Messages.abnormalRun)))\n      }\n\n    val splitAckMessagesPendingLogCollection = collectLogs.logsToBeCollected(job.action)\n    // Sending an active ack is an asynchronous operation. The result is forwarded as soon as\n    // possible for blocking activations so that dependent activations can be scheduled. The\n    // completion message which frees a load balancer slot is sent after the active ack future\n    // completes to ensure proper ordering.\n    val sendResult = if (job.msg.blocking) {\n      activation.map { result =>\n        val msg =\n          if (splitAckMessagesPendingLogCollection) ResultMessage(tid, result)\n          else CombinedCompletionAndResultMessage(tid, result, instance)\n        sendActiveAck(tid, result, job.msg.blocking, job.msg.rootControllerIndex, job.msg.user.namespace.uuid, msg)\n      }\n    } else {\n      // For non-blocking request, do not forward the result.\n      if (splitAckMessagesPendingLogCollection) Future.successful(())\n      else\n        activation.map { result =>\n          val msg = CompletionMessage(tid, result, instance)\n          sendActiveAck(tid, result, job.msg.blocking, job.msg.rootControllerIndex, job.msg.user.namespace.uuid, msg)\n        }\n    }\n\n    val context = UserContext(job.msg.user)\n\n    // Adds logs to the raw activation.\n    val activationWithLogs: Future[Either[ActivationLogReadingError, WhiskActivation]] = activation\n      .flatMap { activation =>\n        // Skips log collection entirely, if the limit is set to 0\n        if (!splitAckMessagesPendingLogCollection) {\n          Future.successful(Right(activation))\n        } else {\n          val start = tid.started(this, LoggingMarkers.INVOKER_COLLECT_LOGS, logLevel = InfoLevel)\n          collectLogs(tid, job.msg.user, activation, container, job.action)\n            .andThen {\n              case Success(_) => tid.finished(this, start)\n              case Failure(t) => tid.failed(this, start, s\"reading logs failed: $t\")\n            }\n            .map(logs => Right(activation.withLogs(logs)))\n            .recover {\n              case LogCollectingException(logs) =>\n                Left(ActivationLogReadingError(activation.withLogs(logs)))\n              case _ =>\n                Left(ActivationLogReadingError(activation.withLogs(ActivationLogs(Vector(Messages.logFailure)))))\n            }\n        }\n      }\n\n    activationWithLogs\n      .map(_.fold(_.activation, identity))\n      .foreach { activation =>\n        // Sending the completion message to the controller after the active ack ensures proper ordering\n        // (result is received before the completion message for blocking invokes).\n        if (splitAckMessagesPendingLogCollection) {\n          sendResult.onComplete(\n            _ =>\n              sendActiveAck(\n                tid,\n                activation,\n                job.msg.blocking,\n                job.msg.rootControllerIndex,\n                job.msg.user.namespace.uuid,\n                CompletionMessage(tid, activation, instance)))\n        }\n        storeActivation(tid, activation, job.msg.blocking, context)\n      }\n\n    // Disambiguate activation errors and transform the Either into a failed/successful Future respectively.\n    activationWithLogs.flatMap {\n      case Right(act) if act.response.isSuccess || act.response.isApplicationError =>\n        if (act.response.isApplicationError && activationErrorLoggingConfig.applicationErrors) {\n          logTruncatedError(act)\n        }\n        Future.successful(act)\n      case Right(act) =>\n        if ((act.response.isContainerError && activationErrorLoggingConfig.developerErrors) ||\n            (act.response.isWhiskError && activationErrorLoggingConfig.whiskErrors)) {\n          logTruncatedError(act)\n        }\n        Future.failed(ActivationUnsuccessfulError(act))\n      case Left(error) => Future.failed(error)\n    }\n  }\n  //to ensure we don't blow up logs with potentially large activation response error\n  private def logTruncatedError(act: WhiskActivation) = {\n    val truncate = 1024\n    val resultString = act.response.result.map(_.compactPrint).getOrElse(\"[no result]\")\n    val truncatedResult = if (resultString.length > truncate) {\n      s\"${resultString.take(truncate)}...\"\n    } else {\n      resultString\n    }\n    val errorTypeMessage = ActivationResponse.messageForCode(act.response.statusCode)\n    logging.warn(\n      this,\n      s\"Activation ${act.activationId} at container ${stateData.getContainer} (with $activeCount still active) returned a $errorTypeMessage: $truncatedResult\")\n  }\n}\n\nfinal case class ContainerProxyTimeoutConfig(idleContainer: FiniteDuration,\n                                             pauseGrace: FiniteDuration,\n                                             keepingDuration: FiniteDuration)\nfinal case class ContainerProxyHealthCheckConfig(enabled: Boolean, checkPeriod: FiniteDuration, maxFails: Int)\nfinal case class ContainerProxyActivationErrorLogConfig(applicationErrors: Boolean,\n                                                        developerErrors: Boolean,\n                                                        whiskErrors: Boolean)\n\nobject ContainerProxy {\n  def props(factory: (TransactionId,\n                      String,\n                      ImageName,\n                      Boolean,\n                      ByteSize,\n                      Int,\n                      Option[Double],\n                      Option[ExecutableWhiskAction]) => Future[Container],\n            ack: ActiveAck,\n            store: (TransactionId, WhiskActivation, Boolean, UserContext) => Future[Any],\n            collectLogs: LogsCollector,\n            instance: InvokerInstanceId,\n            poolConfig: ContainerPoolConfig,\n            healthCheckConfig: ContainerProxyHealthCheckConfig =\n              loadConfigOrThrow[ContainerProxyHealthCheckConfig](ConfigKeys.containerProxyHealth),\n            activationErrorLogConfig: ContainerProxyActivationErrorLogConfig = activationErrorLogging,\n            unusedTimeout: FiniteDuration = timeouts.idleContainer,\n            pauseGrace: FiniteDuration = timeouts.pauseGrace,\n            tcp: Option[ActorRef] = None) =\n    Props(\n      new ContainerProxy(\n        factory,\n        ack,\n        store,\n        collectLogs,\n        instance,\n        poolConfig,\n        healthCheckConfig,\n        activationErrorLogConfig,\n        unusedTimeout,\n        pauseGrace,\n        tcp))\n\n  // Needs to be thread-safe as it's used by multiple proxies concurrently.\n  private val containerCount = new Counter\n\n  val timeouts = loadConfigOrThrow[ContainerProxyTimeoutConfig](ConfigKeys.containerProxyTimeouts)\n  val activationErrorLogging =\n    loadConfigOrThrow[ContainerProxyActivationErrorLogConfig](ConfigKeys.containerProxyActivationErrorLogs)\n\n  /**\n   * Generates a unique container name.\n   *\n   * @param prefix the container name's prefix\n   * @param suffix the container name's suffix\n   * @return a unique container name\n   */\n  def containerName(instance: InvokerInstanceId, prefix: String, suffix: String): String = {\n    def isAllowed(c: Char): Boolean = c.isLetterOrDigit || c == '_'\n\n    val sanitizedPrefix = prefix.filter(isAllowed)\n    val sanitizedSuffix = suffix.filter(isAllowed)\n\n    s\"${ContainerFactory.containerNamePrefix(instance)}_${containerCount.next()}_${sanitizedPrefix}_${sanitizedSuffix}\"\n  }\n\n  /**\n   * Creates a WhiskActivation ready to be sent via active ack.\n   *\n   * @param job the job that was executed\n   * @param totalInterval the time it took to execute the job\n   * @param response the response to return to the user\n   * @return a WhiskActivation to be sent to the user\n   */\n  def constructWhiskActivation(job: Run,\n                               initInterval: Option[Interval],\n                               totalInterval: Interval,\n                               isTimeout: Boolean,\n                               response: ActivationResponse) = {\n    val causedBy = if (job.msg.causedBySequence) {\n      Some(Parameters(WhiskActivation.causedByAnnotation, JsString(Exec.SEQUENCE)))\n    } else None\n\n    val waitTime = {\n      val end = initInterval.map(_.start).getOrElse(totalInterval.start)\n      Parameters(WhiskActivation.waitTimeAnnotation, Interval(job.msg.transid.meta.start, end).duration.toMillis.toJson)\n    }\n\n    val initTime = {\n      initInterval.map(initTime => Parameters(WhiskActivation.initTimeAnnotation, initTime.duration.toMillis.toJson))\n    }\n\n    val binding =\n      job.msg.action.binding.map(f => Parameters(WhiskActivation.bindingAnnotation, JsString(f.asString)))\n\n    WhiskActivation(\n      activationId = job.msg.activationId,\n      namespace = job.msg.user.namespace.name.toPath,\n      subject = job.msg.user.subject,\n      cause = job.msg.cause,\n      name = job.action.name,\n      version = job.action.version,\n      start = totalInterval.start,\n      end = totalInterval.end,\n      duration = Some(totalInterval.duration.toMillis),\n      response = response,\n      annotations = {\n        Parameters(WhiskActivation.limitsAnnotation, job.action.limits.toJson) ++\n          Parameters(WhiskActivation.pathAnnotation, JsString(job.action.fullyQualifiedName(false).asString)) ++\n          Parameters(WhiskActivation.kindAnnotation, JsString(job.action.exec.kind)) ++\n          Parameters(WhiskActivation.timeoutAnnotation, JsBoolean(isTimeout)) ++\n          causedBy ++ initTime ++ waitTime ++ binding\n      })\n  }\n\n  /**\n   * Partitions the activation arguments into two JsObject instances. The first is exported as intended for export\n   * by the action runtime to the environment. The second is passed on as arguments to the action.\n   *\n   * @param content the activation arguments\n   * @param initArgs set of parameters to treat as initialization arguments\n   * @return A partition of the arguments into an environment variables map and the JsObject argument to the action\n   */\n  def partitionArguments(content: Option[JsValue], initArgs: Set[String]): (Map[String, JsValue], JsValue) = {\n    content match {\n      case None                                       => (Map.empty, JsObject.empty)\n      case Some(JsArray(elements))                    => (Map.empty, JsArray(elements))\n      case Some(JsObject(fields)) if initArgs.isEmpty => (Map.empty, JsObject(fields))\n      case Some(JsObject(fields)) =>\n        val (env, args) = fields.partition(k => initArgs.contains(k._1))\n        (env, JsObject(args))\n    }\n  }\n\n  def unlockArguments(content: Option[JsValue],\n                      lockedArgs: Map[String, String],\n                      decoder: ParameterEncryption): Option[JsValue] = {\n    content match {\n      case Some(JsObject(fields)) =>\n        Some(JsObject(fields.map {\n          case (k, v: JsString) if lockedArgs.contains(k) => (k -> decoder.encryptor(lockedArgs(k)).decrypt(v))\n          case p                                          => p\n        }))\n      // keep the original for other type(e.g. JsArray)\n      case contentValue => contentValue\n    }\n  }\n}\n\nobject TCPPingClient {\n  def props(tcp: ActorRef, containerId: String, config: ContainerProxyHealthCheckConfig, remote: InetSocketAddress) =\n    Props(new TCPPingClient(tcp, containerId, remote, config))\n}\n\nclass TCPPingClient(tcp: ActorRef,\n                    containerId: String,\n                    remote: InetSocketAddress,\n                    config: ContainerProxyHealthCheckConfig)\n    extends Actor {\n  implicit val logging = new PekkoLogging(context.system.log)\n  implicit val ec = context.system.dispatcher\n  implicit var healthPingTx = TransactionId.actionHealthPing\n  case object HealthPingSend\n\n  var scheduledPing: Option[Cancellable] = None\n  var failedCount = 0\n  val addressString = s\"${remote.getHostString}:${remote.getPort}\"\n  restartPing()\n\n  private def restartPing() = {\n    cancelPing() //just in case restart is called twice\n    scheduledPing = Some(\n      context.system.scheduler.scheduleAtFixedRate(config.checkPeriod, config.checkPeriod, self, HealthPingSend))\n  }\n  private def cancelPing() = {\n    scheduledPing.foreach(_.cancel())\n  }\n  def receive = {\n    case HealthPingEnabled(enabled) =>\n      if (enabled) {\n        restartPing()\n      } else {\n        cancelPing()\n      }\n    case HealthPingSend =>\n      healthPingTx = TransactionId(systemPrefix + \"actionHealth\") //reset the tx id each iteration\n      tcp ! Connect(remote)\n    case CommandFailed(_: Connect) =>\n      failedCount += 1\n      if (failedCount == config.maxFails) {\n        logging.error(\n          this,\n          s\"Failed health connection to $containerId ($addressString) $failedCount times - exceeded max ${config.maxFails} failures\")\n        //destroy this container since we cannot communicate with it\n        context.parent ! FailureMessage(\n          new SocketException(s\"Health connection to $containerId ($addressString) failed $failedCount times\"))\n        cancelPing()\n        context.stop(self)\n      } else {\n        logging.warn(this, s\"Failed health connection to $containerId ($addressString) $failedCount times\")\n      }\n\n    case Connected(_, _) =>\n      sender() ! Close\n      if (failedCount > 0) {\n        //reset in case of temp failure\n        logging.info(\n          this,\n          s\"Succeeded health connection to $containerId ($addressString) after $failedCount previous failures\")\n        failedCount = 0\n      } else {\n        logging.debug(this, s\"Succeeded health connection to $containerId ($addressString)\")\n      }\n\n  }\n}\n\n/** Indicates that something went wrong with an activation and the container should be removed */\ntrait ActivationError extends Exception {\n  val activation: WhiskActivation\n}\n\n/** Indicates an activation with a non-successful response */\ncase class ActivationUnsuccessfulError(activation: WhiskActivation) extends ActivationError\n\n/** Indicates reading logs for an activation failed (terminally, truncated) */\ncase class ActivationLogReadingError(activation: WhiskActivation) extends ActivationError\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/docker/DockerCliLogStore.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker\n\nimport java.time.Instant\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.{Logging, PekkoLogging, TransactionId}\nimport org.apache.openwhisk.core.containerpool.Container.ACTIVATION_LOG_SENTINEL\nimport org.apache.openwhisk.core.containerpool.logging.{DockerToActivationLogStore, LogStore, LogStoreProvider}\nimport org.apache.openwhisk.core.containerpool.{Container, ContainerId}\nimport org.apache.openwhisk.core.entity.{ActivationLogs, ExecutableWhiskAction, Identity, WhiskActivation}\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\n\n/**\n * Docker based log store implementation which fetches logs via cli command.\n * This mode is inefficient and is only provided for running in developer modes\n */\nobject DockerCliLogStoreProvider extends LogStoreProvider {\n  override def instance(actorSystem: ActorSystem): LogStore = {\n    //Logger is currently not passed implicitly to LogStoreProvider. So create one explicitly\n    implicit val logger = new PekkoLogging(org.apache.pekko.event.Logging.getLogger(actorSystem, this))\n    new DockerCliLogStore(actorSystem)\n  }\n}\n\nclass DockerCliLogStore(system: ActorSystem)(implicit log: Logging) extends DockerToActivationLogStore(system) {\n  private val client = new ExtendedDockerClient()(system.dispatcher)(log, system)\n  override def collectLogs(transid: TransactionId,\n                           user: Identity,\n                           activation: WhiskActivation,\n                           container: Container,\n                           action: ExecutableWhiskAction): Future[ActivationLogs] = {\n    client\n      .collectLogs(container.containerId, activation.start, activation.end)(transid)\n      .map(logs => ActivationLogs(logs.linesIterator.takeWhile(!_.contains(ACTIVATION_LOG_SENTINEL)).toVector))\n  }\n}\n\nclass ExtendedDockerClient(dockerHost: Option[String] = None)(executionContext: ExecutionContext)(implicit log: Logging,\n                                                                                                  as: ActorSystem)\n    extends DockerClientWithFileAccess(dockerHost)(executionContext)\n    with DockerApiWithFileAccess\n    with WindowsDockerClient {\n\n  implicit private val ec: ExecutionContext = executionContext\n  private val waitForLogs: FiniteDuration = 2.seconds\n  private val logTimeSpanMargin = 1.second\n\n  def collectLogs(id: ContainerId, since: Instant, until: Instant)(implicit transid: TransactionId): Future[String] = {\n    //Add a slight buffer to account for delay writes of logs\n    val end = until.plusSeconds(logTimeSpanMargin.toSeconds)\n    runCmd(\n      Seq(\n        \"logs\",\n        id.asString,\n        \"--since\",\n        since.getEpochSecond.toString,\n        \"--until\",\n        end.getEpochSecond.toString,\n        \"--timestamps\"),\n      waitForLogs)\n  }\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/docker/DockerClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker\n\nimport java.io.FileNotFoundException\nimport java.nio.file.Files\nimport java.nio.file.Paths\nimport java.util.concurrent.Semaphore\n\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.blocking\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.{Await, Future}\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\nimport org.apache.pekko.event.Logging.{ErrorLevel, InfoLevel}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.common.{Logging, LoggingMarkers, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.ContainerId\nimport org.apache.openwhisk.core.containerpool.ContainerAddress\n\nimport scala.concurrent.duration.Duration\n\nobject DockerContainerId {\n\n  val containerIdRegex = \"\"\"^([0-9a-f]{64})$\"\"\".r\n\n  def parse(id: String): Try[ContainerId] = {\n    id match {\n      case containerIdRegex(_) => Success(ContainerId(id))\n      case _                   => Failure(new IllegalArgumentException(s\"Does not comply with Docker container ID format: ${id}\"))\n    }\n  }\n}\n\n/**\n * Configuration for docker client command timeouts.\n */\ncase class DockerClientTimeoutConfig(run: Duration,\n                                     rm: Duration,\n                                     pull: Duration,\n                                     ps: Duration,\n                                     pause: Duration,\n                                     unpause: Duration,\n                                     version: Duration,\n                                     inspect: Duration)\n\n/**\n * Configuration for docker client\n */\ncase class DockerClientConfig(parallelRuns: Int, timeouts: DockerClientTimeoutConfig, maskDockerRunArgs: Boolean)\n\n/**\n * Serves as interface to the docker CLI tool.\n *\n * Be cautious with the ExecutionContext passed to this, as the\n * calls to the CLI are blocking.\n *\n * You only need one instance (and you shouldn't get more).\n */\nclass DockerClient(dockerHost: Option[String] = None,\n                   config: DockerClientConfig = loadConfigOrThrow[DockerClientConfig](ConfigKeys.dockerClient))(\n  executionContext: ExecutionContext)(implicit log: Logging, as: ActorSystem)\n    extends DockerApi\n    with ProcessRunner {\n  implicit private val ec = executionContext\n\n  // Determines how to run docker. Failure to find a Docker binary implies\n  // a failure to initialize this instance of DockerClient.\n  protected val dockerCmd: Seq[String] = {\n    val alternatives = List(\"/usr/bin/docker\", \"/usr/local/bin/docker\") ++ executableAlternatives\n\n    val dockerBin = Try {\n      alternatives.find(a => Files.isExecutable(Paths.get(a))).get\n    } getOrElse {\n      throw new FileNotFoundException(s\"Couldn't locate docker binary (tried: ${alternatives.mkString(\", \")}).\")\n    }\n\n    val host = dockerHost.map(host => Seq(\"--host\", s\"tcp://$host\")).getOrElse(Seq.empty[String])\n    Seq(dockerBin) ++ host\n  }\n\n  protected def executableAlternatives: List[String] = List.empty\n\n  // Invoke docker CLI to determine client version.\n  // If the docker client version cannot be determined, an exception will be thrown and instance initialization will fail.\n  // Rationale: if we cannot invoke `docker version` successfully, it is unlikely subsequent `docker` invocations will succeed.\n  protected def getClientVersion(): String = {\n    val vf = executeProcess(dockerCmd ++ Seq(\"version\", \"--format\", \"{{.Client.Version}}\"), config.timeouts.version)\n      .andThen {\n        case Success(version) => log.info(this, s\"Detected docker client version $version\")\n        case Failure(e) =>\n          log.error(this, s\"Failed to determine docker client version: ${e.getClass} - ${e.getMessage}\")\n      }\n    Await.result(vf, 2 * config.timeouts.version)\n  }\n  val clientVersion: String = getClientVersion()\n\n  protected val maxParallelRuns = config.parallelRuns\n  protected val runSemaphore =\n    new Semaphore( /* permits= */ if (maxParallelRuns > 0) maxParallelRuns else Int.MaxValue, /* fair= */ true)\n\n  // Docker < 1.13.1 has a known problem: if more than 10 containers are created (docker run)\n  // concurrently, there is a good chance that some of them will fail.\n  // See https://github.com/moby/moby/issues/29369\n  // Use a semaphore to make sure that at most 10 `docker run` commands are active\n  // the same time.\n  def run(image: String, args: Seq[String] = Seq.empty[String])(\n    implicit transid: TransactionId): Future[ContainerId] = {\n    Future {\n      blocking {\n        // Acquires a permit from this semaphore, blocking until one is available, or the thread is interrupted.\n        // Throws InterruptedException if the current thread is interrupted\n        runSemaphore.acquire()\n      }\n    }.flatMap { _ =>\n      // Iff the semaphore was acquired successfully\n      runCmd(\n        Seq(\"run\", \"-d\") ++ args ++ Seq(image),\n        config.timeouts.run,\n        if (config.maskDockerRunArgs) Some(Seq(\"run\", \"-d\", \"**ARGUMENTS HIDDEN**\", image)) else None)\n        .andThen {\n          // Release the semaphore as quick as possible regardless of the runCmd() result\n          case _ => runSemaphore.release()\n        }\n        .map(ContainerId.apply)\n        .recoverWith {\n          // https://docs.docker.com/v1.12/engine/reference/run/#/exit-status\n          // Exit status 125 means an error reported by the Docker daemon.\n          // Examples:\n          // - Unrecognized option specified\n          // - Not enough disk space\n          // Exit status 127 means an error that container command cannot be found.\n          // Examples:\n          // - executable file not found in $PATH\": unknown\n          case pre: ProcessUnsuccessfulException\n              if pre.exitStatus == ExitStatus(125) || pre.exitStatus == ExitStatus(127) =>\n            Future.failed(\n              DockerContainerId\n                .parse(pre.stdout)\n                .map(BrokenDockerContainer(_, s\"Broken container: ${pre.getMessage}\", Some(pre.exitStatus.statusValue)))\n                .getOrElse(pre))\n        }\n    }\n  }\n\n  def inspectIPAddress(id: ContainerId, network: String)(implicit transid: TransactionId): Future[ContainerAddress] =\n    runCmd(\n      Seq(\"inspect\", \"--format\", s\"{{.NetworkSettings.Networks.${network}.IPAddress}}\", id.asString),\n      config.timeouts.inspect).flatMap {\n      case \"<no value>\" => Future.failed(new NoSuchElementException)\n      case stdout       => Future.successful(ContainerAddress(stdout))\n    }\n\n  def pause(id: ContainerId)(implicit transid: TransactionId): Future[Unit] =\n    runCmd(Seq(\"pause\", id.asString), config.timeouts.pause).map(_ => ())\n\n  def unpause(id: ContainerId)(implicit transid: TransactionId): Future[Unit] =\n    runCmd(Seq(\"unpause\", id.asString), config.timeouts.unpause).map(_ => ())\n\n  def rm(id: ContainerId)(implicit transid: TransactionId): Future[Unit] =\n    runCmd(Seq(\"rm\", \"-f\", id.asString), config.timeouts.rm).map(_ => ())\n\n  def ps(filters: Seq[(String, String)] = Seq.empty, all: Boolean = false)(\n    implicit transid: TransactionId): Future[Seq[ContainerId]] = {\n    val filterArgs = filters.flatMap { case (attr, value) => Seq(\"--filter\", s\"$attr=$value\") }\n    val allArg = if (all) Seq(\"--all\") else Seq.empty[String]\n    val cmd = Seq(\"ps\", \"--quiet\", \"--no-trunc\") ++ allArg ++ filterArgs\n    runCmd(cmd, config.timeouts.ps).map(_.linesIterator.toSeq.map(ContainerId.apply))\n  }\n\n  /**\n   * Stores pulls that are currently being executed and collapses multiple\n   * pulls into just one. After a pull is finished, the cached future is removed\n   * to enable constant updates of an image without changing its tag.\n   */\n  private val pullsInFlight = TrieMap[String, Future[Unit]]()\n  def pull(image: String)(implicit transid: TransactionId): Future[Unit] =\n    pullsInFlight.getOrElseUpdate(image, {\n      runCmd(Seq(\"pull\", image), config.timeouts.pull).map(_ => ()).andThen { case _ => pullsInFlight.remove(image) }\n    })\n\n  def isOomKilled(id: ContainerId)(implicit transid: TransactionId): Future[Boolean] =\n    runCmd(Seq(\"inspect\", id.asString, \"--format\", \"{{.State.OOMKilled}}\"), config.timeouts.inspect).map(_.toBoolean)\n\n  protected def runCmd(args: Seq[String], timeout: Duration, maskedArgs: Option[Seq[String]] = None)(\n    implicit transid: TransactionId): Future[String] = {\n    val cmd = dockerCmd ++ args\n    val start = transid.started(\n      this,\n      LoggingMarkers.INVOKER_DOCKER_CMD(args.head),\n      s\"running ${maskedArgs.map(maskedArgs => (dockerCmd ++ maskedArgs).mkString(\" \")).getOrElse(cmd.mkString(\" \"))} (timeout: $timeout)\",\n      logLevel = InfoLevel)\n    executeProcess(cmd, timeout).andThen {\n      case Success(_) => transid.finished(this, start)\n      case Failure(pte: ProcessTimeoutException) =>\n        transid.failed(this, start, pte.getMessage, ErrorLevel)\n      case Failure(t) => transid.failed(this, start, t.getMessage, ErrorLevel)\n    }\n  }\n}\n\ntrait DockerApi {\n\n  /**\n   * The version number of the docker client cli\n   *\n   * @return The version of the docker client cli being used by the invoker\n   */\n  def clientVersion: String\n\n  /**\n   * Spawns a container in detached mode.\n   *\n   * @param image the image to start the container with\n   * @param args arguments for the docker run command\n   * @return id of the started container\n   */\n  def run(image: String, args: Seq[String] = Seq.empty[String])(implicit transid: TransactionId): Future[ContainerId]\n\n  /**\n   * Gets the IP address of a given container.\n   *\n   * A container may have more than one network. The container has an\n   * IP address in each of these networks such that the network name\n   * is needed.\n   *\n   * @param id the id of the container to get the IP address from\n   * @param network name of the network to get the IP address from\n   * @return ip of the container\n   */\n  def inspectIPAddress(id: ContainerId, network: String)(implicit transid: TransactionId): Future[ContainerAddress]\n\n  /**\n   * Pauses the container with the given id.\n   *\n   * @param id the id of the container to pause\n   * @return a Future completing according to the command's exit-code\n   */\n  def pause(id: ContainerId)(implicit transid: TransactionId): Future[Unit]\n\n  /**\n   * Unpauses the container with the given id.\n   *\n   * @param id the id of the container to unpause\n   * @return a Future completing according to the command's exit-code\n   */\n  def unpause(id: ContainerId)(implicit transid: TransactionId): Future[Unit]\n\n  /**\n   * Removes the container with the given id.\n   *\n   * @param id the id of the container to remove\n   * @return a Future completing according to the command's exit-code\n   */\n  def rm(id: ContainerId)(implicit transid: TransactionId): Future[Unit]\n\n  /**\n   * Returns a list of ContainerIds in the system.\n   *\n   * @param filters Filters to apply to the 'ps' command\n   * @param all Whether or not to return stopped containers as well\n   * @return A list of ContainerIds\n   */\n  def ps(filters: Seq[(String, String)] = Seq.empty, all: Boolean = false)(\n    implicit transid: TransactionId): Future[Seq[ContainerId]]\n\n  /**\n   * Pulls the given image.\n   *\n   * @param image the image to pull\n   * @return a Future completing once the pull is complete\n   */\n  def pull(image: String)(implicit transid: TransactionId): Future[Unit]\n\n  /**\n   * Determines whether the given container was killed due to\n   * memory constraints.\n   *\n   * @param id the id of the container to check\n   * @return a Future containing whether the container was killed or not\n   */\n  def isOomKilled(id: ContainerId)(implicit transid: TransactionId): Future[Boolean]\n}\n\n/** Indicates any error while starting a container that leaves a broken container behind that needs to be removed */\ncase class BrokenDockerContainer(id: ContainerId, msg: String, existStatus: Option[Int] = None) extends Exception(msg)\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/docker/DockerClientWithFileAccess.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker\n\nimport java.io.File\nimport java.nio.file.Paths\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.connectors.file.scaladsl.FileTailSource\nimport org.apache.pekko.stream.scaladsl.{FileIO, Source => PekkoSource}\nimport org.apache.pekko.util.ByteString\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.concurrent.blocking\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.ContainerId\nimport org.apache.openwhisk.core.containerpool.ContainerAddress\n\nimport scala.io.Source\nimport scala.concurrent.duration.FiniteDuration\n\nclass DockerClientWithFileAccess(dockerHost: Option[String] = None,\n                                 containersDirectory: File = Paths.get(\"containers\").toFile)(\n  executionContext: ExecutionContext)(implicit log: Logging, as: ActorSystem)\n    extends DockerClient(dockerHost)(executionContext)\n    with DockerApiWithFileAccess {\n\n  implicit private val ec = executionContext\n\n  /**\n   * Provides the home directory of the specified Docker container.\n   *\n   * Assumes that property \"containersDirectory\" holds the location of the\n   * home directory of all Docker containers. Default: directory \"containers\"\n   * in the current working directory.\n   *\n   * Does not verify that the returned directory actually exists.\n   *\n   * @param containerId Id of the desired Docker container\n   * @return canonical location of the container's home directory\n   */\n  protected def containerDirectory(containerId: ContainerId) = {\n    new File(containersDirectory, containerId.asString).getCanonicalFile()\n  }\n\n  /**\n   * Provides the configuration file of the specified Docker container.\n   *\n   * Assumes that the file has the well-known location and name.\n   *\n   * Does not verify that the returned file actually exists.\n   *\n   * @param containerId Id of the desired Docker container\n   * @return canonical location of the container's configuration file\n   */\n  protected def containerConfigFile(containerId: ContainerId) = {\n    new File(containerDirectory(containerId), \"config.v2.json\").getCanonicalFile()\n  }\n\n  /**\n   * Provides the log file of the specified Docker container written by\n   * Docker's JSON log driver.\n   *\n   * Assumes that the file has the well-known location and name.\n   *\n   * Does not verify that the returned file actually exists.\n   *\n   * @param containerId Id of the desired Docker container\n   * @return canonical location of the container's log file\n   */\n  protected def containerLogFile(containerId: ContainerId) = {\n    new File(containerDirectory(containerId), s\"${containerId.asString}-json.log\").getCanonicalFile()\n  }\n\n  /**\n   * Provides the contents of the specified Docker container's configuration\n   * file as JSON object.\n   *\n   * @param configFile the container's configuration file in JSON format\n   * @return contents of configuration file as JSON object\n   */\n  protected def configFileContents(configFile: File): Future[JsObject] = Future {\n    blocking { // Needed due to synchronous file operations\n      val source = Source.fromFile(configFile)\n      val config = try source.mkString\n      finally source.close()\n      config.parseJson.asJsObject\n    }\n  }\n\n  /**\n   * Extracts the IP of the container from the local config file of the docker daemon.\n   *\n   * A container may have more than one network. The container has an\n   * IP address in each of these networks such that the network name\n   * is needed.\n   *\n   * @param id the id of the container to get the IP address from\n   * @param network name of the network to get the IP address from\n   * @return the ip address of the container\n   */\n  protected def ipAddressFromFile(id: ContainerId, network: String): Future[ContainerAddress] = {\n    configFileContents(containerConfigFile(id)).map { json =>\n      val networks = json.fields(\"NetworkSettings\").asJsObject.fields(\"Networks\").asJsObject\n      val specifiedNetwork = networks.fields(network).asJsObject\n      val ipAddr = specifiedNetwork.fields(\"IPAddress\")\n      ContainerAddress(ipAddr.convertTo[String])\n    }\n  }\n\n  // See extended trait for description\n  override def inspectIPAddress(id: ContainerId, network: String)(\n    implicit transid: TransactionId): Future[ContainerAddress] = {\n    ipAddressFromFile(id, network).recoverWith {\n      case _ => super.inspectIPAddress(id, network)\n    }\n  }\n\n  override def isOomKilled(id: ContainerId)(implicit transid: TransactionId): Future[Boolean] =\n    configFileContents(containerConfigFile(id))\n      .map(_.fields(\"State\").asJsObject.fields(\"OOMKilled\").convertTo[Boolean])\n      .recover { case _ => false }\n\n  private val readChunkSize = 8192 // bytes\n  override def rawContainerLogs(containerId: ContainerId,\n                                fromPos: Long,\n                                pollInterval: Option[FiniteDuration]): PekkoSource[ByteString, Any] =\n    try {\n      // If there is no waiting interval, we can end the stream early by reading just what is there from file.\n      pollInterval match {\n        case Some(interval) => FileTailSource(containerLogFile(containerId).toPath, readChunkSize, fromPos, interval)\n        case None           => FileIO.fromPath(containerLogFile(containerId).toPath, readChunkSize, fromPos)\n      }\n    } catch {\n      case t: Throwable => PekkoSource.failed(t)\n    }\n}\n\ntrait DockerApiWithFileAccess extends DockerApi {\n\n  /**\n   * Reads logs from the container written json-log file and returns them\n   * streamingly in bytes.\n   *\n   * @param containerId id of the container to get the logs for\n   * @param fromPos position to start to read in the file\n   * @param pollInterval interval to poll for changes of the file\n   * @return a source emitting chunks read from the log-file\n   */\n  def rawContainerLogs(containerId: ContainerId,\n                       fromPos: Long,\n                       pollInterval: Option[FiniteDuration]): PekkoSource[ByteString, Any]\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/docker/DockerContainer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker\n\nimport java.time.Instant\nimport java.util.concurrent.TimeoutException\nimport java.util.concurrent.atomic.AtomicLong\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream._\nimport org.apache.pekko.stream.scaladsl.Framing.FramingException\nimport spray.json._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.concurrent.duration._\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.entity.ActivationResponse.{ConnectionError, MemoryExhausted}\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.pekko.stream.scaladsl.{Framing, Source}\nimport org.apache.pekko.stream.stage._\nimport org.apache.pekko.util.ByteString\nimport spray.json._\nimport org.apache.openwhisk.core.containerpool.logging.LogLine\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.http.Messages\n\nobject DockerContainer {\n\n  private val byteStringSentinel = ByteString(Container.ACTIVATION_LOG_SENTINEL)\n\n  /**\n   * Creates a container running on a docker daemon.\n   *\n   * @param transid transaction creating the container\n   * @param image either a user provided (Left) or OpenWhisk provided (Right) image\n   * @param memory memorylimit of the container\n   * @param cpuShares sharefactor for the container\n   * @param environment environment variables to set on the container\n   * @param network network to launch the container in\n   * @param dnsServers list of dns servers to use in the container\n   * @param name optional name for the container\n   * @param useRunc use runc to pause/unpause container?\n   * @return a Future which either completes with a DockerContainer or one of two specific failures\n   */\n  def create(transid: TransactionId,\n             image: Either[ImageName, ImageName],\n             registryConfig: Option[RuntimesRegistryConfig] = None,\n             memory: ByteSize = 256.MB,\n             cpuShares: Int = 0,\n             cpuLimit: Option[Double] = None,\n             environment: Map[String, String] = Map.empty,\n             network: String = \"bridge\",\n             dnsServers: Seq[String] = Seq.empty,\n             dnsSearch: Seq[String] = Seq.empty,\n             dnsOptions: Seq[String] = Seq.empty,\n             name: Option[String] = None,\n             useRunc: Boolean = true,\n             dockerRunParameters: Map[String, Set[String]])(implicit docker: DockerApiWithFileAccess,\n                                                            runc: RuncApi,\n                                                            as: ActorSystem,\n                                                            ec: ExecutionContext,\n                                                            log: Logging): Future[DockerContainer] = {\n    implicit val tid: TransactionId = transid\n\n    val environmentArgs = environment.flatMap {\n      case (key, value) => Seq(\"-e\", s\"$key=$value\")\n    }\n\n    val params = dockerRunParameters.flatMap {\n      case (key, valueList) => valueList.toList.flatMap(Seq(key, _))\n    }\n\n    // NOTE: --dns-option on modern versions of docker, but is --dns-opt on docker 1.12\n    val dnsOptString = if (docker.clientVersion.startsWith(\"1.12\")) { \"--dns-opt\" } else { \"--dns-option\" }\n    val args = Seq(\n      \"--cpu-shares\",\n      cpuShares.toString,\n      \"--memory\",\n      s\"${memory.toMB}m\",\n      \"--memory-swap\",\n      s\"${memory.toMB}m\",\n      \"--network\",\n      network) ++\n      environmentArgs ++\n      dnsServers.flatMap(d => Seq(\"--dns\", d)) ++\n      dnsSearch.flatMap(d => Seq(\"--dns-search\", d)) ++\n      dnsOptions.flatMap(d => Seq(dnsOptString, d)) ++\n      name.map(n => Seq(\"--name\", n)).getOrElse(Seq.empty) ++\n      cpuLimit.map(c => Seq(\"--cpus\", c.toString)).getOrElse(Seq.empty) ++\n      params\n\n    val registryConfigUrl = registryConfig.map(_.url).getOrElse(\"\")\n    val imageToUse = image.merge.resolveImageName(Some(registryConfigUrl))\n\n    val pulled = image match {\n      case Left(userProvided) if userProvided.tag.map(_ == \"latest\").getOrElse(true) =>\n        // Iff the image tag is \"latest\" explicitly (or implicitly because no tag is given at all), failing to pull will\n        // fail the whole container bringup process, because it is expected to pick up the very latest \"untagged\"\n        // version every time.\n        docker.pull(imageToUse).map(_ => true).recoverWith {\n          case _ => Future.failed(BlackboxStartupError(Messages.imagePullError(imageToUse)))\n        }\n      case Left(_) =>\n        // Iff the image tag is something else than latest, we tolerate an outdated image if one is available locally.\n        // A `docker run` will be tried nonetheless to try to start a container (which will succeed if the image is\n        // already available locally)\n        docker.pull(imageToUse).map(_ => true).recover { case _ => false }\n      case Right(_) =>\n        // Iff we're not pulling at all (OpenWhisk provided image) we act as if the pull was successful.\n        Future.successful(true)\n    }\n\n    for {\n      pullSuccessful <- pulled\n      id <- docker.run(imageToUse, args).recoverWith {\n        case BrokenDockerContainer(brokenId, _, exitStatus) if exitStatus.isEmpty || exitStatus.contains(125) =>\n          // Remove the broken container - but don't wait or check for the result.\n          // If the removal fails, there is nothing we could do to recover from the recovery.\n          docker.rm(brokenId)\n          Future.failed(WhiskContainerStartupError(Messages.resourceProvisionError))\n        case BrokenDockerContainer(brokenId, _, exitStatus) if exitStatus.contains(127) =>\n          docker.rm(brokenId)\n          Future.failed(BlackboxStartupError(s\"${Messages.commandNotFoundError} in image ${imageToUse}\"))\n        case _ =>\n          // Iff the pull was successful, we assume that the error is not due to an image pull error, otherwise\n          // the docker run was a backup measure to try and start the container anyway. If it fails again, we assume\n          // the image could still not be pulled and wasn't available locally.\n          if (pullSuccessful) {\n            Future.failed(WhiskContainerStartupError(Messages.resourceProvisionError))\n          } else {\n            Future.failed(BlackboxStartupError(Messages.imagePullError(imageToUse)))\n          }\n      }\n      ip <- docker.inspectIPAddress(id, network).recoverWith {\n        // remove the container immediately if inspect failed as\n        // we cannot recover that case automatically\n        case _ =>\n          docker.rm(id)\n          Future.failed(WhiskContainerStartupError(Messages.resourceProvisionError))\n      }\n    } yield new DockerContainer(id, ip, useRunc)\n  }\n}\n\n/**\n * Represents a container as run by docker.\n *\n * This class contains OpenWhisk specific behavior and as such does not necessarily\n * use docker commands to achieve the effects needed.\n *\n * @constructor\n * @param id the id of the container\n * @param addr the ip of the container\n */\nclass DockerContainer(protected val id: ContainerId,\n                      protected[core] val addr: ContainerAddress,\n                      protected val useRunc: Boolean)(implicit docker: DockerApiWithFileAccess,\n                                                      runc: RuncApi,\n                                                      override protected val as: ActorSystem,\n                                                      protected val ec: ExecutionContext,\n                                                      protected val logging: Logging)\n    extends Container {\n\n  /** The last read-position in the log file */\n  private var logFileOffset = new AtomicLong(0)\n\n  protected val logCollectingIdleTimeout: FiniteDuration = 2.seconds\n  protected val logCollectingTimeoutPerMBLogLimit: FiniteDuration = 2.seconds\n  protected val waitForOomState: FiniteDuration = 2.seconds\n  protected val filePollInterval: FiniteDuration = 5.milliseconds\n\n  override def suspend()(implicit transid: TransactionId): Future[Unit] = {\n    super.suspend().flatMap(_ => if (useRunc) runc.pause(id) else docker.pause(id))\n  }\n  override def resume()(implicit transid: TransactionId): Future[Unit] = {\n    (if (useRunc) { runc.resume(id) } else { docker.unpause(id) }).flatMap(_ => super.resume())\n  }\n  override def destroy()(implicit transid: TransactionId): Future[Unit] = {\n    super.destroy()\n    docker.rm(id)\n  }\n\n  /**\n   * Was the container killed due to memory exhaustion?\n   *\n   * Retries because as all docker state-relevant operations, they won't\n   * be reflected by the respective commands immediately but will take\n   * some time to be propagated.\n   *\n   * @param retries number of retries to make\n   * @return a Future indicating a memory exhaustion situation\n   */\n  private def isOomKilled(retries: Int = (waitForOomState / filePollInterval).toInt)(\n    implicit transid: TransactionId): Future[Boolean] = {\n    docker.isOomKilled(id)(TransactionId.invoker).flatMap { killed =>\n      if (killed) Future.successful(true)\n      else if (retries > 0) org.apache.pekko.pattern.after(filePollInterval, as.scheduler)(isOomKilled(retries - 1))\n      else Future.successful(false)\n    }\n  }\n\n  override protected def callContainer(\n    path: String,\n    body: JsObject,\n    timeout: FiniteDuration,\n    maxConcurrent: Int,\n    maxResponse: ByteSize,\n    truncation: ByteSize,\n    retry: Boolean = false,\n    reschedule: Boolean = false)(implicit transid: TransactionId): Future[RunResult] = {\n    val started = Instant.now()\n    val http = httpConnection.getOrElse {\n      val conn = if (Container.config.pekkoClient) {\n        new PekkoContainerClient(addr.host, addr.port, timeout, 1024)\n      } else {\n        new ApacheBlockingContainerClient(s\"${addr.host}:${addr.port}\", timeout, maxConcurrent)\n      }\n      httpConnection = Some(conn)\n      conn\n    }\n\n    http\n      .post(path, body, maxResponse, truncation, retry, reschedule)\n      .flatMap { response =>\n        val finished = Instant.now()\n\n        response.left\n          .map {\n            // Only check for memory exhaustion if there was a\n            // terminal connection error.\n            case error: ConnectionError =>\n              isOomKilled().map {\n                case true  => MemoryExhausted()\n                case false => error\n              }\n            case other => Future.successful(other)\n          }\n          .fold(_.map(Left(_)), right => Future.successful(Right(right)))\n          .map(res => RunResult(Interval(started, finished), res))\n      }\n  }\n\n  /**\n   * Obtains the container's stdout and stderr output and converts it to our own JSON format.\n   * At the moment, this is done by reading the internal Docker log file for the container.\n   * Said file is written by Docker's JSON log driver and has a \"well-known\" location and name.\n   *\n   * For warm containers, the container log file already holds output from\n   * previous activations that have to be skipped. For this reason, a starting position\n   * is kept and updated upon each invocation.\n   *\n   * There are two possible modes controlled by parameter waitForSentinel:\n   *\n   * 1. Wait for sentinel:\n   *    Tail container log file until two sentinel markers show up. Complete\n   *    once two sentinel markers have been identified, regardless whether more\n   *    data could be read from container log file.\n   *    A log file reading error is reported if sentinels cannot be found.\n   *    Managed action runtimes use the the sentinels to mark the end of\n   *    an individual activation.\n   *\n   * 2. Do not wait for sentinel:\n   *    Read container log file up to its end. Stop reading once the end\n   *    has been reached. Complete once two sentinel markers have been\n   *    identified, regardless whether more data could be read from\n   *    container log file.\n   *    No log file reading error is reported if sentinels cannot be found.\n   *    Blackbox actions do not necessarily produce marker sentinels properly,\n   *    so this mode is used for all blackbox actions.\n   *    In addition, this mode can / should be used in error situations with\n   *    managed action runtimes where sentinel markers may be missing or\n   *    arrive too late - Example: action exceeds time or memory limit during\n   *    init or run.\n   *\n   * The result returned from this method does never contain any log sentinel markers. These are always\n   * filtered - regardless of the specified waitForSentinel mode.\n   *\n   * Only parses and returns as much logs as fit in the passed log limit. Stops log collection with an error\n   * if processing takes too long or time gaps between processing individual log lines are too long.\n   *\n   * @param limit the limit to apply to the log size\n   * @param waitForSentinel determines if the processor should wait for a sentinel to appear\n   * @return a vector of Strings with log lines in our own JSON format\n   */\n  def logs(limit: ByteSize, waitForSentinel: Boolean)(implicit transid: TransactionId): Source[ByteString, Any] = {\n    // Define a time limit for collecting and processing the logs of a single activation.\n    // If this time limit is exceeded, log processing is stopped and declared unsuccessful.\n    // Calculate the timeout based on the maximum expected log size, i.e. the log limit.\n    // Use a lower bound of 5 MB log size to account for base overhead.\n    val logCollectingTimeout = limit.toMB.toInt.max(5) * logCollectingTimeoutPerMBLogLimit\n\n    docker\n      .rawContainerLogs(id, logFileOffset.get(), if (waitForSentinel) Some(filePollInterval) else None)\n      // This stage only throws 'FramingException' so we cannot decide whether we got truncated due to a size\n      // constraint (like StreamLimitReachedException below) or due to the file being truncated itself.\n      .via(Framing.delimiter(delimiter, limit.toBytes.toInt))\n      .limitWeighted(limit.toBytes) { obj =>\n        // Adding + 1 since we know there's a newline byte being read\n        val size = obj.size + 1\n        logFileOffset.addAndGet(size)\n        size\n      }\n      .via(new CompleteAfterOccurrences(_.containsSlice(DockerContainer.byteStringSentinel), 2, waitForSentinel))\n      // As we're reading the logs after the activation has finished the invariant is that all loglines are already\n      // written and we mostly await them being flushed by the docker daemon. Therefore we can time out based on the time\n      // between two loglines appear without relying on the log frequency in the action itself.\n      .idleTimeout(logCollectingIdleTimeout)\n      // Apply an overall time limit for this log collecting and processing stream.\n      .completionTimeout(logCollectingTimeout)\n      .recover {\n        case _: StreamLimitReachedException =>\n          // While the stream has already ended by failing the limitWeighted stage above, we inject a truncation\n          // notice downstream, which will be processed as usual. This will be the last element of the stream.\n          ByteString(LogLine(Instant.now.toString, \"stderr\", Messages.truncateLogs(limit)).toJson.compactPrint)\n        case _: OccurrencesNotFoundException | _: FramingException | _: TimeoutException =>\n          // Stream has already ended and we insert a notice that data might be missing from the logs. While a\n          // FramingException can also mean exceeding the limits, we cannot decide which case happened so we resort\n          // to the general error message. This will be the last element of the stream.\n          ByteString(LogLine(Instant.now.toString, \"stderr\", Messages.logFailure).toJson.compactPrint)\n      }\n  }\n\n  /** Delimiter used to split log-lines as written by the json-log-driver. */\n  private val delimiter = ByteString(\"\\n\")\n}\n\n/**\n * Completes the stream once the given predicate is fulfilled by N events in the stream.\n *\n * '''Emits when''' an upstream element arrives and does not fulfill the predicate\n *\n * '''Backpressures when''' downstream backpressures\n *\n * '''Completes when''' upstream completes or predicate is fulfilled N times\n *\n * '''Cancels when''' downstream cancels\n *\n * '''Errors when''' stream completes, not enough occurrences have been found and errorOnNotEnough is true\n */\nclass CompleteAfterOccurrences[T](isInEvent: T => Boolean, neededOccurrences: Int, errorOnNotEnough: Boolean)\n    extends GraphStage[FlowShape[T, T]] {\n  val in: Inlet[T] = Inlet[T](\"WaitForOccurrences.in\")\n  val out: Outlet[T] = Outlet[T](\"WaitForOccurrences.out\")\n  override val shape: FlowShape[T, T] = FlowShape.of(in, out)\n\n  override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =\n    new GraphStageLogic(shape) with InHandler with OutHandler {\n      private var occurrencesFound = 0\n\n      override def onPull(): Unit = pull(in)\n\n      override def onPush(): Unit = {\n        val element = grab(in)\n        val isOccurrence = isInEvent(element)\n\n        if (isOccurrence) occurrencesFound += 1\n\n        if (occurrencesFound >= neededOccurrences) {\n          completeStage()\n        } else {\n          if (isOccurrence) {\n            pull(in)\n          } else {\n            push(out, element)\n          }\n        }\n      }\n\n      override def onUpstreamFinish(): Unit = {\n        if (occurrencesFound >= neededOccurrences || !errorOnNotEnough) {\n          completeStage()\n        } else {\n          failStage(OccurrencesNotFoundException(neededOccurrences, occurrencesFound))\n        }\n      }\n\n      setHandlers(in, out, this)\n    }\n}\n\n/** Indicates that Occurrences have not been found in the stream */\ncase class OccurrencesNotFoundException(neededCount: Int, actualCount: Int)\n    extends RuntimeException(s\"Only found $actualCount out of $neededCount occurrences.\")\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/docker/DockerContainerFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker\n\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.concurrent.Await\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.ExecManifest\nimport org.apache.openwhisk.core.entity.InvokerInstanceId\n\nimport scala.concurrent.duration._\nimport java.util.concurrent.TimeoutException\n\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.core.ConfigKeys\n\ncase class DockerContainerFactoryConfig(useRunc: Boolean)\n\nclass DockerContainerFactory(instance: InvokerInstanceId,\n                             parameters: Map[String, Set[String]],\n                             containerArgsConfig: ContainerArgsConfig =\n                               loadConfigOrThrow[ContainerArgsConfig](ConfigKeys.containerArgs),\n                             protected val runtimesRegistryConfig: RuntimesRegistryConfig =\n                               loadConfigOrThrow[RuntimesRegistryConfig](ConfigKeys.runtimesRegistry),\n                             protected val userImagesRegistryConfig: RuntimesRegistryConfig =\n                               loadConfigOrThrow[RuntimesRegistryConfig](ConfigKeys.userImagesRegistry),\n                             dockerContainerFactoryConfig: DockerContainerFactoryConfig =\n                               loadConfigOrThrow[DockerContainerFactoryConfig](ConfigKeys.dockerContainerFactory))(\n  implicit actorSystem: ActorSystem,\n  ec: ExecutionContext,\n  logging: Logging,\n  docker: DockerApiWithFileAccess,\n  runc: RuncApi)\n    extends ContainerFactory {\n\n  /** Create a container using docker cli */\n  override def createContainer(\n    tid: TransactionId,\n    name: String,\n    actionImage: ExecManifest.ImageName,\n    userProvidedImage: Boolean,\n    memory: ByteSize,\n    cpuShares: Int,\n    cpuLimit: Option[Double])(implicit config: WhiskConfig, logging: Logging): Future[Container] = {\n    val registryConfig =\n      ContainerFactory.resolveRegistryConfig(userProvidedImage, runtimesRegistryConfig, userImagesRegistryConfig)\n    val image = if (userProvidedImage) Left(actionImage) else Right(actionImage)\n    DockerContainer.create(\n      tid,\n      image = image,\n      registryConfig = Some(registryConfig),\n      memory = memory,\n      cpuShares = cpuShares,\n      cpuLimit = cpuLimit,\n      environment = Map(\"__OW_API_HOST\" -> config.wskApiHost) ++ containerArgsConfig.extraEnvVarMap,\n      network = containerArgsConfig.network,\n      dnsServers = containerArgsConfig.dnsServers,\n      dnsSearch = containerArgsConfig.dnsSearch,\n      dnsOptions = containerArgsConfig.dnsOptions,\n      name = Some(name),\n      useRunc = dockerContainerFactoryConfig.useRunc,\n      parameters ++ containerArgsConfig.extraArgs.map { case (k, v) => (\"--\" + k, v) })\n  }\n\n  /** Perform cleanup on init */\n  override def init(): Unit = removeAllActionContainers()\n\n  /** Perform cleanup on exit - to be registered as shutdown hook */\n  override def cleanup(): Unit = {\n    implicit val transid = TransactionId.invoker\n    try {\n      removeAllActionContainers()\n    } catch {\n      case e: Exception => logging.error(this, s\"Failed to remove action containers: ${e.getMessage}\")\n    }\n  }\n\n  /**\n   * Removes all wsk_ containers - regardless of their state\n   *\n   * If the system in general or Docker in particular has a very\n   * high load, commands may take longer than the specified time\n   * resulting in an exception.\n   *\n   * There is no checking whether container removal was successful\n   * or not.\n   *\n   * @throws InterruptedException     if the current thread is interrupted while waiting\n   * @throws TimeoutException         if after waiting for the specified time this `Awaitable` is still not ready\n   */\n  @throws(classOf[TimeoutException])\n  @throws(classOf[InterruptedException])\n  private def removeAllActionContainers(): Unit = {\n    implicit val transid = TransactionId.invoker\n    val cleaning =\n      docker.ps(filters = Seq(\"name\" -> s\"${ContainerFactory.containerNamePrefix(instance)}_\"), all = true).flatMap {\n        containers =>\n          logging.info(this, s\"removing ${containers.size} action containers.\")\n          val removals = containers.map { id =>\n            (if (dockerContainerFactoryConfig.useRunc) {\n               runc.resume(id)\n             } else {\n               docker.unpause(id)\n             })\n              .recoverWith {\n                // Ignore resume failures and try to remove anyway\n                case _ => Future.successful(())\n              }\n              .flatMap { _ =>\n                docker.rm(id)\n              }\n          }\n          Future.sequence(removals)\n      }\n    Await.ready(cleaning, 30.seconds)\n  }\n}\n\nobject DockerContainerFactoryProvider extends ContainerFactoryProvider {\n  override def instance(actorSystem: ActorSystem,\n                        logging: Logging,\n                        config: WhiskConfig,\n                        instanceId: InvokerInstanceId,\n                        parameters: Map[String, Set[String]]): ContainerFactory = {\n\n    new DockerContainerFactory(instanceId, parameters)(\n      actorSystem,\n      actorSystem.dispatcher,\n      logging,\n      new DockerClientWithFileAccess()(actorSystem.dispatcher)(logging, actorSystem),\n      new RuncClient()(actorSystem.dispatcher)(logging, actorSystem))\n  }\n\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/docker/DockerForMacContainerFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.entity.InvokerInstanceId\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.concurrent.duration._\n\n/**\n * This factory provides a Docker for Mac client which exposes action container's ports on the host.\n */\nobject DockerForMacContainerFactoryProvider extends ContainerFactoryProvider {\n  override def instance(actorSystem: ActorSystem,\n                        logging: Logging,\n                        config: WhiskConfig,\n                        instanceId: InvokerInstanceId,\n                        parameters: Map[String, Set[String]]): ContainerFactory = {\n\n    new DockerContainerFactory(instanceId, parameters)(\n      actorSystem,\n      actorSystem.dispatcher,\n      logging,\n      new DockerForMacClient()(actorSystem.dispatcher)(logging, actorSystem),\n      new RuncClient()(actorSystem.dispatcher)(logging, actorSystem))\n  }\n\n}\n\nclass DockerForMacClient(dockerHost: Option[String] = None)(executionContext: ExecutionContext)(implicit log: Logging,\n                                                                                                as: ActorSystem)\n    extends DockerClientWithFileAccess(dockerHost)(executionContext)\n    with DockerApiWithFileAccess {\n\n  implicit private val ec: ExecutionContext = executionContext\n\n  override def run(image: String, args: Seq[String] = Seq.empty[String])(\n    implicit transid: TransactionId): Future[ContainerId] = {\n    // b/c docker for mac doesn't have a routing to the action containers\n    // the port 8080 is exposed on the host on a random port number\n    val extraArgs: Seq[String] = Seq(\"-p\", \"0:8080\") ++ args\n    super.run(image, extraArgs)\n  }\n  // See extended trait for description\n  override def inspectIPAddress(id: ContainerId, network: String)(\n    implicit transid: TransactionId): Future[ContainerAddress] = {\n    super\n      .runCmd(Seq(\"inspect\", \"--format\", inspectCommand, id.asString), 10.seconds)\n      .flatMap {\n        case \"<no value>\" => Future.failed(new NoSuchElementException)\n        case stdout       => Future.successful(ContainerAddress(\"localhost\", stdout.toInt))\n      }\n  }\n\n  def inspectCommand: String = \"\"\"{{(index (index .NetworkSettings.Ports \"8080/tcp\") 0).HostPort}}\"\"\"\n\n  //Pause unpause is causing issue on non Linux setups. So disable by default\n  override def pause(id: ContainerId)(implicit transid: TransactionId): Future[Unit] = Future.successful(())\n\n  override def unpause(id: ContainerId)(implicit transid: TransactionId): Future[Unit] = Future.successful(())\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/docker/ProcessRunner.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker\n\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.collection.mutable\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.concurrent.blocking\nimport scala.concurrent.duration.{Duration, FiniteDuration}\nimport scala.sys.process._\n\ntrait ProcessRunner {\n\n  /**\n   * Runs the specified command with arguments asynchronously and\n   * capture stdout as well as stderr.\n   *\n   * If not set to infinite, after timeout is reached the process is terminated.\n   *\n   * Be cautious with the execution context you pass because the command\n   * is blocking.\n   *\n   * @param args command to be run including arguments\n   * @param timeout maximum time the command is allowed to take\n   * @return a future completing according to the command's exit code\n   */\n  protected def executeProcess(args: Seq[String], timeout: Duration)(implicit ec: ExecutionContext, as: ActorSystem) =\n    Future(blocking {\n      val out = new mutable.ListBuffer[String]\n      val err = new mutable.ListBuffer[String]\n      val process = args.run(ProcessLogger(o => out += o, e => err += e))\n\n      val scheduled = timeout match {\n        case t: FiniteDuration => Some(as.scheduler.scheduleOnce(t)(process.destroy()))\n        case _                 => None\n      }\n\n      (ExitStatus(process.exitValue()), out.mkString(\"\\n\"), err.mkString(\"\\n\"), scheduled)\n    }).flatMap {\n      case (ExitStatus(0), stdout, _, scheduled) =>\n        scheduled.foreach(_.cancel())\n        Future.successful(stdout)\n      case (exitStatus, stdout, stderr, scheduled) =>\n        scheduled.foreach(_.cancel())\n        timeout match {\n          case _: FiniteDuration if exitStatus.terminatedBySIGTERM =>\n            Future.failed(ProcessTimeoutException(timeout, exitStatus, stdout, stderr))\n          case _ => Future.failed(ProcessUnsuccessfulException(exitStatus, stdout, stderr))\n        }\n    }\n}\n\nobject ExitStatus {\n  // Based on The Open Group Base Specifications Issue 7, 2018 edition:\n  // Shell & Utilities - Shell Command Language - 2.8.2 Exit Status for Commands\n  // http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02\n  val STATUS_SUCCESSFUL = 0\n  val STATUS_NOT_EXECUTABLE = 126\n  val STATUS_NOT_FOUND = 127\n  // When a command is stopped by a signal, the exit status is 128 + signal numer\n  val STATUS_SIGNAL = 128\n\n  // Based on The Open Group Base Specifications Issue 7, 2018 edition:\n  // Shell & Utilities - Utilities - kill\n  // http://pubs.opengroup.org/onlinepubs/9699919799/utilities/kill.html\n  val SIGHUP = 1\n  val SIGINT = 2\n  val SIGQUIT = 3\n  val SIGABRT = 6\n  val SIGKILL = 9\n  val SIGALRM = 14\n  val SIGTERM = 15\n}\n\ncase class ExitStatus(statusValue: Int) {\n\n  import ExitStatus._\n\n  override def toString(): String = {\n    def signalAsString(signal: Int): String = {\n      signal match {\n        case SIGHUP  => \"SIGHUP\"\n        case SIGINT  => \"SIGINT\"\n        case SIGQUIT => \"SIGQUIT\"\n        case SIGABRT => \"SIGABRT\"\n        case SIGKILL => \"SIGKILL\"\n        case SIGALRM => \"SIGALRM\"\n        case SIGTERM => \"SIGTERM\"\n        case _       => signal.toString\n      }\n    }\n\n    val detail = statusValue match {\n      case STATUS_SUCCESSFUL     => \"successful\"\n      case STATUS_NOT_EXECUTABLE => \"not executable\"\n      case STATUS_NOT_FOUND      => \"not found\"\n      case _ if statusValue >= ExitStatus.STATUS_SIGNAL =>\n        \"terminated by signal \" + signalAsString(statusValue - ExitStatus.STATUS_SIGNAL)\n      case _ => \"unsuccessful\"\n    }\n\n    s\"$statusValue ($detail)\"\n  }\n\n  val successful = statusValue == ExitStatus.STATUS_SUCCESSFUL\n  val terminatedBySIGTERM = (statusValue - ExitStatus.STATUS_SIGNAL) == ExitStatus.SIGTERM\n}\n\nabstract class ProcessRunningException(info: String, val exitStatus: ExitStatus, val stdout: String, val stderr: String)\n    extends Exception(s\"info: $info, code: $exitStatus, stdout: $stdout, stderr: $stderr\")\n\ncase class ProcessUnsuccessfulException(override val exitStatus: ExitStatus,\n                                        override val stdout: String,\n                                        override val stderr: String)\n    extends ProcessRunningException(\"command was unsuccessful\", exitStatus, stdout, stderr)\n\ncase class ProcessTimeoutException(timeout: Duration,\n                                   override val exitStatus: ExitStatus,\n                                   override val stdout: String,\n                                   override val stderr: String)\n    extends ProcessRunningException(s\"command was terminated, took longer than $timeout\", exitStatus, stdout, stderr)\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/docker/RuncClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker\n\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.concurrent.Future\nimport scala.concurrent.ExecutionContext\nimport scala.util.Failure\nimport org.apache.openwhisk.common.TransactionId\n\nimport scala.util.Success\nimport org.apache.openwhisk.common.LoggingMarkers\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.ConfigKeys\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.pekko.event.Logging.{ErrorLevel, InfoLevel}\nimport org.apache.openwhisk.core.containerpool.ContainerId\n\nimport scala.concurrent.duration.Duration\n\n/**\n * Configuration for runc client command timeouts.\n */\ncase class RuncClientTimeouts(pause: Duration, resume: Duration)\n\n/**\n * Serves as interface to the docker CLI tool.\n *\n * Be cautious with the ExecutionContext passed to this, as the\n * calls to the CLI are blocking.\n *\n * You only need one instance (and you shouldn't get more).\n */\nclass RuncClient(timeouts: RuncClientTimeouts = loadConfigOrThrow[RuncClientTimeouts](ConfigKeys.runcTimeouts))(\n  executionContext: ExecutionContext)(implicit log: Logging, as: ActorSystem)\n    extends RuncApi\n    with ProcessRunner {\n  implicit private val ec = executionContext\n\n  // Determines how to run docker. Failure to find a Docker binary implies\n  // a failure to initialize this instance of DockerClient.\n  protected val runcCmd: Seq[String] = Seq(\"/usr/bin/runc\")\n\n  def pause(id: ContainerId)(implicit transid: TransactionId): Future[Unit] =\n    runCmd(Seq(\"pause\", id.asString), timeouts.pause).map(_ => ())\n\n  def resume(id: ContainerId)(implicit transid: TransactionId): Future[Unit] =\n    runCmd(Seq(\"resume\", id.asString), timeouts.resume).map(_ => ())\n\n  private def runCmd(args: Seq[String], timeout: Duration)(implicit transid: TransactionId): Future[String] = {\n    val cmd = runcCmd ++ args\n    val start = transid.started(\n      this,\n      LoggingMarkers.INVOKER_RUNC_CMD(args.head),\n      s\"running ${cmd.mkString(\" \")} (timeout: $timeout)\",\n      logLevel = InfoLevel)\n    executeProcess(cmd, timeout).andThen {\n      case Success(_) => transid.finished(this, start, logLevel = InfoLevel)\n      case Failure(t) => transid.failed(this, start, t.getMessage, ErrorLevel)\n    }\n  }\n}\n\ntrait RuncApi {\n\n  /**\n   * Pauses the container with the given id.\n   *\n   * @param id the id of the container to pause\n   * @return a Future completing according to the command's exit-code\n   */\n  def pause(id: ContainerId)(implicit transid: TransactionId): Future[Unit]\n\n  /**\n   * Unpauses the container with the given id.\n   *\n   * @param id the id of the container to unpause\n   * @return a Future completing according to the command's exit-code\n   */\n  def resume(id: ContainerId)(implicit transid: TransactionId): Future[Unit]\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/docker/StandaloneDockerContainerFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.commons.lang3.SystemUtils\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.core.containerpool.{Container, ContainerFactory, ContainerFactoryProvider}\nimport org.apache.openwhisk.core.entity.{ByteSize, ExecManifest, InvokerInstanceId}\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.{ExecutionContext, Future}\n\nobject StandaloneDockerContainerFactoryProvider extends ContainerFactoryProvider {\n  override def instance(actorSystem: ActorSystem,\n                        logging: Logging,\n                        config: WhiskConfig,\n                        instanceId: InvokerInstanceId,\n                        parameters: Map[String, Set[String]]): ContainerFactory = {\n    val client =\n      if (SystemUtils.IS_OS_MAC) new DockerForMacClient()(actorSystem.dispatcher)(logging, actorSystem)\n      else if (SystemUtils.IS_OS_WINDOWS) new DockerForWindowsClient()(actorSystem.dispatcher)(logging, actorSystem)\n      else new DockerClientWithFileAccess()(actorSystem.dispatcher)(logging, actorSystem)\n\n    new StandaloneDockerContainerFactory(instanceId, parameters)(\n      actorSystem,\n      actorSystem.dispatcher,\n      logging,\n      client,\n      new RuncClient()(actorSystem.dispatcher)(logging, actorSystem))\n  }\n}\n\ncase class StandaloneDockerConfig(pullStandardImages: Boolean)\n\nclass StandaloneDockerContainerFactory(instance: InvokerInstanceId, parameters: Map[String, Set[String]])(\n  implicit actorSystem: ActorSystem,\n  ec: ExecutionContext,\n  logging: Logging,\n  docker: DockerApiWithFileAccess,\n  runc: RuncApi)\n    extends DockerContainerFactory(instance, parameters) {\n  private val pulledImages = new TrieMap[String, Boolean]()\n  private val factoryConfig = loadConfigOrThrow[StandaloneDockerConfig](ConfigKeys.standaloneDockerContainerFactory)\n\n  override def createContainer(\n    tid: TransactionId,\n    name: String,\n    actionImage: ExecManifest.ImageName,\n    userProvidedImage: Boolean,\n    memory: ByteSize,\n    cpuShares: Int,\n    cpuLimit: Option[Double])(implicit config: WhiskConfig, logging: Logging): Future[Container] = {\n\n    //For standalone server usage we would also want to pull the OpenWhisk provided image so as to ensure if\n    //local setup does not have the image then it pulls it down\n    //For standard usage its expected that standard images have already been pulled in.\n    val imageName = actionImage.resolveImageName(Some(runtimesRegistryConfig.url))\n    val pulled =\n      if (!userProvidedImage\n          && factoryConfig.pullStandardImages\n          && !pulledImages.contains(imageName)\n          && actionImage.prefix.contains(\"openwhisk\")) {\n        docker.pull(imageName)(tid).map { _ =>\n          logging.info(this, s\"Pulled OpenWhisk provided image $imageName\")\n          pulledImages.put(imageName, true)\n          true\n        }\n      } else Future.successful(true)\n\n    pulled.flatMap(_ => super.createContainer(tid, name, actionImage, userProvidedImage, memory, cpuShares, cpuLimit))\n  }\n\n  override def init(): Unit = {\n    logging.info(\n      this,\n      s\"Standalone docker container factory config pullStandardImages: ${factoryConfig.pullStandardImages}\")\n    super.init()\n  }\n}\n\ntrait WindowsDockerClient {\n  self: DockerClient =>\n\n  override protected def executableAlternatives: List[String] = {\n    val executable = loadConfig[String](\"whisk.docker.executable\").toOption\n    List(\n      \"\"\"C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe\"\"\",\n      \"\"\"C:\\Program Files\\Docker\\Docker\\resources\\docker.exe\"\"\") ++ executable\n  }\n}\n\nclass DockerForWindowsClient(dockerHost: Option[String] = None)(executionContext: ExecutionContext)(\n  implicit log: Logging,\n  as: ActorSystem)\n    extends DockerForMacClient(dockerHost)(executionContext)\n    with WindowsDockerClient {\n  //Due to some Docker + Windows + Go parsing quirks need to add double quotes around whole command\n  //See https://github.com/moby/moby/issues/27592#issuecomment-255227097\n  override def inspectCommand: String = \"\"\"\"{{(index (index .NetworkSettings.Ports \\\"8080/tcp\\\") 0).HostPort}}\"\"\"\"\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/kubernetes/KubernetesClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.kubernetes\n\nimport java.io.IOException\nimport java.net.SocketTimeoutException\nimport java.time.format.DateTimeFormatterBuilder\nimport java.time.temporal.ChronoField\nimport java.time.{Instant, ZoneId}\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.Logging.ErrorLevel\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport org.apache.pekko.http.scaladsl.model.Uri.{Path, Query}\nimport org.apache.pekko.pattern.after\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.stream.stage._\nimport org.apache.pekko.stream.{Attributes, Outlet, SourceShape}\nimport org.apache.pekko.util.ByteString\n\nimport collection.JavaConverters._\nimport io.fabric8.kubernetes.api.model._\nimport io.fabric8.kubernetes.client.utils.Serialization\nimport io.fabric8.kubernetes.client.{ConfigBuilder, DefaultKubernetesClient}\nimport okhttp3.{Call, Callback, Request, Response}\nimport okio.BufferedSource\nimport org.apache.commons.lang3.exception.ExceptionUtils\nimport org.apache.openwhisk.common.LoggingMarkers\nimport org.apache.openwhisk.common.{ConfigMapValue, Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.docker.ProcessRunner\nimport org.apache.openwhisk.core.containerpool.{ContainerAddress, ContainerId}\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.annotation.tailrec\nimport scala.collection.immutable.Queue\nimport scala.collection.mutable\nimport scala.concurrent.duration._\nimport scala.concurrent.{blocking, ExecutionContext, Future}\nimport scala.util.control.NonFatal\nimport scala.util.{Failure, Success, Try}\n\n/**\n * Configuration for kubernetes client command timeouts.\n */\ncase class KubernetesClientTimeoutConfig(run: FiniteDuration, logs: FiniteDuration)\n\n/**\n * Configuration for kubernetes cpu resource request/limit scaling based on action memory limit\n */\ncase class KubernetesCpuScalingConfig(millicpus: Int, memory: ByteSize, maxMillicpus: Int)\n\n/**\n * Configuration for kubernetes ephemeral storage limit for the action container\n */\ncase class KubernetesEphemeralStorageConfig(limit: ByteSize, scaleFactor: Double)\n\n/**\n * Exception to indicate a pod took too long to become ready.\n */\ncase class KubernetesPodReadyTimeoutException(timeout: FiniteDuration)\n    extends Exception(s\"Pod readiness timed out after ${timeout.toSeconds}s\")\n\n/**\n * Exception to indicate it failed to pull an image for blackbox actions.\n */\ncase class KubernetesImagePullFailedException(msg: String) extends Exception(msg)\n\n/**\n * Exception to indicate the command for an image is not found.\n */\ncase class KubernetesImageCommandNotFoundException(msg: String) extends Exception(msg)\n\n/**\n * Exception to indicate a pod could not be created at the apiserver.\n */\ncase class KubernetesPodApiException(e: Throwable) extends Exception(s\"Pod was not created at apiserver: ${e}\", e)\n\n/**\n * Configuration for node affinity for the pods that execute user action containers\n * The key,value pair should match the <key,value> pair with which the invoker worker nodes\n * are labeled in the Kubernetes cluster.  The default pair is <openwhisk-role,invoker>,\n * but a deployment may override this default if needed.\n */\ncase class KubernetesInvokerNodeAffinity(enabled: Boolean, key: String, value: String)\n\n/**\n * General configuration for kubernetes client\n */\ncase class KubernetesClientConfig(timeouts: KubernetesClientTimeoutConfig,\n                                  userPodNodeAffinity: KubernetesInvokerNodeAffinity,\n                                  portForwardingEnabled: Boolean,\n                                  actionNamespace: Option[String],\n                                  podTemplate: Option[ConfigMapValue],\n                                  cpuScaling: Option[KubernetesCpuScalingConfig],\n                                  pdbEnabled: Boolean,\n                                  fieldRefEnvironment: Option[Map[String, String]],\n                                  ephemeralStorage: Option[KubernetesEphemeralStorageConfig])\n\n/**\n * Serves as an interface to the Kubernetes API by proxying its REST API and/or invoking the kubectl CLI.\n *\n * Be cautious with the ExecutionContext passed to this, as many\n * operations are blocking.\n *\n * You only need one instance (and you shouldn't get more).\n */\nclass KubernetesClient(\n  config: KubernetesClientConfig = loadConfigOrThrow[KubernetesClientConfig](ConfigKeys.kubernetes),\n  testClient: Option[DefaultKubernetesClient] = None)(executionContext: ExecutionContext)(implicit log: Logging,\n                                                                                          as: ActorSystem)\n    extends KubernetesApi\n    with ProcessRunner {\n  implicit protected val ec = executionContext\n  implicit protected val scheduler = as.scheduler\n  implicit protected val kubeRestClient = testClient.getOrElse {\n    val configBuilder = new ConfigBuilder()\n      .withConnectionTimeout(config.timeouts.logs.toMillis.toInt)\n      .withRequestTimeout(config.timeouts.logs.toMillis.toInt)\n    config.actionNamespace.foreach(configBuilder.withNamespace)\n    new DefaultKubernetesClient(configBuilder.build())\n  }\n\n  private val imagePullFailedMsgs = Set(\"ImagePullBackOff\", \"ErrImagePull\")\n\n  private val podBuilder = new WhiskPodBuilder(kubeRestClient, config)\n\n  def run(name: String,\n          image: String,\n          memory: ByteSize = 256.MB,\n          environment: Map[String, String] = Map.empty,\n          labels: Map[String, String] = Map.empty)(implicit transid: TransactionId): Future[KubernetesContainer] = {\n\n    val (pod, pdb) = podBuilder.buildPodSpec(name, image, memory, environment, labels, config)\n    if (transid.meta.extraLogging) {\n      log.info(this, s\"Pod spec being created\\n${Serialization.asYaml(pod)}\")\n    }\n    val namespace = kubeRestClient.getNamespace\n    val start = transid.started(\n      this,\n      LoggingMarkers.INVOKER_KUBEAPI_CMD(\"create\"),\n      s\"launching pod $name (image:$image, mem: ${memory.toMB}) (timeout: ${config.timeouts.run.toSeconds}s)\",\n      logLevel = org.apache.pekko.event.Logging.InfoLevel)\n\n    //create the pod; catch any failure to end the transaction timer\n    Try {\n      val created = kubeRestClient.pods.inNamespace(namespace).create(pod)\n      pdb.map(\n        p =>\n          kubeRestClient.policy.podDisruptionBudget\n            .inNamespace(namespace)\n            .withName(name)\n            .create(p))\n      created\n    } match {\n      case Failure(e) =>\n        //call to api-server failed\n        val stackTrace = ExceptionUtils.getStackTrace(e)\n        transid.failed(\n          this,\n          start,\n          s\"Failed create pod for '$name': ${e.getClass} (Caused by: ${e.getCause}) - ${e.getMessage}; stacktrace: $stackTrace\",\n          ErrorLevel)\n        Future.failed(KubernetesPodApiException(e))\n      case Success(createdPod) => {\n        //call to api-server succeeded; wait for the pod to become ready; catch any failure to end the transaction timer\n        waitForPod(namespace, createdPod, start.start, config.timeouts.run)\n          .map { readyPod =>\n            transid.finished(this, start, logLevel = InfoLevel)\n            toContainer(readyPod)\n          }\n          .recoverWith {\n            case e =>\n              transid.failed(this, start, s\"Failed create pod for '$name': ${e.getClass} - ${e.getMessage}\", ErrorLevel)\n              //log pod events to diagnose pod readiness failures\n              val podEvents = kubeRestClient\n                .v1()\n                .events()\n                .inNamespace(namespace)\n                .withField(\"involvedObject.name\", name)\n                .list()\n                .getItems\n                .asScala\n              if (podEvents.isEmpty) {\n                log.info(this, s\"No pod events for failed pod '$name'\")\n              } else {\n                podEvents.foreach { podEvent =>\n                  log.info(\n                    this,\n                    s\"Pod event for failed pod '$name' ${podEvent.getLastTimestamp}: ${podEvent.getMessage}\")\n                }\n              }\n              Future.failed(e)\n          }\n      }\n    }\n  }\n\n  def rm(container: KubernetesContainer)(implicit transid: TransactionId): Future[Unit] = {\n    deleteByName(container.id.asString)\n  }\n\n  def rm(podName: String)(implicit transid: TransactionId): Future[Unit] = {\n    deleteByName(podName)\n  }\n\n  def rm(labels: Map[String, String], ensureUnpaused: Boolean = false)(\n    implicit transid: TransactionId): Future[Unit] = {\n    val start = transid.started(\n      this,\n      LoggingMarkers.INVOKER_KUBEAPI_CMD(\"delete\"),\n      s\"Deleting pods with label $labels\",\n      logLevel = org.apache.pekko.event.Logging.InfoLevel)\n    Future {\n      blocking {\n        kubeRestClient\n          .inNamespace(kubeRestClient.getNamespace)\n          .pods()\n          .withLabels(labels.asJava)\n          .delete()\n        if (config.pdbEnabled) {\n          kubeRestClient.policy.podDisruptionBudget\n            .inNamespace(kubeRestClient.getNamespace)\n            .withLabels(labels.asJava)\n            .delete()\n        }\n      }\n    }.map(_ => transid.finished(this, start, logLevel = InfoLevel))\n      .recover {\n        case e =>\n          transid.failed(\n            this,\n            start,\n            s\"Failed delete pods with label $labels: ${e.getClass} - ${e.getMessage}\",\n            ErrorLevel)\n      }\n  }\n\n  private def deleteByName(podName: String)(implicit transid: TransactionId) = {\n    val start = transid.started(\n      this,\n      LoggingMarkers.INVOKER_KUBEAPI_CMD(\"delete\"),\n      s\"Deleting pod ${podName}\",\n      logLevel = org.apache.pekko.event.Logging.InfoLevel)\n    Future {\n      blocking {\n        kubeRestClient\n          .inNamespace(kubeRestClient.getNamespace)\n          .pods()\n          .withName(podName)\n          .delete()\n        if (config.pdbEnabled) {\n          kubeRestClient.policy.podDisruptionBudget\n            .inNamespace(kubeRestClient.getNamespace)\n            .withName(podName)\n            .delete()\n        }\n      }\n    }.map(_ => transid.finished(this, start, logLevel = InfoLevel))\n      .recover {\n        case e =>\n          transid.failed(\n            this,\n            start,\n            s\"Failed delete pod for '${podName}': ${e.getClass} - ${e.getMessage}\",\n            ErrorLevel)\n      }\n  }\n\n  // suspend is a no-op with the basic KubernetesClient\n  def suspend(container: KubernetesContainer)(implicit transid: TransactionId): Future[Unit] = Future.successful({})\n\n  // resume is a no-op with the basic KubernetesClient\n  def resume(container: KubernetesContainer)(implicit transid: TransactionId): Future[Unit] = Future.successful({})\n\n  def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean = false)(\n    implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n\n    log.debug(this, \"Parsing logs from Kubernetes Graph Stage…\")\n\n    Source\n      .fromGraph(new KubernetesRestLogSourceStage(container.id, sinceTime, waitForSentinel))\n      .log(\"kubernetesLogs\")\n\n  }\n\n  protected def toContainer(pod: Pod): KubernetesContainer = {\n    val id = ContainerId(pod.getMetadata.getName)\n\n    val portFwd = if (config.portForwardingEnabled) {\n      Some(kubeRestClient.pods().withName(pod.getMetadata.getName).portForward(8080))\n    } else None\n\n    val addr = portFwd\n      .map(fwd => ContainerAddress(\"localhost\", fwd.getLocalPort))\n      .getOrElse(ContainerAddress(pod.getStatus.getPodIP))\n    val workerIP = pod.getStatus.getHostIP\n    // Extract the native (docker or containerd) containerId for the container\n    // By convention, kubernetes adds a docker:// prefix when using docker as the low-level container engine\n    val nativeContainerId = pod.getStatus.getContainerStatuses.get(0).getContainerID.stripPrefix(\"docker://\")\n    implicit val kubernetes = this\n    new KubernetesContainer(id, addr, workerIP, nativeContainerId, portFwd)\n  }\n\n  // check for ready status every 1 second until timeout (minus the start time, which is the time for the pod create call) has past\n  private def waitForPod(namespace: String,\n                         pod: Pod,\n                         start: Instant,\n                         timeout: FiniteDuration,\n                         deadlineOpt: Option[Deadline] = None): Future[Pod] = {\n    val readyPod = kubeRestClient\n      .pods()\n      .inNamespace(namespace)\n      .withName(pod.getMetadata.getName)\n    val deadline = deadlineOpt.getOrElse((timeout - (System.currentTimeMillis() - start.toEpochMilli).millis).fromNow)\n    if (!readyPod.isReady) {\n      // when action pod is failed while pulling images, we need to let users know that\n      val imagePullErr = Try {\n        readyPod.get.getStatus.getContainerStatuses.asScala.exists { status =>\n          imagePullFailedMsgs.contains(status.getState.getWaiting.getReason)\n        }\n      } getOrElse false\n      // when command not found in image, we need to let users know that as well\n      val commandNotFoundErr = Try {\n        readyPod.get.getStatus.getContainerStatuses.asScala.exists { status =>\n          status.getState.getWaiting.getMessage.contains(Messages.commandNotFoundError)\n        }\n      } getOrElse false\n      if (imagePullErr) {\n        Future.failed(KubernetesImagePullFailedException(s\"Failed to pull image for pod ${pod.getMetadata.getName}\"))\n      } else if (commandNotFoundErr) {\n        Future.failed(KubernetesImageCommandNotFoundException(Messages.commandNotFoundError))\n      } else if (deadline.isOverdue()) {\n        Future.failed(KubernetesPodReadyTimeoutException(timeout))\n      } else {\n        after(1.seconds, scheduler) {\n          waitForPod(namespace, pod, start, timeout, Some(deadline))\n        }\n      }\n    } else {\n      Future.successful(readyPod.get())\n    }\n  }\n\n  def addLabel(container: KubernetesContainer, labels: Map[String, String]): Future[Unit] =\n    try {\n      kubeRestClient\n        .pods()\n        .withName(container.id.asString)\n        .edit()\n        .editMetadata()\n        .addToLabels(labels.asJava)\n        .endMetadata()\n        .done()\n      Future.successful({})\n    } catch {\n      case e: Throwable => Future.failed(e)\n    }\n}\nobject KubernetesClient {\n\n  // Necessary, as Kubernetes uses nanosecond precision in logs, but java.time.Instant toString uses milliseconds\n  //%Y-%m-%dT%H:%M:%S.%N%z\n  val K8STimestampFormat = new DateTimeFormatterBuilder()\n    .parseCaseInsensitive()\n    .appendPattern(\"u-MM-dd\")\n    .appendLiteral('T')\n    .appendPattern(\"HH:mm:ss\")\n    .appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true)\n    .appendLiteral('Z')\n    .toFormatter()\n    .withZone(ZoneId.of(\"UTC\"))\n\n  def parseK8STimestamp(ts: String): Try[Instant] =\n    Try(Instant.from(K8STimestampFormat.parse(ts)))\n\n  def formatK8STimestamp(ts: Instant): Try[String] =\n    Try(K8STimestampFormat.format(ts))\n}\n\ntrait KubernetesApi {\n\n  def run(name: String,\n          image: String,\n          memory: ByteSize,\n          environment: Map[String, String] = Map.empty,\n          labels: Map[String, String] = Map.empty)(implicit transid: TransactionId): Future[KubernetesContainer]\n\n  def rm(container: KubernetesContainer)(implicit transid: TransactionId): Future[Unit]\n  def rm(podName: String)(implicit transid: TransactionId): Future[Unit]\n  def rm(labels: Map[String, String], ensureUnpaused: Boolean)(implicit transid: TransactionId): Future[Unit]\n\n  def suspend(container: KubernetesContainer)(implicit transid: TransactionId): Future[Unit]\n\n  def resume(container: KubernetesContainer)(implicit transid: TransactionId): Future[Unit]\n\n  def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean = false)(\n    implicit transid: TransactionId): Source[TypedLogLine, Any]\n\n  def addLabel(container: KubernetesContainer, labels: Map[String, String]): Future[Unit]\n}\n\nobject KubernetesRestLogSourceStage {\n\n  import KubernetesClient.{formatK8STimestamp, parseK8STimestamp}\n\n  val retryDelay = 100.milliseconds\n\n  val actionContainerName = \"user-action\"\n\n  sealed trait K8SRestLogTimingEvent\n\n  case object K8SRestLogRetry extends K8SRestLogTimingEvent\n\n  def constructPath(namespace: String, containerId: String): Path =\n    Path / \"api\" / \"v1\" / \"namespaces\" / namespace / \"pods\" / containerId / \"log\"\n\n  def constructQuery(sinceTime: Option[Instant], waitForSentinel: Boolean): Query = {\n\n    val sinceTimestamp = sinceTime.flatMap(time => formatK8STimestamp(time).toOption)\n\n    Query(Map(\"timestamps\" -> \"true\", \"container\" -> actionContainerName) ++ sinceTimestamp.map(time =>\n      \"sinceTime\" -> time))\n\n  }\n\n  @tailrec\n  def readLines(src: BufferedSource,\n                lastTimestamp: Option[Instant],\n                lines: Queue[TypedLogLine] = Queue.empty[TypedLogLine]): Queue[TypedLogLine] = {\n    if (!src.exhausted()) {\n      (for {\n        line <- Option(src.readUtf8Line()) if !line.isEmpty\n        timestampDelimiter = line.indexOf(\" \")\n        // Kubernetes is ignoring nanoseconds in sinceTime, so we have to filter additionally here\n        rawTimestamp = line.substring(0, timestampDelimiter)\n        timestamp <- parseK8STimestamp(rawTimestamp).toOption if isRelevantLogLine(lastTimestamp, timestamp)\n        msg = line.substring(timestampDelimiter + 1)\n        stream = \"stdout\" // TODO - when we can distinguish stderr: https://github.com/kubernetes/kubernetes/issues/28167\n      } yield {\n        TypedLogLine(timestamp, stream, msg)\n      }) match {\n        case Some(logLine) =>\n          readLines(src, lastTimestamp, lines :+ logLine)\n        case None =>\n          // we may have skipped a line for filtering conditions only; keep going\n          readLines(src, lastTimestamp, lines)\n      }\n    } else {\n      lines\n    }\n\n  }\n\n  def isRelevantLogLine(lastTimestamp: Option[Instant], newTimestamp: Instant): Boolean =\n    lastTimestamp match {\n      case Some(last) =>\n        newTimestamp.isAfter(last)\n      case None =>\n        true\n    }\n\n}\n\nfinal class KubernetesRestLogSourceStage(id: ContainerId, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n  implicit val kubeRestClient: DefaultKubernetesClient)\n    extends GraphStage[SourceShape[TypedLogLine]] { stage =>\n\n  import KubernetesRestLogSourceStage._\n\n  val out = Outlet[TypedLogLine](\"K8SHttpLogging.out\")\n\n  override val shape: SourceShape[TypedLogLine] = SourceShape.of(out)\n\n  override protected def initialAttributes: Attributes = Attributes.name(\"KubernetesHttpLogSource\")\n\n  override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =\n    new TimerGraphStageLogicWithLogging(shape) { logic =>\n\n      private val queue = mutable.Queue.empty[TypedLogLine]\n      private var lastTimestamp = sinceTime\n\n      def fetchLogs(): Unit =\n        try {\n          val path = constructPath(kubeRestClient.getNamespace, id.asString)\n          val query = constructQuery(lastTimestamp, waitForSentinel)\n\n          log.debug(\"*** Fetching K8S HTTP Logs w/ Path: {} Query: {}\", path, query)\n\n          val url = Uri(kubeRestClient.getMasterUrl.toString)\n            .withPath(path)\n            .withQuery(query)\n\n          val request = new Request.Builder().get().url(url.toString).build\n\n          kubeRestClient.getHttpClient.newCall(request).enqueue(new LogFetchCallback())\n        } catch {\n          case NonFatal(e) =>\n            onFailure(e)\n            throw e\n        }\n\n      def onFailure(e: Throwable): Unit = e match {\n        case _: SocketTimeoutException =>\n          log.warning(\"* Logging socket to Kubernetes timed out.\") // this should only happen with follow behavior\n        case _ =>\n          log.error(e, \"* Retrieving the logs from Kubernetes failed.\")\n      }\n\n      val emitCallback: AsyncCallback[Seq[TypedLogLine]] = getAsyncCallback[Seq[TypedLogLine]] {\n        case lines @ firstLine +: restOfLines =>\n          if (isAvailable(out)) {\n            log.debug(\"* Lines Available & output ready; pushing {} (remaining: {})\", firstLine, restOfLines)\n            pushLine(firstLine)\n            queue ++= restOfLines\n          } else {\n            log.debug(\"* Output isn't ready; queueing lines: {}\", lines)\n            queue ++= lines\n          }\n        case Nil =>\n          log.debug(\"* Empty lines returned.\")\n          retryLogs()\n      }\n\n      class LogFetchCallback extends Callback {\n\n        override def onFailure(call: Call, e: IOException): Unit = logic.onFailure(e)\n\n        override def onResponse(call: Call, response: Response): Unit =\n          try {\n            val lines = readLines(response.body.source, lastTimestamp)\n\n            log.debug(\"* Read & decoded lines for K8S HTTP: {}\", lines)\n\n            response.body.source.close()\n\n            lines.lastOption.foreach { line =>\n              log.debug(\"* Updating lastTimestamp (sinceTime) to {}\", Option(line.time))\n              lastTimestamp = Option(line.time)\n            }\n\n            emitCallback.invoke(lines)\n          } catch {\n            case NonFatal(e) =>\n              log.error(e, \"* Reading Kubernetes HTTP Response failed.\")\n              logic.onFailure(e)\n              throw e\n          }\n      }\n\n      def pushLine(line: TypedLogLine): Unit = {\n        log.debug(\"* Pushing a chunk of kubernetes logging: {}\", line)\n        push(out, line)\n      }\n\n      setHandler(\n        out,\n        new OutHandler {\n          override def onPull(): Unit = {\n            // if we still have lines queued up, return those; else make a new HTTP read.\n            if (queue.nonEmpty) {\n              log.debug(\"* onPull, nonEmpty queue... pushing line\")\n              pushLine(queue.dequeue())\n            } else {\n              log.debug(\"* onPull, empty queue... fetching logs\")\n              fetchLogs()\n            }\n          }\n        })\n\n      def retryLogs(): Unit = {\n        // Pause before retrying so we don't thrash Kubernetes w/ HTTP requests\n        log.debug(\"* Scheduling a retry of log fetch in {}\", retryDelay)\n        scheduleOnce(K8SRestLogRetry, retryDelay)\n      }\n\n      override protected def onTimer(timerKey: Any): Unit = timerKey match {\n        case K8SRestLogRetry =>\n          log.debug(\"* Timer trigger for log fetch retry\")\n          fetchLogs()\n        case x =>\n          log.warning(\"* Got a timer trigger with an unknown key: {}\", x)\n      }\n    }\n}\n\nprotected[core] final case class TypedLogLine(time: Instant, stream: String, log: String) {\n  import KubernetesClient.formatK8STimestamp\n\n  lazy val toJson: JsObject =\n    JsObject(\"time\" -> formatK8STimestamp(time).getOrElse(\"\").toJson, \"stream\" -> stream.toJson, \"log\" -> log.toJson)\n\n  lazy val jsonPrinted: String = toJson.compactPrint\n  lazy val jsonSize: Int = jsonPrinted.length\n\n  /**\n   * Returns a ByteString representation of the json for this Log Line\n   */\n  val toByteString = ByteString(jsonPrinted)\n\n  override def toString = s\"${formatK8STimestamp(time).get} $stream: ${log.trim}\"\n}\n\nprotected[core] object TypedLogLine {\n\n  import KubernetesClient.{parseK8STimestamp, K8STimestampFormat}\n\n  def readInstant(json: JsValue): Instant = json match {\n    case JsString(str) =>\n      parseK8STimestamp(str) match {\n        case Success(time) =>\n          time\n        case Failure(e) =>\n          deserializationError(\n            s\"Could not parse a java.time.Instant from $str (Expected in format: $K8STimestampFormat: $e\")\n      }\n    case _ =>\n      deserializationError(s\"Could not parse a java.time.Instant from $json (Expected in format: $K8STimestampFormat)\")\n  }\n\n  implicit val typedLogLineFormat = new RootJsonFormat[TypedLogLine] {\n    override def write(obj: TypedLogLine): JsValue = obj.toJson\n\n    override def read(json: JsValue): TypedLogLine = {\n      val obj = json.asJsObject\n      val fields = obj.fields\n      TypedLogLine(readInstant(fields(\"time\")), fields(\"stream\").convertTo[String], fields(\"log\").convertTo[String])\n    }\n  }\n\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/kubernetes/KubernetesContainer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.kubernetes\n\nimport org.apache.pekko.actor.ActorSystem\nimport java.time.Instant\nimport java.util.concurrent.atomic.AtomicReference\n\nimport org.apache.pekko.stream.StreamLimitReachedException\nimport org.apache.pekko.stream.scaladsl.Framing.FramingException\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.util.ByteString\nimport io.fabric8.kubernetes.client.PortForward\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.containerpool.docker.{CompleteAfterOccurrences, OccurrencesNotFoundException}\nimport org.apache.openwhisk.core.entity.{ByteSize, WhiskAction}\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\nimport spray.json.JsObject\n\nimport scala.util.Failure\n\nobject KubernetesContainer {\n\n  /**\n   * Creates a container running in kubernetes\n   *\n   * @param transid transaction creating the container\n   * @param image image to create the container from\n   * @param userProvidedImage whether the image is provided by the user\n   *     or is an OpenWhisk provided image\n   * @param labels labels to set on the container\n   * @param name optional name for the container\n   * @return a Future which either completes with a KubernetesContainer or a failure to create a container\n   */\n  def create(transid: TransactionId,\n             name: String,\n             image: String,\n             userProvidedImage: Boolean = false,\n             memory: ByteSize = 256.MB,\n             environment: Map[String, String] = Map.empty,\n             labels: Map[String, String] = Map.empty)(implicit kubernetes: KubernetesApi,\n                                                      ec: ExecutionContext,\n                                                      log: Logging): Future[KubernetesContainer] = {\n    implicit val tid = transid\n\n    // Kubernetes naming rule allows maximum length of 63 character and ended with character only.\n    val origName = name.replace(\"_\", \"-\").replaceAll(\"[()]\", \"\").toLowerCase.take(63)\n    val podName = if (origName.endsWith(\"-\")) origName.reverse.dropWhile(_ == '-').reverse else origName\n\n    for {\n      container <- kubernetes.run(podName, image, memory, environment, labels).recoverWith {\n        case e: KubernetesPodApiException =>\n          //apiserver call failed - this will expose a different error to users\n          cleanupFailedPod(e, podName, WhiskContainerStartupError(Messages.resourceProvisionError))\n        case e: KubernetesImagePullFailedException =>\n          cleanupFailedPod(e, podName, BlackboxStartupError(Messages.imagePullError(image)))\n        case e: KubernetesImageCommandNotFoundException =>\n          cleanupFailedPod(e, podName, BlackboxStartupError(s\"${Messages.commandNotFoundError} in image ${image}\"))\n        case e: Throwable =>\n          cleanupFailedPod(e, podName, WhiskContainerStartupError(s\"Failed to run container with image '${image}'.\"))\n      }\n    } yield container\n  }\n  private def cleanupFailedPod(e: Throwable, podName: String, failureCause: Exception)(\n    implicit kubernetes: KubernetesApi,\n    ec: ExecutionContext,\n    tid: TransactionId,\n    log: Logging) = {\n    log.info(this, s\"Deleting failed pod '$podName' after: ${e.getClass} - ${e.getMessage}\")\n    kubernetes\n      .rm(podName)\n      .andThen {\n        case Failure(e) =>\n          log.error(this, s\"Failed delete pod for '$podName': ${e.getClass} - ${e.getMessage}\")\n      }\n      .transformWith { _ =>\n        Future.failed(failureCause)\n      }\n  }\n}\n\n/**\n * Represents a container as run by kubernetes.\n *\n * This class contains OpenWhisk specific behavior and as such does not necessarily\n * use kubernetes commands to achieve the effects needed.\n *\n * @constructor\n * @param id the id of the container\n * @param addr the ip & port of the container\n * @param workerIP the ip of the workernode on which the container is executing\n * @param nativeContainerId the docker/containerd lowlevel id for the container\n */\nclass KubernetesContainer(protected[core] val id: ContainerId,\n                          protected[core] val addr: ContainerAddress,\n                          protected[core] val workerIP: String,\n                          protected[core] val nativeContainerId: String,\n                          portForward: Option[PortForward] = None)(implicit kubernetes: KubernetesApi,\n                                                                   override protected val as: ActorSystem,\n                                                                   protected val ec: ExecutionContext,\n                                                                   protected val logging: Logging)\n    extends Container {\n\n  /** The last read timestamp in the log file */\n  private val lastTimestamp = new AtomicReference[Option[Instant]](None)\n\n  protected val waitForLogs: FiniteDuration = 2.seconds\n\n  override def suspend()(implicit transid: TransactionId): Future[Unit] = {\n    super.suspend().flatMap(_ => kubernetes.suspend(this))\n  }\n\n  override def resume()(implicit transid: TransactionId): Future[Unit] =\n    kubernetes.resume(this).flatMap(_ => super.resume())\n\n  override def destroy()(implicit transid: TransactionId): Future[Unit] = {\n    super.destroy()\n    portForward.foreach(_.close())\n    kubernetes.rm(this)\n  }\n\n  override def initialize(initializer: JsObject,\n                          timeout: FiniteDuration,\n                          maxConcurrent: Int,\n                          entity: Option[WhiskAction] = None)(implicit transid: TransactionId): Future[Interval] = {\n    entity match {\n      case Some(e) => {\n        kubernetes\n          .addLabel(this, Map(\"openwhisk/action\" -> e.name.toString, \"openwhisk/namespace\" -> e.namespace.toString))\n          .map(return super.initialize(initializer, timeout, maxConcurrent, entity))\n      }\n      case None => super.initialize(initializer, timeout, maxConcurrent, entity)\n    }\n  }\n\n  def logs(limit: ByteSize, waitForSentinel: Boolean)(implicit transid: TransactionId): Source[ByteString, Any] = {\n\n    kubernetes\n      .logs(this, lastTimestamp.get, waitForSentinel)\n      .limitWeighted(limit.toBytes) { obj =>\n        // Adding + 1 since we know there's a newline byte being read\n        obj.jsonSize.toLong + 1\n      }\n      .map { line =>\n        lastTimestamp.set(Option(line.time))\n        line\n      }\n      .via(new CompleteAfterOccurrences(_.log == Container.ACTIVATION_LOG_SENTINEL, 2, waitForSentinel))\n      .recover {\n        case _: StreamLimitReachedException =>\n          // While the stream has already ended by failing the limitWeighted stage above, we inject a truncation\n          // notice downstream, which will be processed as usual. This will be the last element of the stream.\n          TypedLogLine(Instant.now, \"stderr\", Messages.truncateLogs(limit))\n        case _: OccurrencesNotFoundException | _: FramingException =>\n          // Stream has already ended and we insert a notice that data might be missing from the logs. While a\n          // FramingException can also mean exceeding the limits, we cannot decide which case happened so we resort\n          // to the general error message. This will be the last element of the stream.\n          TypedLogLine(Instant.now, \"stderr\", Messages.logFailure)\n      }\n      .takeWithin(waitForLogs)\n      .map { _.toByteString }\n  }\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/kubernetes/KubernetesContainerFactory.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.kubernetes\n\nimport org.apache.pekko.actor.ActorSystem\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.Await\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.{\n  Container,\n  ContainerArgsConfig,\n  ContainerFactory,\n  ContainerFactoryProvider,\n  RuntimesRegistryConfig\n}\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.core.entity.InvokerInstanceId\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\n\nclass KubernetesContainerFactory(\n  label: String,\n  config: WhiskConfig,\n  containerArgsConfig: ContainerArgsConfig = loadConfigOrThrow[ContainerArgsConfig](ConfigKeys.containerArgs),\n  runtimesRegistryConfig: RuntimesRegistryConfig =\n    loadConfigOrThrow[RuntimesRegistryConfig](ConfigKeys.runtimesRegistry),\n  userImagesRegistryConfig: RuntimesRegistryConfig = loadConfigOrThrow[RuntimesRegistryConfig](\n    ConfigKeys.userImagesRegistry))(implicit actorSystem: ActorSystem, ec: ExecutionContext, logging: Logging)\n    extends ContainerFactory {\n\n  implicit val kubernetes = initializeKubeClient()\n\n  private def initializeKubeClient(): KubernetesClient = {\n    val config = loadConfigOrThrow[KubernetesClientConfig](ConfigKeys.kubernetes)\n    new KubernetesClient(config)(ec)\n  }\n\n  /** Perform cleanup on init */\n  override def init(): Unit = cleanup()\n\n  override def cleanup() = {\n    logging.info(this, \"Cleaning up function runtimes\")\n    val labels = Map(\"invoker\" -> label, \"release\" -> KubernetesContainerFactoryProvider.release)\n    val cleaning = kubernetes.rm(labels, true)(TransactionId.invokerNanny)\n    Await.ready(cleaning, KubernetesContainerFactoryProvider.runtimeDeleteTimeout)\n  }\n\n  override def createContainer(\n    tid: TransactionId,\n    name: String,\n    actionImage: ImageName,\n    userProvidedImage: Boolean,\n    memory: ByteSize,\n    cpuShares: Int,\n    cpuLimit: Option[Double])(implicit config: WhiskConfig, logging: Logging): Future[Container] = {\n    val image = actionImage.resolveImageName(Some(\n      ContainerFactory.resolveRegistryConfig(userProvidedImage, runtimesRegistryConfig, userImagesRegistryConfig).url))\n\n    KubernetesContainer.create(\n      tid,\n      name,\n      image,\n      userProvidedImage,\n      memory,\n      environment = Map(\"__OW_API_HOST\" -> config.wskApiHost) ++ containerArgsConfig.extraEnvVarMap,\n      labels = Map(\"invoker\" -> label, \"release\" -> KubernetesContainerFactoryProvider.release))\n  }\n}\n\nobject KubernetesContainerFactoryProvider extends ContainerFactoryProvider {\n\n  val release = loadConfigOrThrow[String](\"whisk.helm.release\")\n  val runtimeDeleteTimeout = loadConfigOrThrow[FiniteDuration](\"whisk.runtime.delete.timeout\")\n\n  override def instance(actorSystem: ActorSystem,\n                        logging: Logging,\n                        config: WhiskConfig,\n                        instance: InvokerInstanceId,\n                        parameters: Map[String, Set[String]]): ContainerFactory =\n    new KubernetesContainerFactory(s\"invoker${instance.toInt}\", config)(actorSystem, actorSystem.dispatcher, logging)\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/kubernetes/WhiskPodBuilder.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.kubernetes\n\nimport java.io.ByteArrayInputStream\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport io.fabric8.kubernetes.api.builder.Predicate\nimport io.fabric8.kubernetes.api.model.policy.{PodDisruptionBudget, PodDisruptionBudgetBuilder}\nimport io.fabric8.kubernetes.api.model.{\n  ContainerBuilder,\n  EnvVarBuilder,\n  EnvVarSourceBuilder,\n  IntOrString,\n  LabelSelectorBuilder,\n  Pod,\n  PodBuilder,\n  Quantity\n}\nimport io.fabric8.kubernetes.client.NamespacedKubernetesClient\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.ByteSize\n\nimport scala.collection.JavaConverters._\n\nclass WhiskPodBuilder(client: NamespacedKubernetesClient, config: KubernetesClientConfig) {\n  private val template = config.podTemplate.map(_.value.getBytes(UTF_8))\n  private val actionContainerName = KubernetesRestLogSourceStage.actionContainerName\n  private val actionContainerPredicate: Predicate[ContainerBuilder] = (cb) => cb.getName == actionContainerName\n\n  def affinityEnabled: Boolean = config.userPodNodeAffinity.enabled\n\n  def buildPodSpec(\n    name: String,\n    image: String,\n    memory: ByteSize,\n    environment: Map[String, String],\n    labels: Map[String, String],\n    config: KubernetesClientConfig)(implicit transid: TransactionId): (Pod, Option[PodDisruptionBudget]) = {\n    val envVars = environment.map {\n      case (key, value) => new EnvVarBuilder().withName(key).withValue(value).build()\n    }.toSeq ++ config.fieldRefEnvironment\n      .map(_.map({\n        case (key, value) =>\n          new EnvVarBuilder()\n            .withName(key)\n            .withValueFrom(new EnvVarSourceBuilder().withNewFieldRef().withFieldPath(value).endFieldRef().build())\n            .build()\n      }).toSeq)\n      .getOrElse(Seq.empty)\n\n    val baseBuilder = template match {\n      case Some(bytes) =>\n        new PodBuilder(loadPodSpec(bytes))\n      case None => new PodBuilder()\n    }\n\n    val pb1 = baseBuilder\n      .editOrNewMetadata()\n      .withName(name)\n      .addToLabels(\"name\", name)\n      .addToLabels(\"user-action-pod\", \"true\")\n      .addToLabels(labels.asJava)\n      .endMetadata()\n\n    val specBuilder = pb1.editOrNewSpec().withRestartPolicy(\"Always\")\n\n    if (config.userPodNodeAffinity.enabled) {\n      val affinity = specBuilder\n        .editOrNewAffinity()\n        .editOrNewNodeAffinity()\n        .editOrNewRequiredDuringSchedulingIgnoredDuringExecution()\n      affinity\n        .addNewNodeSelectorTerm()\n        .addNewMatchExpression()\n        .withKey(config.userPodNodeAffinity.key)\n        .withOperator(\"In\")\n        .withValues(config.userPodNodeAffinity.value)\n        .endMatchExpression()\n        .endNodeSelectorTerm()\n        .endRequiredDuringSchedulingIgnoredDuringExecution()\n        .endNodeAffinity()\n        .endAffinity()\n    }\n\n    val containerBuilder = if (specBuilder.hasMatchingContainer(actionContainerPredicate)) {\n      specBuilder.editMatchingContainer(actionContainerPredicate)\n    } else specBuilder.addNewContainer()\n\n    //if cpu scaling is enabled, calculate cpu from memory, 100m per 256Mi, min is 100m(.1cpu), max is 10000 (10cpu)\n    val cpu = config.cpuScaling\n      .map(cpuConfig => Map(\"cpu\" -> new Quantity(calculateCpu(cpuConfig, memory) + \"m\")))\n      .getOrElse(Map.empty)\n\n    val diskLimit = config.ephemeralStorage\n      .map(\n        diskConfig =>\n          // Scale the ephemeral storage unless it exceeds the limit, if it exceeds the limit use the limit.\n          if ((diskConfig.scaleFactor > 0) && (diskConfig.scaleFactor * memory.toMB < diskConfig.limit.toMB)) {\n            Map(\"ephemeral-storage\" -> new Quantity(diskConfig.scaleFactor * memory.toMB + \"Mi\"))\n          } else {\n            Map(\"ephemeral-storage\" -> new Quantity(diskConfig.limit.toMB + \"Mi\"))\n        })\n      .getOrElse(Map.empty)\n\n    //In container its assumed that env, port, resource limits are set explicitly\n    //Here if any value exist in template then that would be overridden\n    containerBuilder\n      .withNewResources()\n      //explicitly set requests and limits to same values\n      .withLimits((Map(\"memory\" -> new Quantity(memory.toMB + \"Mi\")) ++ cpu ++ diskLimit).asJava)\n      .withRequests((Map(\"memory\" -> new Quantity(memory.toMB + \"Mi\")) ++ cpu ++ diskLimit).asJava)\n      .endResources()\n      .withName(actionContainerName)\n      .withImage(image)\n      .withEnv(envVars.asJava)\n      .addNewPort()\n      .withContainerPort(8080)\n      .withName(\"action\")\n      .endPort()\n\n    //If any existing context entry is present then \"update\" it else add new\n    containerBuilder\n      .editOrNewSecurityContext()\n      .editOrNewCapabilities()\n      .addToDrop(\"NET_RAW\", \"NET_ADMIN\")\n      .endCapabilities()\n      .endSecurityContext()\n\n    val pod = containerBuilder\n      .endContainer()\n      .endSpec()\n      .build()\n    val pdb = if (config.pdbEnabled) {\n      Some(\n        new PodDisruptionBudgetBuilder().withNewMetadata\n          .withName(name)\n          .addToLabels(labels.asJava)\n          .endMetadata()\n          .withNewSpec()\n          .withMinAvailable(new IntOrString(1))\n          .withSelector(new LabelSelectorBuilder().withMatchLabels(Map(\"name\" -> name).asJava).build())\n          .and\n          .build)\n    } else {\n      None\n    }\n    (pod, pdb)\n  }\n\n  def calculateCpu(c: KubernetesCpuScalingConfig, memory: ByteSize): Int = {\n    val cpuPerMemorySegment = c.millicpus\n    val cpuMin = c.millicpus\n    val cpuMax = c.maxMillicpus\n    math.min(math.max((memory.toMB / c.memory.toMB) * cpuPerMemorySegment, cpuMin), cpuMax).toInt\n  }\n\n  private def loadPodSpec(bytes: Array[Byte]): Pod = {\n    val resources = client.load(new ByteArrayInputStream(bytes))\n    resources.get().get(0).asInstanceOf[Pod]\n  }\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/v2/ActivationClientProxy.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.v2\n\nimport org.apache.pekko.actor.Status.{Failure => FailureMessage}\nimport org.apache.pekko.actor.{ActorSystem, FSM, Props, Stash}\nimport org.apache.pekko.grpc.internal.ClientClosedException\nimport org.apache.pekko.pattern.pipe\nimport io.grpc.StatusRuntimeException\nimport org.apache.openwhisk.common.{GracefulShutdown, Logging, TransactionId}\nimport org.apache.openwhisk.core.connector.ActivationMessage\nimport org.apache.openwhisk.core.containerpool.ContainerId\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.scheduler.SchedulerEndpoints\nimport org.apache.openwhisk.core.scheduler.grpc.ActivationResponse\nimport org.apache.openwhisk.core.scheduler.queue.{ActionMismatch, MemoryQueueError, NoActivationMessage, NoMemoryQueue}\nimport org.apache.openwhisk.grpc.{ActivationServiceClient, FetchRequest, RescheduleRequest, RescheduleResponse}\nimport spray.json.JsonParser.ParsingException\n\nimport scala.concurrent.Future\nimport scala.util.{Success, Try}\n\n// Event send by the actor\ncase object ClientCreationCompleted\ncase object ClientClosed\n\n// Event received by the actor\ncase object StartClient\ncase class RequestActivation(lastDuration: Option[Long] = None, newScheduler: Option[SchedulerEndpoints] = None)\ncase class RescheduleActivation(invocationNamespace: String,\n                                fqn: FullyQualifiedEntityName,\n                                rev: DocRevision,\n                                msg: ActivationMessage)\ncase object RetryRequestActivation\ncase object ContainerWarmed\ncase object CloseClientProxy\ncase object StopClientProxy\n\n// state\nsealed trait ActivationClientProxyState\ncase object ClientProxyUninitialized extends ActivationClientProxyState\ncase object ClientProxyReady extends ActivationClientProxyState\ncase object ClientProxyRemoving extends ActivationClientProxyState\n\n// data\nsealed trait ActivationClientProxyData\ncase class Client(activationClient: ActivationServiceClient, rpcHost: String, rpcPort: Int)\n    extends ActivationClientProxyData\ncase class Retry(count: Int) extends ActivationClientProxyData\n\nclass ActivationClientProxy(\n  invocationNamespace: String,\n  action: FullyQualifiedEntityName,\n  rev: DocRevision,\n  schedulerHost: String,\n  rpcPort: Int,\n  containerId: ContainerId,\n  activationClientFactory: (String, FullyQualifiedEntityName, String, Int, Boolean) => Future[ActivationServiceClient])(\n  implicit actorSystem: ActorSystem,\n  logging: Logging)\n    extends FSM[ActivationClientProxyState, ActivationClientProxyData]\n    with Stash {\n\n  implicit val ec = actorSystem.dispatcher\n\n  private var warmed = false\n\n  startWith(ClientProxyUninitialized, Retry(3))\n\n  when(ClientProxyUninitialized) {\n    case Event(StartClient, r: Retry) =>\n      // build activation client using original scheduler endpoint firstly\n      createActivationClient(invocationNamespace, action, schedulerHost, rpcPort, tryOtherScheduler = false)\n        .pipeTo(self)\n\n      stay using r\n\n    case Event(client: ActivationClient, _) =>\n      context.parent ! ClientCreationCompleted\n\n      goto(ClientProxyReady) using Client(client.client, client.rpcHost, client.rpcPort)\n\n    case Event(f: FailureMessage, _) =>\n      logging.error(this, s\"failed to create grpc client for ${action} caused by: $f\")\n      context.parent ! f\n\n      self ! ClientClosed\n\n      goto(ClientProxyRemoving)\n\n    case _ => delay\n  }\n\n  when(ClientProxyReady) {\n    case Event(request: RequestActivation, client: Client) =>\n      request.newScheduler match {\n        // if scheduler is changed, client needs to be recreated\n        case Some(scheduler) if scheduler.host != client.rpcHost || scheduler.rpcPort != client.rpcPort =>\n          val newHost = request.newScheduler.get.host\n          val newPort = request.newScheduler.get.rpcPort\n          client.activationClient\n            .close()\n            .flatMap(_ =>\n              createActivationClient(invocationNamespace, action, newHost, newPort, tryOtherScheduler = false))\n            .pipeTo(self)\n\n        case _ =>\n          requestActivationMessage(invocationNamespace, action, rev, client.activationClient, request.lastDuration)\n            .pipeTo(self)\n      }\n      stay()\n\n    case Event(e: RescheduleActivation, client: Client) =>\n      logging.info(\n        this,\n        s\"[${containerId.asString}] got a reschedule message ${e.msg.activationId} for action: ${e.msg.action}\")\n      client.activationClient\n        .rescheduleActivation(\n          RescheduleRequest(e.invocationNamespace, e.fqn.serialize, e.rev.serialize, e.msg.serialize))\n        .recover {\n          case t =>\n            logging.error(this, s\"[${containerId.asString}] Failed to reschedule activation (error: $t)\")\n            RescheduleResponse()\n        }\n        .foreach(res => {\n          context.parent ! res\n        })\n      stay()\n\n    case Event(msg: ActivationMessage, _: Client) =>\n      logging.debug(this, s\"[${containerId.asString}] got a message ${msg.activationId} for action: ${msg.action}\")\n      context.parent ! msg\n\n      stay()\n\n    /**\n     * Case of scheduler error\n     */\n    case Event(error: MemoryQueueError, c: Client) =>\n      error match {\n        case _: NoMemoryQueue =>\n          logging.error(\n            this,\n            s\"[${containerId.asString}] The queue of action ${action} under invocationNamespace ${invocationNamespace} does not exist. Check for queues in other schedulers.\")\n          c.activationClient\n            .close()\n            .flatMap(_ =>\n              createActivationClient(invocationNamespace, action, c.rpcHost, c.rpcPort, tryOtherScheduler = true))\n            .pipeTo(self)\n\n          stay()\n\n        case _: ActionMismatch =>\n          val errorMsg = s\"[${containerId.asString}] action version does not match: $action\"\n          logging.error(this, errorMsg)\n          c.activationClient.close().andThen {\n            case _ =>\n              context.parent ! FailureMessage(new RuntimeException(errorMsg))\n              self ! ClientClosed\n          }\n\n          goto(ClientProxyRemoving)\n\n        case _: NoActivationMessage => // retry\n          logging.debug(this, s\"[${containerId.asString}] no activation message exist: $action\")\n          context.parent ! RetryRequestActivation\n\n          stay()\n      }\n\n    /**\n     * Case of system error like grpc, parsing message\n     */\n    case Event(f: FailureMessage, c: Client) =>\n      f.cause match {\n        case t: ParsingException =>\n          logging.error(this, s\"[${containerId.asString}] failed to parse activation message: $t\")\n          context.parent ! RetryRequestActivation\n\n          stay()\n\n        // When scheduler pod recreated, the StatusRuntimeException with `Unable to resolve host` would happen.\n        // In such situation, it is better to stop the activationClientProxy, otherwise, in short time,\n        // it would print huge log due to create another grpcClient to fetch activation again.\n        case t: StatusRuntimeException if t.getMessage.contains(ActivationClientProxy.hostResolveError) =>\n          logging.error(this, s\"[${containerId.asString}] pekko grpc server connection failed: $t\")\n          context.parent ! FailureMessage(t)\n          self ! ClientClosed\n\n          goto(ClientProxyRemoving)\n\n        case t: StatusRuntimeException =>\n          logging.error(this, s\"[${containerId.asString}] pekko grpc server connection failed: $t\")\n          c.activationClient\n            .close()\n            .flatMap(_ =>\n              createActivationClient(invocationNamespace, action, c.rpcHost, c.rpcPort, tryOtherScheduler = true))\n            .pipeTo(self)\n\n          stay()\n\n        case t: ClientClosedException =>\n          logging.error(this, s\"[${containerId.asString}] grpc client is already closed for $action\")\n          context.parent ! FailureMessage(t)\n\n          self ! ClientClosed\n\n          goto(ClientProxyRemoving)\n\n        case t: Throwable =>\n          logging.error(this, s\"[${containerId.asString}] get activation from remote server error: $t\")\n          context.parent ! FailureMessage(t)\n\n          safelyCloseClient(c)\n          goto(ClientProxyRemoving)\n      }\n\n    case Event(client: ActivationClient, _) =>\n      // long poll\n      requestActivationMessage(invocationNamespace, action, rev, client.client)\n        .pipeTo(self)\n\n      stay using Client(client.client, client.rpcHost, client.rpcPort)\n  }\n\n  when(ClientProxyRemoving) {\n\n    // This is the case where the last activation message is sent to the container proxy and container proxy requested\n    // another activation. But the activation client is being shut down and it no longer fetches any request.\n    case Event(_: RequestActivation, c: Client) =>\n      safelyCloseClient(c)\n\n      stay()\n\n    case Event(msg: ActivationMessage, _: Client) =>\n      context.parent ! msg\n\n      stay()\n\n    case Event(_: MemoryQueueError, c: Client) =>\n      safelyCloseClient(c)\n      self ! ClientClosed\n\n      stay()\n\n    case Event(f: FailureMessage, c: Client) =>\n      logging.error(\n        this,\n        s\"[${containerId.asString}] some error happened for action: ${action} in state: $stateName, caused by: $f\")\n      safelyCloseClient(c)\n      stay()\n\n    case Event(client: ActivationClient, _) =>\n      // long poll\n      requestActivationMessage(invocationNamespace, action, rev, client.client)\n        .pipeTo(self)\n\n      stay using Client(client.client, client.rpcHost, client.rpcPort)\n  }\n\n  // Unstash all messages stashed while in intermediate state\n  onTransition {\n    case _ -> ClientProxyReady    => unstashAll()\n    case _ -> ClientProxyRemoving => unstashAll()\n  }\n\n  whenUnhandled {\n    case Event(ContainerWarmed, _) =>\n      warmed = true\n      stay\n\n    // When disabling an invoker, there could still be activations in the queue.\n    // The activation client keeps fetching data and will forward it to the container(parent).\n    // Once it receives `NoActivationMessage` from the queue, it will close the activation client and send `ClientClosed`\n    // to the container(parent), rather than sending `RetryRequestActivation`.\n    // When a container proxy(parent) receives `ClientClosed`, it will finally shut down.\n    case Event(GracefulShutdown, _: Client) =>\n      logging.info(this, s\"[${containerId.asString}] safely close client proxy and go to the ClientProxyRemoving state\")\n\n      goto(ClientProxyRemoving)\n\n    case Event(ClientClosed, _) =>\n      logging.info(\n        this,\n        s\"[${containerId.asString}] the underlying client is closed, stopping the activation client proxy\")\n      context.parent ! ClientClosed\n\n      stop()\n\n    case Event(StopClientProxy, c: Client) =>\n      logging.info(this, s\"[${containerId.asString}] stop close client proxy and go to the ClientProxyRemoving state\")\n      safelyCloseClient(c)\n      stay()\n  }\n\n  initialize()\n\n  /** Delays all incoming messages until unstashAll() is called */\n  def delay = {\n    stash()\n    stay\n  }\n\n  /**\n   * Safely shut down the client.\n   */\n  private def safelyCloseClient(client: Client): Unit = {\n    Try {\n      client.activationClient\n        .fetchActivation(\n          FetchRequest(\n            TransactionId(TransactionId.generateTid()).serialize,\n            invocationNamespace,\n            action.serialize,\n            rev.serialize,\n            containerId.asString,\n            warmed,\n            None,\n            false))\n        .andThen {\n          case _ =>\n            client.activationClient.close().andThen {\n              case _ => self ! ClientClosed\n            }\n        }\n    }.recover {\n      // If the fetchActivation is executed when the client is closed, the andThen statement is not executed.\n      case _: ClientClosedException =>\n        self ! ClientClosed\n    }\n  }\n\n  /**\n   * Request activation message to scheduler by long poll\n   *\n   * @return ActivationMessage or MemoryQueueError\n   */\n  private def requestActivationMessage(invocationNamespace: String,\n                                       fqn: FullyQualifiedEntityName,\n                                       rev: DocRevision,\n                                       client: ActivationServiceClient,\n                                       lastDuration: Option[Long] = None) = {\n    Try {\n      client\n        .fetchActivation(\n          FetchRequest(\n            TransactionId(TransactionId.generateTid()).serialize,\n            invocationNamespace,\n            fqn.serialize,\n            rev.serialize,\n            containerId.asString,\n            warmed,\n            lastDuration,\n            true))\n        .flatMap { r =>\n          Future(ActivationResponse.parse(r.activationMessage))\n            .flatMap(Future.fromTry)\n            .flatMap {\n              case ActivationResponse(Right(msg)) =>\n                Future.successful(msg)\n              case ActivationResponse(Left(msg)) =>\n                Future.successful(msg)\n            }\n        }\n    }.recover {\n        case _: ClientClosedException =>\n          logging.debug(this, s\"grpc client is closed for $fqn in the Try closure\")\n          Future.successful(ClientClosed)\n      }\n      .getOrElse(Future.failed(new RuntimeException(s\"error to get $fqn activation from grpc server\")))\n  }\n\n  private def createActivationClient(invocationNamespace: String,\n                                     fqn: FullyQualifiedEntityName,\n                                     schedulerHost: String,\n                                     rpcPort: Int,\n                                     tryOtherScheduler: Boolean,\n                                     retry: Int = 5): Future[ActivationClient] = {\n    activationClientFactory(invocationNamespace, fqn, schedulerHost, rpcPort, tryOtherScheduler)\n      .map { client =>\n        ActivationClient(client, schedulerHost, rpcPort)\n      }\n      .andThen {\n        case Success(_) => logging.debug(this, \"The gRPC client created successfully\")\n      }\n      .recoverWith {\n        case _: Throwable =>\n          if (retry < 5)\n            createActivationClient(invocationNamespace, action, schedulerHost, rpcPort, tryOtherScheduler, retry - 1)\n          else {\n            Future.failed(new Exception(\"The number of client creation retries has been exceeded.\"))\n          }\n      }\n  }\n}\n\nobject ActivationClientProxy {\n\n  val hostResolveError = \"Unable to resolve host\"\n\n  def props(invocationNamespace: String,\n            action: FullyQualifiedEntityName,\n            rev: DocRevision,\n            schedulerHost: String,\n            rpcPort: Int,\n            containerId: ContainerId,\n            activationClientFactory: (\n              String,\n              FullyQualifiedEntityName,\n              String,\n              Int,\n              Boolean) => Future[ActivationServiceClient])(implicit actorSystem: ActorSystem, logging: Logging) = {\n    Props(\n      new ActivationClientProxy(\n        invocationNamespace,\n        action,\n        rev,\n        schedulerHost,\n        rpcPort,\n        containerId,\n        activationClientFactory))\n  }\n}\n\ncase class ActivationClient(client: ActivationServiceClient, rpcHost: String, rpcPort: Int)\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/v2/FunctionPullingContainerPool.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.v2\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, Cancellable, Props}\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.connector.ContainerCreationError._\nimport org.apache.openwhisk.core.connector.{\n  ContainerCreationAckMessage,\n  ContainerCreationMessage,\n  ContainerDeletionMessage,\n  GetState,\n  ResultMetadata\n}\nimport org.apache.openwhisk.core.containerpool.{\n  AdjustPrewarmedContainer,\n  BlackboxStartupError,\n  ColdStartKey,\n  ContainerPool,\n  ContainerPoolConfig,\n  ContainerRemoved,\n  PrewarmingConfig,\n  WhiskContainerStartupError\n}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\nimport spray.json.DefaultJsonProtocol\n\nimport scala.annotation.tailrec\nimport scala.collection.concurrent.TrieMap\nimport scala.collection.immutable\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.util.{Random, Try}\nimport scala.collection.immutable.Queue\n\nobject TotalContainerPoolState extends DefaultJsonProtocol {\n  implicit val prewarmedPoolSerdes = jsonFormat2(PrewarmedContainerPoolState.apply)\n  implicit val warmPoolSerdes = jsonFormat2(WarmContainerPoolState.apply)\n  implicit val totalPoolSerdes = jsonFormat5(TotalContainerPoolState.apply)\n}\n\ncase class PrewarmedContainerPoolState(total: Int, countsByKind: Map[String, Int])\ncase class WarmContainerPoolState(total: Int, containers: List[BasicContainerInfo])\ncase class TotalContainerPoolState(totalContainers: Int,\n                                   inProgressCount: Int,\n                                   prewarmedPool: PrewarmedContainerPoolState,\n                                   busyPool: WarmContainerPoolState,\n                                   pausedPool: WarmContainerPoolState) {\n  def serialize(): String = TotalContainerPoolState.totalPoolSerdes.write(this).compactPrint\n}\n\ncase class NotSupportedPoolState() {\n  def serialize(): String = \"not supported\"\n}\n\ncase class CreationContainer(creationMessage: ContainerCreationMessage, action: WhiskAction)\ncase class DeletionContainer(deletionMessage: ContainerDeletionMessage)\ncase object Remove\ncase class Keep(timeout: FiniteDuration)\ncase class PrewarmContainer(maxConcurrent: Int)\n\n/**\n * A pool managing containers to run actions on.\n *\n * This pool fulfills the other half of the ContainerProxy contract. Only\n * one job (either Start or Run) is sent to a child-actor at any given\n * time. The pool then waits for a response of that container, indicating\n * the container is done with the job. Only then will the pool send another\n * request to that container.\n *\n * Upon actor creation, the pool will start to prewarm containers according\n * to the provided prewarmConfig, iff set. Those containers will **not** be\n * part of the poolsize calculation, which is capped by the poolSize parameter.\n * Prewarm containers are only used, if they have matching arguments\n * (kind, memory) and there is space in the pool.\n *\n * @param childFactory method to create new container proxy actor\n * @param prewarmConfig optional settings for container prewarming\n * @param poolConfig config for the ContainerPool\n */\nclass FunctionPullingContainerPool(\n  childFactory: ActorRefFactory => ActorRef,\n  invokerHealthService: ActorRef,\n  poolConfig: ContainerPoolConfig,\n  instance: InvokerInstanceId,\n  prewarmConfig: List[PrewarmingConfig] = List.empty,\n  sendAckToScheduler: (SchedulerInstanceId, ContainerCreationAckMessage) => Future[ResultMetadata])(\n  implicit val logging: Logging)\n    extends Actor {\n  import ContainerPoolV2.memoryConsumptionOf\n\n  implicit val ec = context.system.dispatcher\n\n  protected[containerpool] var busyPool = immutable.Map.empty[ActorRef, ContainerAvailableData]\n  protected[containerpool] var inProgressPool = immutable.Map.empty[ActorRef, Data]\n  protected[containerpool] var warmedPool = immutable.Map.empty[ActorRef, WarmData]\n  protected[containerpool] var prewarmedPool = immutable.Map.empty[ActorRef, PreWarmData]\n  protected[containerpool] var prewarmStartingPool = immutable.Map.empty[ActorRef, (String, ByteSize)]\n\n  // for shutting down\n  protected[containerpool] var disablingPool = immutable.Set.empty[ActorRef]\n\n  private var shuttingDown = false\n\n  private val creationMessages = TrieMap[ActorRef, ContainerCreationMessage]()\n\n  private var preWarmScheduler: Option[Cancellable] = None\n  private var prewarmConfigQueue = Queue.empty[(CodeExec[_], ByteSize, Option[FiniteDuration])]\n  private val prewarmCreateFailedCount = new AtomicInteger(0)\n\n  val logScheduler = context.system.scheduler.scheduleAtFixedRate(0.seconds, 1.seconds)(() => {\n    MetricEmitter.emitHistogramMetric(\n      LoggingMarkers.INVOKER_CONTAINERPOOL_MEMORY(\"inprogress\"),\n      memoryConsumptionOf(inProgressPool))\n    MetricEmitter\n      .emitHistogramMetric(LoggingMarkers.INVOKER_CONTAINERPOOL_MEMORY(\"busy\"), memoryConsumptionOf(busyPool))\n    MetricEmitter\n      .emitHistogramMetric(LoggingMarkers.INVOKER_CONTAINERPOOL_MEMORY(\"prewarmed\"), memoryConsumptionOf(prewarmedPool))\n    MetricEmitter\n      .emitHistogramMetric(LoggingMarkers.INVOKER_CONTAINERPOOL_MEMORY(\"warmed\"), memoryConsumptionOf(warmedPool))\n    MetricEmitter.emitHistogramMetric(LoggingMarkers.INVOKER_CONTAINERPOOL_MEMORY(\"max\"), poolConfig.userMemory.toMB)\n    val prewarmedSize = prewarmedPool.size\n    val busySize = busyPool.size\n    val warmedSize = warmedPool.size\n    val warmedPoolMap = warmedPool groupBy {\n      case (_, warmedData) => (warmedData.invocationNamespace, warmedData.action.toString)\n    } mapValues (_.size)\n    for ((data, size) <- warmedPoolMap) {\n      val tags: Option[Map[String, String]] = Some(Map(\"namespace\" -> data._1, \"action\" -> data._2))\n      MetricEmitter.emitHistogramMetric(LoggingMarkers.INVOKER_CONTAINERPOOL_CONTAINER(\"warmed\", tags), size)\n    }\n    val allSize = prewarmedSize + busySize + warmedSize\n    MetricEmitter.emitHistogramMetric(LoggingMarkers.INVOKER_CONTAINERPOOL_CONTAINER(\"prewarmed\"), prewarmedSize)\n    MetricEmitter.emitHistogramMetric(LoggingMarkers.INVOKER_CONTAINERPOOL_CONTAINER(\"busy\"), busySize)\n    MetricEmitter.emitHistogramMetric(LoggingMarkers.INVOKER_CONTAINERPOOL_CONTAINER(\"all\"), allSize)\n  })\n\n  // Key is ColdStartKey, value is the number of cold Start in minute\n  var coldStartCount = immutable.Map.empty[ColdStartKey, Int]\n\n  adjustPrewarmedContainer(true, false)\n\n  // check periodically, adjust prewarmed container(delete if unused for some time and create some increment containers)\n  // add some random amount to this schedule to avoid a herd of container removal + creation\n  val interval = poolConfig.prewarmExpirationCheckInterval + poolConfig.prewarmExpirationCheckIntervalVariance\n    .map(v =>\n      Random\n        .nextInt(v.toSeconds.toInt))\n    .getOrElse(0)\n    .seconds\n\n  if (prewarmConfig.exists(!_.reactive.isEmpty)) {\n    context.system.scheduler.scheduleAtFixedRate(\n      poolConfig.prewarmExpirationCheckInitDelay,\n      interval,\n      self,\n      AdjustPrewarmedContainer)\n  }\n\n  val resourceSubmitter = context.system.scheduler.scheduleAtFixedRate(0.seconds, poolConfig.memorySyncInterval)(() => {\n    syncMemoryInfo\n  })\n\n  private def logContainerStart(c: ContainerCreationMessage, action: WhiskAction, containerState: String): Unit = {\n    val FQN = c.action\n    if (FQN.namespace.name == \"whisk.system\" && FQN.fullPath.segments > 2) {\n      MetricEmitter.emitCounterMetric(LoggingMarkers.INVOKER_SHAREDPACKAGE(FQN.fullPath.asString))\n    }\n\n    MetricEmitter.emitCounterMetric(\n      LoggingMarkers.INVOKER_CONTAINER_START(\n        containerState,\n        c.invocationNamespace,\n        c.action.namespace.toString,\n        c.action.name.toString))\n  }\n\n  def receive: Receive = {\n    case PrewarmContainer(maxConcurrent) =>\n      if (prewarmConfigQueue.isEmpty) {\n        preWarmScheduler.map(_.cancel())\n        preWarmScheduler = None\n      } else {\n        for (_ <- 1 to maxConcurrent if !prewarmConfigQueue.isEmpty) {\n          val ((codeExec, byteSize, ttl), newQueue) = prewarmConfigQueue.dequeue\n          prewarmConfigQueue = newQueue\n          prewarmContainer(codeExec, byteSize, ttl)\n        }\n      }\n\n    case CreationContainer(create: ContainerCreationMessage, action: WhiskAction) =>\n      if (shuttingDown) {\n        val message =\n          s\"creationId: ${create.creationId}, invoker is shutting down, reschedule ${action.fullyQualifiedName(false)}\"\n        val ack = ContainerCreationAckMessage(\n          create.transid,\n          create.creationId,\n          create.invocationNamespace,\n          create.action,\n          create.revision,\n          create.whiskActionMetaData,\n          instance,\n          create.schedulerHost,\n          create.rpcPort,\n          create.retryCount,\n          Some(ShuttingDownError),\n          Some(message))\n        logging.warn(this, message)\n        sendAckToScheduler(create.rootSchedulerIndex, ack)\n      } else {\n        logging.info(this, s\"received a container creation message: ${create.creationId}\")\n        action.toExecutableWhiskAction match {\n          case Some(executable) =>\n            val createdContainer =\n              takeWarmedContainer(executable, create.invocationNamespace, create.revision)\n                .map(container => (container, \"warmed\"))\n                .orElse {\n                  takeContainer(executable)\n                }\n            handleChosenContainer(create, executable, createdContainer)\n          case None =>\n            val message =\n              s\"creationId: ${create.creationId}, non-executable action reached the container pool ${action.fullyQualifiedName(false)}\"\n            logging.error(this, message)\n            val ack = ContainerCreationAckMessage(\n              create.transid,\n              create.creationId,\n              create.invocationNamespace,\n              create.action,\n              create.revision,\n              create.whiskActionMetaData,\n              instance,\n              create.schedulerHost,\n              create.rpcPort,\n              create.retryCount,\n              Some(NonExecutableActionError),\n              Some(message))\n            sendAckToScheduler(create.rootSchedulerIndex, ack)\n        }\n      }\n\n    case DeletionContainer(deletionMessage: ContainerDeletionMessage) =>\n      val oldRevision = deletionMessage.revision\n      val invocationNamespace = deletionMessage.invocationNamespace\n      val fqn = deletionMessage.action.copy(version = None)\n\n      warmedPool.foreach(warmed => {\n        val proxy = warmed._1\n        val data = warmed._2\n\n        if (data.invocationNamespace == invocationNamespace\n            && data.action.fullyQualifiedName(withVersion = false) == fqn.copy(version = None)\n            && data.revision <= oldRevision) {\n          proxy ! GracefulShutdown\n        }\n      })\n\n      busyPool.foreach(f = busy => {\n        val proxy = busy._1\n        busy._2 match {\n          case warmData: WarmData\n              if warmData.invocationNamespace == invocationNamespace\n                && warmData.action.fullyQualifiedName(withVersion = false) == fqn.copy(version = None)\n                && warmData.revision <= oldRevision =>\n            proxy ! GracefulShutdown\n          case initializedData: InitializedData\n              if initializedData.invocationNamespace == invocationNamespace\n                && initializedData.action.fullyQualifiedName(withVersion = false) == fqn.copy(version = None) =>\n            proxy ! GracefulShutdown\n          case _ => // Other actions are ignored.\n        }\n      })\n\n    case ReadyToWork(data) =>\n      prewarmStartingPool = prewarmStartingPool - sender()\n      prewarmedPool = prewarmedPool + (sender() -> data)\n      // after create prewarm successfully, reset the value to 0\n      if (prewarmCreateFailedCount.get() > 0) {\n        prewarmCreateFailedCount.set(0)\n      }\n\n    // Container is initialized\n    case Initialized(data) =>\n      busyPool = busyPool + (sender() -> data)\n      inProgressPool = inProgressPool - sender()\n      // container init completed, send creationAck(success) to scheduler\n      creationMessages.remove(sender()).foreach { msg =>\n        val ack = ContainerCreationAckMessage(\n          msg.transid,\n          msg.creationId,\n          msg.invocationNamespace,\n          msg.action,\n          msg.revision,\n          msg.whiskActionMetaData,\n          instance,\n          msg.schedulerHost,\n          msg.rpcPort,\n          msg.retryCount)\n        sendAckToScheduler(msg.rootSchedulerIndex, ack)\n      }\n\n    case Resumed(data) =>\n      busyPool = busyPool + (sender() -> data)\n      inProgressPool = inProgressPool - sender()\n      // container init completed, send creationAck(success) to scheduler\n      creationMessages.remove(sender()).foreach { msg =>\n        val ack = ContainerCreationAckMessage(\n          msg.transid,\n          msg.creationId,\n          msg.invocationNamespace,\n          msg.action,\n          msg.revision,\n          msg.whiskActionMetaData,\n          instance,\n          msg.schedulerHost,\n          msg.rpcPort,\n          msg.retryCount)\n        sendAckToScheduler(msg.rootSchedulerIndex, ack)\n      }\n\n    // if warmed containers is failed to resume, we should try to use other container or create a new one\n    case ResumeFailed(data) =>\n      inProgressPool = inProgressPool - sender()\n      creationMessages.remove(sender()).foreach { msg =>\n        val container = takeWarmedContainer(data.action, data.invocationNamespace, data.revision)\n          .map(container => (container, \"warmed\"))\n          .orElse {\n            takeContainer(data.action)\n          }\n        handleChosenContainer(msg, data.action, container)\n      }\n\n    case ContainerCreationFailed(t) =>\n      val (error, message) = t match {\n        case WhiskContainerStartupError(msg) => (WhiskError, msg)\n        case BlackboxStartupError(msg)       => (BlackBoxError, msg)\n        case _                               => (WhiskError, Messages.resourceProvisionError)\n      }\n      creationMessages.remove(sender()).foreach { msg =>\n        val ack = ContainerCreationAckMessage(\n          msg.transid,\n          msg.creationId,\n          msg.invocationNamespace,\n          msg.action,\n          msg.revision,\n          msg.whiskActionMetaData,\n          instance,\n          msg.schedulerHost,\n          msg.rpcPort,\n          msg.retryCount,\n          Some(error),\n          Some(message))\n        sendAckToScheduler(msg.rootSchedulerIndex, ack)\n      }\n\n    case ContainerIsPaused(data) =>\n      warmedPool = warmedPool + (sender() -> data)\n      busyPool = busyPool - sender() // remove container from busy pool\n\n    // Container got removed\n    case ContainerRemoved(replacePrewarm) =>\n      inProgressPool = inProgressPool - sender()\n      warmedPool = warmedPool - sender()\n      disablingPool -= sender()\n\n      // container was busy (busy indicates at full capacity), so there is capacity to accept another job request\n      busyPool = busyPool - sender()\n\n      //in case this was a prewarm\n      prewarmedPool.get(sender()).foreach { data =>\n        prewarmedPool = prewarmedPool - sender()\n        logging.info(\n          this,\n          s\"${if (replacePrewarm) \"failed\" else \"expired\"} prewarm [kind: ${data.kind}, memory: ${data.memoryLimit.toString}] removed\")\n      }\n\n      //in case this was a starting prewarm\n      prewarmStartingPool.get(sender()).foreach { data =>\n        logging.info(this, s\"failed starting prewarm [kind: ${data._1}, memory: ${data._2.toString}] removed\")\n        prewarmStartingPool = prewarmStartingPool - sender()\n        prewarmCreateFailedCount.incrementAndGet()\n      }\n\n      //backfill prewarms on every ContainerRemoved, just in case\n      if (replacePrewarm) {\n        adjustPrewarmedContainer(false, false) //in case a prewarm is removed due to health failure or crash\n      }\n\n      // there maybe a chance that container create failed or init grpc client failed,\n      // send creationAck(reschedule) to scheduler\n      creationMessages.remove(sender()).foreach { msg =>\n        val ack = ContainerCreationAckMessage(\n          msg.transid,\n          msg.creationId,\n          msg.invocationNamespace,\n          msg.action,\n          msg.revision,\n          msg.whiskActionMetaData,\n          instance,\n          msg.schedulerHost,\n          msg.rpcPort,\n          msg.retryCount,\n          Some(UnknownError),\n          Some(\"ContainerProxy init failed.\"))\n        sendAckToScheduler(msg.rootSchedulerIndex, ack)\n      }\n\n    case GracefulShutdown =>\n      shuttingDown = true\n      waitForPoolToClear()\n\n    case Enable =>\n      shuttingDown = false\n\n    case AdjustPrewarmedContainer =>\n      // Reset the prewarmCreateCount value when do expiration check and backfill prewarm if possible\n      prewarmCreateFailedCount.set(0)\n      adjustPrewarmedContainer(false, true)\n    case GetState =>\n      val totalContainers = busyPool.size + inProgressPool.size + warmedPool.size + prewarmedPool.size\n      val prewarmedState =\n        PrewarmedContainerPoolState(prewarmedPool.size, prewarmedPool.groupBy(_._2.kind).mapValues(_.size).toMap)\n      val busyState = WarmContainerPoolState(busyPool.size, busyPool.values.map(_.basicContainerInfo).toList)\n      val pausedState = WarmContainerPoolState(warmedPool.size, warmedPool.values.map(_.basicContainerInfo).toList)\n      sender() ! TotalContainerPoolState(totalContainers, inProgressPool.size, prewarmedState, busyState, pausedState)\n  }\n\n  /** Install prewarm containers up to the configured requirements for each kind/memory combination or specified kind/memory */\n  private def adjustPrewarmedContainer(init: Boolean, scheduled: Boolean): Unit = {\n    if (!shuttingDown) {\n      if (scheduled) {\n        //on scheduled time, remove expired prewarms\n        ContainerPoolV2.removeExpired(poolConfig, prewarmConfig, prewarmedPool).foreach { p =>\n          prewarmedPool = prewarmedPool - p\n          p ! Remove\n        }\n        //on scheduled time, emit cold start counter metric with memory + kind\n        coldStartCount foreach { coldStart =>\n          val coldStartKey = coldStart._1\n          MetricEmitter.emitCounterMetric(\n            LoggingMarkers.CONTAINER_POOL_PREWARM_COLDSTART(coldStartKey.memory.toString, coldStartKey.kind))\n        }\n      }\n\n      ContainerPoolV2\n        .increasePrewarms(\n          init,\n          scheduled,\n          coldStartCount,\n          prewarmConfig,\n          prewarmedPool,\n          prewarmStartingPool,\n          prewarmConfigQueue)\n        .foreach { c =>\n          val config = c._1\n          val currentCount = c._2._1\n          val desiredCount = c._2._2\n          if (prewarmCreateFailedCount.get() > poolConfig.prewarmMaxRetryLimit) {\n            logging.warn(\n              this,\n              s\"[kind: ${config.exec.kind}, memory: ${config.memoryLimit.toString}] prewarm create failed count exceeds max retry limit: ${poolConfig.prewarmMaxRetryLimit}, currentCount: ${currentCount}, desiredCount: ${desiredCount}\")\n          } else {\n            if (currentCount < desiredCount) {\n              (currentCount until desiredCount).foreach { _ =>\n                poolConfig.prewarmContainerCreationConfig match {\n                  case Some(_) =>\n                    prewarmConfigQueue =\n                      prewarmConfigQueue.enqueue((config.exec, config.memoryLimit, config.reactive.map(_.ttl)))\n                  case None =>\n                    prewarmContainer(config.exec, config.memoryLimit, config.reactive.map(_.ttl))\n                }\n              }\n            }\n          }\n        }\n\n      // run queue consumer\n      poolConfig.prewarmContainerCreationConfig.foreach(config => {\n        logging.info(\n          this,\n          s\"prewarm container creation is starting with creation delay configuration [maxConcurrent: ${config.maxConcurrent}, creationDelay: ${config.creationDelay.toMillis} millisecond]\")\n        if (preWarmScheduler.isEmpty) {\n          preWarmScheduler = Some(\n            context.system.scheduler\n              .scheduleAtFixedRate(0.seconds, config.creationDelay, self, PrewarmContainer(config.maxConcurrent)))\n        }\n      })\n\n      if (scheduled) {\n        //   lastly, clear coldStartCounts each time scheduled event is processed to reset counts\n        coldStartCount = immutable.Map.empty[ColdStartKey, Int]\n      }\n    }\n  }\n\n  private def syncMemoryInfo: Unit = {\n    val busyMemory = memoryConsumptionOf(busyPool)\n    val inProgressMemory = memoryConsumptionOf(inProgressPool)\n    invokerHealthService ! MemoryInfo(\n      poolConfig.userMemory.toMB - busyMemory - inProgressMemory,\n      busyMemory,\n      inProgressMemory)\n  }\n\n  /** Creates a new container and updates state accordingly. */\n  private def createContainer(memoryLimit: ByteSize): (ActorRef, Data) = {\n    val ref = childFactory(context)\n    val data = MemoryData(memoryLimit)\n    ref -> data\n  }\n\n  /** Creates a new prewarmed container */\n  private def prewarmContainer(exec: CodeExec[_], memoryLimit: ByteSize, ttl: Option[FiniteDuration]): Unit = {\n    val newContainer = childFactory(context)\n    prewarmStartingPool = prewarmStartingPool + (newContainer -> (exec.kind, memoryLimit))\n    newContainer ! Start(exec, memoryLimit, ttl)\n  }\n\n  /** this is only for cold start statistics of prewarm configs, e.g. not blackbox or other configs. */\n  def incrementColdStartCount(kind: String, memoryLimit: ByteSize): Unit = {\n    prewarmConfig\n      .filter { config =>\n        kind == config.exec.kind && memoryLimit == config.memoryLimit\n      }\n      .foreach { _ =>\n        val coldStartKey = ColdStartKey(kind, memoryLimit)\n        coldStartCount.get(coldStartKey) match {\n          case Some(value) => coldStartCount = coldStartCount + (coldStartKey -> (value + 1))\n          case None        => coldStartCount = coldStartCount + (coldStartKey -> 1)\n        }\n      }\n  }\n\n  /**\n   * Takes a warmed container out of the warmed pool\n   * iff a container with a matching revision is found\n   *\n   * @param action the action.\n   * @param invocationNamespace the invocation namespace for shared package.\n   * @param revision the DocRevision.\n   * @return the container iff found\n   */\n  private def takeWarmedContainer(action: ExecutableWhiskAction,\n                                  invocationNamespace: String,\n                                  revision: DocRevision): Option[(ActorRef, Data)] = {\n    warmedPool\n      .find {\n        case (_, WarmData(_, `invocationNamespace`, `action`, `revision`, _, _)) => true\n        case _                                                                   => false\n      }\n      .map {\n        case (ref, data) =>\n          warmedPool = warmedPool - ref\n          logging.info(this, s\"Choose warmed container ${data.container.containerId}\")\n          (ref, data)\n      }\n  }\n\n  /**\n   * Takes a prewarm container out of the prewarmed pool\n   * iff a container with a matching kind and suitable memory is found.\n   *\n   * @param action the action that holds the kind and the required memory.\n   * @param maximumMemory the maximum memory container can have\n   * @return the container iff found\n   */\n  private def takePrewarmContainer(action: ExecutableWhiskAction, maximumMemory: ByteSize): Option[(ActorRef, Data)] = {\n    val kind = action.exec.kind\n    val memory = action.limits.memory.megabytes.MB\n\n    // find a container with same kind and smallest memory\n    prewarmedPool.filter {\n      case (_, PreWarmData(_, `kind`, preMemory, _)) if preMemory >= memory && preMemory <= maximumMemory => true\n      case _                                                                                              => false\n    }.toList match {\n      case Nil =>\n        None\n      case res =>\n        val (ref, data) = res.minBy(_._2.memoryLimit)\n        prewarmedPool = prewarmedPool - ref\n\n        //get the appropriate ttl from prewarm configs\n        val ttl =\n          prewarmConfig.find(pc => pc.memoryLimit == memory && pc.exec.kind == kind).flatMap(_.reactive.map(_.ttl))\n        prewarmContainer(action.exec, data.memoryLimit, ttl)\n        Some(ref, data)\n    }\n  }\n\n  /** Removes a container and updates state accordingly. */\n  def removeContainer(toDelete: ActorRef) = {\n    toDelete ! Remove\n    warmedPool = warmedPool - toDelete\n  }\n\n  /**\n   * Calculate if there is enough free memory within a given pool.\n   *\n   * @param pool The pool, that has to be checked, if there is enough free memory.\n   * @param memory The amount of memory to check.\n   * @return true, if there is enough space for the given amount of memory.\n   */\n  private def hasPoolSpaceFor[A](pool: Map[A, Data], memory: ByteSize): Boolean = {\n    memoryConsumptionOf(pool) + memory.toMB <= poolConfig.userMemory.toMB\n  }\n\n  /**\n   * Make all busyPool's memoryQueue actor shutdown gracefully\n   */\n  private def waitForPoolToClear(): Unit = {\n    val pool = self\n    // how many busy containers will be removed in this term\n    val slotsForBusyPool = math.max(poolConfig.batchDeletionSize - disablingPool.size, 0)\n    (busyPool.keySet &~ disablingPool)\n      .take(slotsForBusyPool)\n      .foreach(container => {\n        disablingPool += container\n        container ! GracefulShutdown\n      })\n    // how many warm containers will be removed in this term\n    val slotsForWarmPool = math.max(poolConfig.batchDeletionSize - disablingPool.size, 0)\n    (warmedPool.keySet &~ disablingPool)\n      .take(slotsForWarmPool)\n      .foreach(container => {\n        disablingPool += container\n        container ! GracefulShutdown\n      })\n    if (inProgressPool.nonEmpty || busyPool.size + warmedPool.size > slotsForBusyPool + slotsForWarmPool) {\n      context.system.scheduler.scheduleOnce(5.seconds) {\n        pool ! GracefulShutdown\n      }\n    }\n  }\n\n  /**\n   * take a prewarmed container or create a new one\n   *\n   * @param executable The executable whisk action\n   * @return\n   */\n  private def takeContainer(executable: ExecutableWhiskAction) = {\n    val freeMemory = poolConfig.userMemory.toMB - memoryConsumptionOf(busyPool ++ warmedPool ++ inProgressPool)\n    val deletableMemory = memoryConsumptionOf(warmedPool)\n    val requiredMemory = executable.limits.memory.megabytes\n    if (requiredMemory > freeMemory + deletableMemory) {\n      None\n    } else {\n      // try to take a preWarmed container whose memory doesn't exceed the max `usable` memory\n      takePrewarmContainer(\n        executable,\n        if (poolConfig.prewarmPromotion) (freeMemory + deletableMemory).MB else requiredMemory.MB) match {\n        // there is a suitable preWarmed container but not enough free memory for it, delete some warmed container first\n        case Some(container) if container._2.memoryLimit > freeMemory.MB =>\n          ContainerPoolV2\n            .remove(warmedPool, container._2.memoryLimit - freeMemory.MB)\n            .map(removeContainer)\n            .headOption\n            .map { _ =>\n              (container, \"prewarmed\")\n            }\n        // there is a suitable preWarmed container and enough free memory for it\n        case Some(container) =>\n          Some((container, \"prewarmed\"))\n        // there is no suitable preWarmed container and not enough free memory for the action\n        case None if executable.limits.memory.megabytes > freeMemory =>\n          ContainerPoolV2\n            .remove(warmedPool, executable.limits.memory.megabytes.MB - freeMemory.MB)\n            .map(removeContainer)\n            .headOption\n            .map { _ =>\n              incrementColdStartCount(executable.exec.kind, executable.limits.memory.megabytes.MB)\n              (createContainer(executable.limits.memory.megabytes.MB), \"cold\")\n            }\n        // there is no suitable preWarmed container and enough free memory\n        case None =>\n          incrementColdStartCount(executable.exec.kind, executable.limits.memory.megabytes.MB)\n          Some(createContainer(executable.limits.memory.megabytes.MB), \"cold\")\n\n        // this should not happen, but just for safety\n        case _ =>\n          None\n      }\n    }\n  }\n\n  private def handleChosenContainer(create: ContainerCreationMessage,\n                                    executable: ExecutableWhiskAction,\n                                    container: Option[((ActorRef, Data), String)]) = {\n    container match {\n      case Some(((proxy, data), containerState)) =>\n        // record creationMessage so when container created failed, we can send failed message to scheduler\n        creationMessages.getOrElseUpdate(proxy, create)\n        proxy ! Initialize(\n          create.invocationNamespace,\n          create.action,\n          executable,\n          create.schedulerHost,\n          create.rpcPort,\n          create.transid)\n        inProgressPool = inProgressPool + (proxy -> data)\n        logContainerStart(create, executable.toWhiskAction, containerState)\n\n      case None =>\n        val message =\n          s\"creationId: ${create.creationId}, invoker[$instance] doesn't have enough resource for container: ${create.action}\"\n        logging.info(this, message)\n        syncMemoryInfo\n        val ack = ContainerCreationAckMessage(\n          create.transid,\n          create.creationId,\n          create.invocationNamespace,\n          create.action,\n          create.revision,\n          create.whiskActionMetaData,\n          instance,\n          create.schedulerHost,\n          create.rpcPort,\n          create.retryCount,\n          Some(ResourceNotEnoughError),\n          Some(message))\n        sendAckToScheduler(create.rootSchedulerIndex, ack)\n    }\n  }\n}\n\nobject ContainerPoolV2 {\n\n  /**\n   * Calculate the memory of a given pool.\n   *\n   * @param pool The pool with the containers.\n   * @return The memory consumption of all containers in the pool in Megabytes.\n   */\n  protected[containerpool] def memoryConsumptionOf[A](pool: Map[A, Data]): Long = {\n    pool.map(_._2.memoryLimit.toMB).sum\n  }\n\n  /**\n   * Finds the oldest previously used container to remove to make space for the job passed to run.\n   * Depending on the space that has to be allocated, several containers might be removed.\n   *\n   * NOTE: This method is never called to remove an action that is in the pool already,\n   * since this would be picked up earlier in the scheduler and the container reused.\n   *\n   * @param pool a map of all free containers in the pool\n   * @param memory the amount of memory that has to be freed up\n   * @return a list of containers to be removed iff found\n   */\n  @tailrec\n  protected[containerpool] def remove[A](pool: Map[A, WarmData],\n                                         memory: ByteSize,\n                                         toRemove: List[A] = List.empty): List[A] = {\n    if (memory > 0.B && pool.nonEmpty && memoryConsumptionOf(pool) >= memory.toMB) {\n      // Remove the oldest container if:\n      // - there is more memory required\n      // - there are still containers that can be removed\n      // - there are enough free containers that can be removed\n      val (ref, data) = pool.minBy(_._2.lastUsed)\n      // Catch exception if remaining memory will be negative\n      val remainingMemory = Try(memory - data.memoryLimit).getOrElse(0.B)\n      remove(pool - ref, remainingMemory, toRemove ++ List(ref))\n    } else {\n      // If this is the first call: All containers are in use currently, or there is more memory needed than\n      // containers can be removed.\n      // Or, if this is one of the recursions: Enough containers are found to get the memory, that is\n      // necessary. -> Abort recursion\n      toRemove\n    }\n  }\n\n  /**\n   * Find the expired actor in prewarmedPool\n   *\n   * @param poolConfig\n   * @param prewarmConfig\n   * @param prewarmedPool\n   * @param logging\n   * @return a list of expired actor\n   */\n  def removeExpired[A](poolConfig: ContainerPoolConfig,\n                       prewarmConfig: List[PrewarmingConfig],\n                       prewarmedPool: Map[A, PreWarmData])(implicit logging: Logging): List[A] = {\n    val now = Deadline.now\n    val expireds = prewarmConfig\n      .flatMap { config =>\n        val kind = config.exec.kind\n        val memory = config.memoryLimit\n        config.reactive\n          .map { c =>\n            val expiredPrewarmedContainer = prewarmedPool.toSeq\n              .filter { warmInfo =>\n                warmInfo match {\n                  case (_, p @ PreWarmData(_, `kind`, `memory`, _)) if p.isExpired() => true\n                  case _                                                             => false\n                }\n              }\n              .sortBy(_._2.expires.getOrElse(now))\n\n            // emit expired container counter metric with memory + kind\n            MetricEmitter.emitCounterMetric(LoggingMarkers.CONTAINER_POOL_PREWARM_EXPIRED(memory.toString, kind))\n            if (expiredPrewarmedContainer.nonEmpty) {\n              logging.info(\n                this,\n                s\"[kind: ${kind} memory: ${memory.toString}] ${expiredPrewarmedContainer.size} expired prewarmed containers\")\n            }\n            expiredPrewarmedContainer.map(e => (e._1, e._2.expires.getOrElse(now)))\n          }\n          .getOrElse(List.empty)\n      }\n      .sortBy(_._2) //need to sort these so that if the results are limited, we take the oldest\n      .map(_._1)\n    if (expireds.nonEmpty) {\n      logging.info(this, s\"removing up to ${poolConfig.prewarmExpirationLimit} of ${expireds.size} expired containers\")\n      expireds.take(poolConfig.prewarmExpirationLimit).foreach { e =>\n        prewarmedPool.get(e).map { d =>\n          logging.info(this, s\"removing expired prewarm of kind ${d.kind} with container ${d.container} \")\n        }\n      }\n    }\n    expireds.take(poolConfig.prewarmExpirationLimit)\n  }\n\n  /**\n   * Find the increased number for the prewarmed kind\n   *\n   * @param init\n   * @param scheduled\n   * @param coldStartCount\n   * @param prewarmConfig\n   * @param prewarmedPool\n   * @param prewarmStartingPool\n   * @param logging\n   * @return the current number and increased number for the kind in the Map\n   */\n  def increasePrewarms(init: Boolean,\n                       scheduled: Boolean,\n                       coldStartCount: Map[ColdStartKey, Int],\n                       prewarmConfig: List[PrewarmingConfig],\n                       prewarmedPool: Map[ActorRef, PreWarmData],\n                       prewarmStartingPool: Map[ActorRef, (String, ByteSize)],\n                       prewarmQueue: Queue[(CodeExec[_], ByteSize, Option[FiniteDuration])])(\n    implicit logging: Logging): Map[PrewarmingConfig, (Int, Int)] = {\n    prewarmConfig.map { config =>\n      val kind = config.exec.kind\n      val memory = config.memoryLimit\n\n      val runningCount = prewarmedPool.count {\n        // done starting (include expired, since they may not have been removed yet)\n        case (_, p @ PreWarmData(_, `kind`, `memory`, _)) => true\n        // started but not finished starting (or expired)\n        case _ => false\n      }\n      val startingCount = prewarmStartingPool.count(p => p._2._1 == kind && p._2._2 == memory)\n      val queuingCount = prewarmQueue.count(p => p._1.kind == kind && p._2 == memory)\n      val currentCount = runningCount + startingCount + queuingCount\n\n      // determine how many are needed\n      val desiredCount: Int =\n        if (init) config.initialCount\n        else {\n          if (scheduled) {\n            // scheduled/reactive config backfill\n            config.reactive\n              .map(c => ContainerPool.getReactiveCold(coldStartCount, c, kind, memory).getOrElse(c.minCount)) //reactive -> desired is either cold start driven, or minCount\n              .getOrElse(config.initialCount) //not reactive -> desired is always initial count\n          } else {\n            // normal backfill after removal - make sure at least minCount or initialCount is started\n            config.reactive.map(_.minCount).getOrElse(config.initialCount)\n          }\n        }\n\n      if (currentCount < desiredCount) {\n        logging.info(\n          this,\n          s\"found ${currentCount} started and ${startingCount} starting and ${queuingCount} queuing; ${if (init) \"initing\"\n          else \"backfilling\"} ${desiredCount - currentCount} pre-warms to desired count: ${desiredCount} for kind:${config.exec.kind} mem:${config.memoryLimit.toString}\")(\n          TransactionId.invokerWarmup)\n      }\n      (config, (currentCount, desiredCount))\n    }.toMap\n  }\n\n  def props(factory: ActorRefFactory => ActorRef,\n            invokerHealthService: ActorRef,\n            poolConfig: ContainerPoolConfig,\n            instance: InvokerInstanceId,\n            prewarmConfig: List[PrewarmingConfig] = List.empty,\n            sendAckToScheduler: (SchedulerInstanceId, ContainerCreationAckMessage) => Future[ResultMetadata])(\n    implicit logging: Logging): Props = {\n    Props(\n      new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService,\n        poolConfig,\n        instance,\n        prewarmConfig,\n        sendAckToScheduler))\n  }\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/v2/FunctionPullingContainerProxy.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.v2\n\nimport java.net.InetSocketAddress\nimport java.time.Instant\nimport org.apache.pekko.actor.Status.{Failure => FailureMessage}\nimport org.apache.pekko.actor.{ActorRef, ActorRefFactory, ActorSystem, FSM, Props, Stash}\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.pekko.io.{IO, Tcp}\nimport org.apache.pekko.pattern.pipe\nimport org.apache.openwhisk.common.tracing.WhiskTracerProvider\nimport org.apache.openwhisk.common.{LoggingMarkers, TransactionId, _}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.ack.ActiveAck\nimport org.apache.openwhisk.core.connector.{\n  ActivationMessage,\n  CombinedCompletionAndResultMessage,\n  CompletionMessage,\n  ResultMessage\n}\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.containerpool.logging.LogCollectingException\nimport org.apache.openwhisk.core.containerpool.v2.FunctionPullingContainerProxy.{\n  constructWhiskActivation,\n  containerName\n}\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.{ExecutableWhiskAction, ActivationResponse => ExecutionResponse, _}\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys\nimport org.apache.openwhisk.core.invoker.Invoker.LogsCollector\nimport org.apache.openwhisk.core.invoker.NamespaceBlacklist\nimport org.apache.openwhisk.core.scheduler.SchedulerEndpoints\nimport org.apache.openwhisk.core.service.{RegisterData, UnregisterData}\nimport org.apache.openwhisk.grpc.RescheduleResponse\nimport org.apache.openwhisk.http.Messages\nimport pureconfig.loadConfigOrThrow\nimport spray.json.DefaultJsonProtocol.{StringJsonFormat, _}\nimport spray.json._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.Future\nimport scala.util.{Failure, Success, Try}\n\n// Events used internally\ncase class RunActivation(action: ExecutableWhiskAction, msg: ActivationMessage)\ncase class RunActivationCompleted(container: Container, action: ExecutableWhiskAction, duration: Option[Long])\ncase class InitCodeCompleted(data: WarmData)\n\n// Events received by the actor\ncase class Initialize(invocationNamespace: String,\n                      fqn: FullyQualifiedEntityName,\n                      action: ExecutableWhiskAction,\n                      schedulerHost: String,\n                      rpcPort: Int,\n                      transId: TransactionId)\ncase class Start(exec: CodeExec[_], memoryLimit: ByteSize, ttl: Option[FiniteDuration] = None)\n\n// Event sent by the actor\ncase class ContainerCreationFailed(throwable: Throwable)\ncase class ContainerIsPaused(data: WarmData)\ncase class ClientCreationFailed(throwable: Throwable,\n                                container: Container,\n                                invocationNamespace: String,\n                                action: ExecutableWhiskAction)\ncase class ReadyToWork(data: PreWarmData)\ncase class Initialized(data: InitializedData)\ncase class Resumed(data: WarmData)\ncase class ResumeFailed(data: WarmData)\ncase class RecreateClient(action: ExecutableWhiskAction)\ncase object PingCache\ncase class DetermineKeepContainer(attempt: Int)\n\n// States\nsealed trait ProxyState\ncase object LeaseStart extends ProxyState\ncase object Uninitialized extends ProxyState\ncase object CreatingContainer extends ProxyState\ncase object ContainerCreated extends ProxyState\ncase object CreatingClient extends ProxyState\ncase object ClientCreated extends ProxyState\ncase object Running extends ProxyState\ncase object Pausing extends ProxyState\ncase object Paused extends ProxyState\ncase object Removing extends ProxyState\ncase object Rescheduling extends ProxyState\n\n// Errors\ncase class ContainerHealthErrorWithResumedRun(tid: TransactionId, msg: String, resumeRun: RunActivation)\n    extends Exception(msg)\n\n// Data\nsealed abstract class Data(val memoryLimit: ByteSize) {\n  def getContainer: Option[Container]\n}\ncase class NonexistentData() extends Data(0.B) {\n  override def getContainer = None\n}\ncase class MemoryData(override val memoryLimit: ByteSize) extends Data(memoryLimit) {\n  override def getContainer = None\n}\ntrait WithClient { val clientProxy: ActorRef }\ncase class PreWarmData(container: Container,\n                       kind: String,\n                       override val memoryLimit: ByteSize,\n                       expires: Option[Deadline] = None)\n    extends Data(memoryLimit) {\n  override def getContainer = Some(container)\n  def isExpired(): Boolean = expires.exists(_.isOverdue())\n}\n\nobject BasicContainerInfo extends DefaultJsonProtocol {\n  implicit val prewarmedPoolSerdes = jsonFormat4(BasicContainerInfo.apply)\n}\n\nsealed case class BasicContainerInfo(containerId: String, namespace: String, action: String, kind: String)\n\nsealed abstract class ContainerAvailableData(container: Container,\n                                             invocationNamespace: String,\n                                             action: ExecutableWhiskAction)\n    extends Data(action.limits.memory.megabytes.MB) {\n  override def getContainer = Some(container)\n\n  val basicContainerInfo =\n    BasicContainerInfo(container.containerId.asString, invocationNamespace, action.name.asString, action.exec.kind)\n}\n\ncase class ContainerCreatedData(container: Container, invocationNamespace: String, action: ExecutableWhiskAction)\n    extends ContainerAvailableData(container, invocationNamespace, action)\n\ncase class InitializedData(container: Container,\n                           invocationNamespace: String,\n                           action: ExecutableWhiskAction,\n                           override val clientProxy: ActorRef)\n    extends ContainerAvailableData(container, invocationNamespace, action)\n    with WithClient {\n  override def getContainer = Some(container)\n  def toReschedulingData(resumeRun: RunActivation) =\n    ReschedulingData(container, invocationNamespace, action, clientProxy, resumeRun)\n}\n\ncase class WarmData(container: Container,\n                    invocationNamespace: String,\n                    action: ExecutableWhiskAction,\n                    revision: DocRevision,\n                    lastUsed: Instant,\n                    override val clientProxy: ActorRef)\n    extends ContainerAvailableData(container, invocationNamespace, action)\n    with WithClient {\n  override def getContainer = Some(container)\n  def toReschedulingData(resumeRun: RunActivation) =\n    ReschedulingData(container, invocationNamespace, action, clientProxy, resumeRun)\n}\n\ncase class ReschedulingData(container: Container,\n                            invocationNamespace: String,\n                            action: ExecutableWhiskAction,\n                            clientProxy: ActorRef,\n                            resumeRun: RunActivation)\n    extends ContainerAvailableData(container, invocationNamespace, action)\n    with WithClient\n\nclass FunctionPullingContainerProxy(\n  factory: (TransactionId,\n            String,\n            ImageName,\n            Boolean,\n            ByteSize,\n            Int,\n            Option[Double],\n            Option[ExecutableWhiskAction]) => Future[Container],\n  entityStore: ArtifactStore[WhiskEntity],\n  namespaceBlacklist: NamespaceBlacklist,\n  get: (ArtifactStore[WhiskEntity], DocId, DocRevision, Boolean, Boolean) => Future[WhiskAction],\n  dataManagementService: ActorRef,\n  clientProxyFactory: (ActorRefFactory,\n                       String,\n                       FullyQualifiedEntityName,\n                       DocRevision,\n                       String,\n                       Int,\n                       ContainerId) => ActorRef,\n  sendActiveAck: ActiveAck,\n  storeActivation: (TransactionId, WhiskActivation, Boolean, UserContext) => Future[Any],\n  collectLogs: LogsCollector,\n  getLiveContainerCount: (String, FullyQualifiedEntityName, DocRevision) => Future[Long],\n  getWarmedContainerLimit: (String) => Future[(Int, FiniteDuration)],\n  instance: InvokerInstanceId,\n  invokerHealthManager: ActorRef,\n  poolConfig: ContainerPoolConfig,\n  timeoutConfig: ContainerProxyTimeoutConfig,\n  healtCheckConfig: ContainerProxyHealthCheckConfig,\n  testTcp: Option[ActorRef])(implicit actorSystem: ActorSystem, logging: Logging)\n    extends FSM[ProxyState, Data]\n    with Stash {\n  startWith(Uninitialized, NonexistentData())\n\n  implicit val ec = actorSystem.dispatcher\n\n  private val UnusedTimeoutName = \"UnusedTimeout\"\n  private val unusedTimeout = timeoutConfig.pauseGrace\n  private val IdleTimeoutName = \"PausingTimeout\"\n  private val idleTimeout = timeoutConfig.idleContainer\n  private val KeepingTimeoutName = \"KeepingTimeout\"\n  private val RunningActivationTimeoutName = \"RunningActivationTimeout\"\n  private val runningActivationTimeout = 10.seconds\n  private val PingCacheName = \"PingCache\"\n  private val pingCacheInterval = 1.minute\n  private var timedOut = false\n\n  var healthPingActor: Option[ActorRef] = None //setup after prewarm starts\n  val tcp: ActorRef = testTcp.getOrElse(IO(Tcp)) //allows to testing interaction with Tcp extension\n\n  val runningActivations = new java.util.concurrent.ConcurrentHashMap[String, Boolean]\n\n  when(Uninitialized) {\n    // pre warm a container (creates a stem cell container)\n    case Event(job: Start, _) =>\n      factory(\n        TransactionId.invokerWarmup,\n        containerName(instance, \"prewarm\", job.exec.kind),\n        job.exec.image,\n        job.exec.pull,\n        job.memoryLimit,\n        poolConfig.cpuShare(job.memoryLimit),\n        poolConfig.cpuLimit(job.memoryLimit),\n        None)\n        .map(container => PreWarmData(container, job.exec.kind, job.memoryLimit, expires = job.ttl.map(_.fromNow)))\n        .pipeTo(self)\n      goto(CreatingContainer)\n\n    // cold start\n    case Event(job: Initialize, _) =>\n      factory( // create a new container\n        TransactionId.invokerColdstart,\n        containerName(instance, job.action.namespace.namespace, job.action.name.asString),\n        job.action.exec.image,\n        job.action.exec.pull,\n        job.action.limits.memory.megabytes.MB,\n        poolConfig.cpuShare(job.action.limits.memory.megabytes.MB),\n        poolConfig.cpuLimit(job.action.limits.memory.megabytes.MB),\n        None)\n        .andThen {\n          case Failure(t) =>\n            context.parent ! ContainerCreationFailed(t)\n        }\n        .map { container =>\n          logging.debug(this, s\"a container ${container.containerId} is created for ${job.action}\")\n          // create a client\n          Try(\n            clientProxyFactory(\n              context,\n              job.invocationNamespace,\n              job.fqn, // include binding field\n              job.action.rev,\n              job.schedulerHost,\n              job.rpcPort,\n              container.containerId)) match {\n            case Success(clientProxy) =>\n              InitializedData(container, job.invocationNamespace, job.action, clientProxy)\n            case Failure(t) =>\n              logging.error(this, s\"failed to create activation client caused by: $t\")\n              ClientCreationFailed(t, container, job.invocationNamespace, job.action)\n          }\n        }\n        .pipeTo(self)\n\n      goto(CreatingClient)\n\n    case _ => delay\n  }\n\n  when(CreatingContainer) {\n    // container was successfully obtained\n    case Event(completed: PreWarmData, _: NonexistentData) =>\n      context.parent ! ReadyToWork(completed)\n      goto(ContainerCreated) using completed\n\n    // container creation failed\n    case Event(t: FailureMessage, _: NonexistentData) =>\n      context.parent ! ContainerRemoved(true)\n      stop()\n\n    case _ => delay\n  }\n\n  // prewarmed state, container created\n  when(ContainerCreated) {\n    case Event(job: Initialize, data: PreWarmData) =>\n      val res = Try(\n        clientProxyFactory(\n          context,\n          job.invocationNamespace,\n          job.fqn, // include binding field\n          job.action.rev,\n          job.schedulerHost,\n          job.rpcPort,\n          data.container.containerId)) match {\n        case Success(proxy) =>\n          InitializedData(data.container, job.invocationNamespace, job.action, proxy)\n        case Failure(t) =>\n          logging.error(this, s\"failed to create activation client for ${job.action} caused by: $t\")\n          ClientCreationFailed(t, data.container, job.invocationNamespace, job.action)\n      }\n\n      self ! res\n\n      goto(CreatingClient)\n\n    case Event(Remove, data: PreWarmData) =>\n      cleanUp(data.container, None, false)\n\n    // prewarm container failed by health check\n    case Event(_: FailureMessage, data: PreWarmData) =>\n      MetricEmitter.emitCounterMetric(LoggingMarkers.INVOKER_CONTAINER_HEALTH_FAILED_PREWARM)\n      cleanUp(data.container, None)\n\n    case _ => delay\n  }\n\n  when(CreatingClient) {\n    // wait for client creation when cold start\n    case Event(job: InitializedData, _) =>\n      job.clientProxy ! StartClient\n\n      stay() using job\n\n    // client was successfully obtained\n    case Event(ClientCreationCompleted, data: InitializedData) =>\n      val fqn = data.action.fullyQualifiedName(true)\n      val revision = data.action.rev\n      dataManagementService ! RegisterData(\n        s\"${ContainerKeys.existingContainers(data.invocationNamespace, fqn, revision, Some(instance), Some(data.container.containerId))}\",\n        \"\")\n      self ! data\n      goto(ClientCreated)\n\n    // client creation failed\n    case Event(t: ClientCreationFailed, _) =>\n      invokerHealthManager ! HealthMessage(state = false)\n      cleanUp(t.container, t.invocationNamespace, t.action.fullyQualifiedName(withVersion = true), t.action.rev, None)\n\n    case Event(ClientClosed, data: InitializedData) =>\n      invokerHealthManager ! HealthMessage(state = false)\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        None)\n\n    // container creation failed when cold start\n    case Event(_: FailureMessage, _) =>\n      context.parent ! ContainerRemoved(true)\n      stop()\n\n    case _ => delay\n  }\n\n  // this is for first invocation, once the first invocation is over we are ready to trigger getActivation for action concurrency\n  when(ClientCreated) {\n    // 1. request activation message to client\n    case Event(initializedData: InitializedData, _) =>\n      context.parent ! Initialized(initializedData)\n      initializedData.clientProxy ! RequestActivation()\n      startTimerWithFixedDelay(PingCacheName, PingCache, pingCacheInterval)\n      startSingleTimer(UnusedTimeoutName, StateTimeout, unusedTimeout)\n      stay() using initializedData\n\n    // 2. read executable action data from db\n    case Event(job: ActivationMessage, data: InitializedData) =>\n      timedOut = false\n      cancelTimer(UnusedTimeoutName)\n      handleActivationMessage(job, data.action)\n        .pipeTo(self)\n      stay() using data\n\n    // 3. request initialize and run command to container\n    case Event(job: RunActivation, data: InitializedData) =>\n      implicit val transid = job.msg.transid\n      logging.debug(this, s\"received RunActivation ${job.msg.activationId} for ${job.action} in $stateName\")\n\n      initializeAndRunActivation(data.container, data.clientProxy, job.action, job.msg, Some(job))\n        .map { activation =>\n          RunActivationCompleted(data.container, job.action, activation.duration)\n        }\n        .pipeTo(self)\n\n      // when it receives InitCodeCompleted, it will move to Running\n      stay using data\n\n    case Event(RetryRequestActivation, data: InitializedData) =>\n      // if this Container is marked with time out, do not retry\n      if (timedOut)\n        cleanUp(\n          data.container,\n          data.invocationNamespace,\n          data.action.fullyQualifiedName(withVersion = true),\n          data.action.rev,\n          Some(data.clientProxy))\n      else {\n        data.clientProxy ! RequestActivation()\n        stay()\n      }\n\n    // code initialization was successful\n    case Event(completed: InitCodeCompleted, data: InitializedData) =>\n      // TODO support concurrency?\n      data.clientProxy ! ContainerWarmed // this container is warmed\n      1 until completed.data.action.limits.concurrency.maxConcurrent foreach { _ =>\n        data.clientProxy ! RequestActivation()\n      }\n\n      goto(Running) using completed.data // set warm data\n\n    // ContainerHealthError should cause\n    case Event(FailureMessage(e: ContainerHealthErrorWithResumedRun), data: InitializedData) =>\n      logging.error(\n        this,\n        s\"container ${data.container.containerId.asString} health check failed on $stateName, ${e.resumeRun.msg.activationId} activation will be rescheduled\")\n      MetricEmitter.emitCounterMetric(LoggingMarkers.INVOKER_CONTAINER_HEALTH_FAILED_WARM)\n\n      // reschedule message\n      data.clientProxy ! RescheduleActivation(\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        e.resumeRun.msg)\n\n      goto(Rescheduling) using data.toReschedulingData(e.resumeRun)\n\n    // Failed to get activation or execute the action\n    case Event(t: FailureMessage, data: InitializedData) =>\n      logging.error(\n        this,\n        s\"failed to initialize a container or run an activation for ${data.action} in state: $stateName caused by: $t\")\n      // Stop containerProxy and ActivationClientProxy both immediately\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        Some(data.clientProxy))\n\n    case Event(StateTimeout, data: InitializedData) =>\n      logging.info(this, s\"No more activation is coming in state: $stateName, action: ${data.action}\")\n      // Just mark the ContainerProxy is timedout\n      timedOut = true\n\n      stay() // stay here because the ActivationClientProxy may send a new Activation message\n\n    case Event(ClientClosed, data: InitializedData) =>\n      logging.error(this, s\"The Client closed in state: $stateName, action: ${data.action}\")\n      // Stop ContainerProxy(ActivationClientProxy will stop also when send ClientClosed to ContainerProxy).\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        None)\n\n    case x: Event if x.event != PingCache => delay\n  }\n\n  when(Rescheduling, stateTimeout = 10.seconds) {\n\n    case Event(res: RescheduleResponse, data: ReschedulingData) =>\n      implicit val transId = data.resumeRun.msg.transid\n      if (!res.isRescheduled) {\n        logging.warn(this, s\"failed to reschedule the message ${data.resumeRun.msg.activationId}, clean up data\")\n        fallbackActivationForReschedulingData(data)\n      } else {\n        logging.warn(this, s\"unhandled message is rescheduled, clean up data\")\n      }\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        Some(data.clientProxy))\n\n    case Event(StateTimeout, data: ReschedulingData) =>\n      logging.error(this, s\"Timeout for rescheduling message ${data.resumeRun.msg.activationId}, clean up data\")(\n        data.resumeRun.msg.transid)\n\n      fallbackActivationForReschedulingData(data)\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        Some(data.clientProxy))\n\n    case x: Event if x.event != PingCache => delay\n  }\n\n  when(Running) {\n    // Run was successful.\n    // 1. request activation message to client\n    case Event(activationResult: RunActivationCompleted, data: WarmData) =>\n      // create timeout\n      startSingleTimer(UnusedTimeoutName, StateTimeout, unusedTimeout)\n      data.clientProxy ! RequestActivation(activationResult.duration)\n      stay() using data\n\n    // 2. read executable action data from db\n    case Event(job: ActivationMessage, data: WarmData) =>\n      timedOut = false\n      cancelTimer(UnusedTimeoutName)\n      handleActivationMessage(job, data.action)\n        .pipeTo(self)\n      stay() using data\n\n    // 3. request run command to container\n    case Event(job: RunActivation, data: WarmData) =>\n      logging.debug(this, s\"received RunActivation ${job.msg.activationId} for ${job.action} in $stateName\")\n      implicit val transid = job.msg.transid\n\n      initializeAndRunActivation(data.container, data.clientProxy, job.action, job.msg, Some(job))\n        .map { activation =>\n          RunActivationCompleted(data.container, job.action, activation.duration)\n        }\n        .pipeTo(self)\n      stay using data.copy(lastUsed = Instant.now)\n\n    case Event(RetryRequestActivation, data: WarmData) =>\n      // if this Container is marked with time out, do not retry\n      if (timedOut) {\n        data.container.suspend()(TransactionId.invokerNanny).map(_ => ContainerPaused).pipeTo(self)\n        goto(Pausing)\n      } else {\n        data.clientProxy ! RequestActivation()\n        stay()\n      }\n\n    case Event(_: ResumeFailed, data: WarmData) =>\n      invokerHealthManager ! HealthMessage(state = false)\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        Some(data.clientProxy))\n\n    // ContainerHealthError should cause\n    case Event(FailureMessage(e: ContainerHealthError), data: WarmData) =>\n      logging.error(this, s\"health check failed on $stateName caused by: ContainerHealthError $e\")\n      MetricEmitter.emitCounterMetric(LoggingMarkers.INVOKER_CONTAINER_HEALTH_FAILED_WARM)\n      // Stop containerProxy and ActivationClientProxy both immediately,\n      invokerHealthManager ! HealthMessage(state = false)\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        Some(data.clientProxy))\n\n    // ContainerHealthError should cause\n    case Event(FailureMessage(e: ContainerHealthErrorWithResumedRun), data: WarmData) =>\n      logging.error(\n        this,\n        s\"container ${data.container.containerId.asString} health check failed on $stateName, ${e.resumeRun.msg.activationId} activation will be rescheduled\")\n      MetricEmitter.emitCounterMetric(LoggingMarkers.INVOKER_CONTAINER_HEALTH_FAILED_WARM)\n\n      // reschedule message\n      data.clientProxy ! RescheduleActivation(\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        e.resumeRun.msg)\n\n      goto(Rescheduling) using data.toReschedulingData(e.resumeRun)\n\n    // Failed to get activation or execute the action\n    case Event(t: FailureMessage, data: WarmData) =>\n      logging.error(this, s\"failed to init or run in state: $stateName caused by: $t\")\n      // Stop containerProxy and ActivationClientProxy both immediately,\n      // and don't send unhealthy state message to the health manager, it's already sent.\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(withVersion = true),\n        data.action.rev,\n        Some(data.clientProxy))\n\n    case Event(StateTimeout, data: WarmData) =>\n      logging.info(\n        this,\n        s\"No more run activation is coming in state: $stateName, action: ${data.action}, container: ${data.container.containerId}\")\n      // Just mark the ContainerProxy is timedout\n      timedOut = true\n\n      stay() // stay here because the ActivationClientProxy may send a new Activation message\n\n    case Event(ClientClosed, data: WarmData) =>\n      if (runningActivations.isEmpty) {\n        logging.info(this, s\"The Client closed in state: $stateName, action: ${data.action}\")\n        // Stop ContainerProxy(ActivationClientProxy will stop also when send ClientClosed to ContainerProxy).\n        cleanUp(\n          data.container,\n          data.invocationNamespace,\n          data.action.fullyQualifiedName(withVersion = true),\n          data.action.rev,\n          None)\n      } else {\n        logging.info(\n          this,\n          s\"Remain running activations ${runningActivations.keySet().toString()} when received ClientClosed\")\n        startSingleTimer(RunningActivationTimeoutName, ClientClosed, runningActivationTimeout)\n        stay\n      }\n\n    // shutdown the client first and wait for any remaining activation to be executed\n    // ContainerProxy will be terminated by StateTimeout if there is no further activation\n    case Event(GracefulShutdown, data: WarmData) =>\n      logging.info(this, s\"receive GracefulShutdown for action: ${data.action}\")\n      // clean up the etcd data first so that the scheduler can provision more containers in advance.\n      dataManagementService ! UnregisterData(\n        ContainerKeys.existingContainers(\n          data.invocationNamespace,\n          data.action.fullyQualifiedName(true),\n          data.action.rev,\n          Some(instance),\n          Some(data.container.containerId)))\n\n      // Just send GracefulShutdown to ActivationClientProxy, make ActivationClientProxy throw ClientClosedException when fetchActivation next time.\n      data.clientProxy ! GracefulShutdown\n      stay\n\n    case x: Event if x.event != PingCache => delay\n  }\n\n  when(Pausing) {\n    case Event(ContainerPaused, data: WarmData) =>\n      dataManagementService ! RegisterData(\n        ContainerKeys.warmedContainers(\n          data.invocationNamespace,\n          data.action.fullyQualifiedName(false),\n          data.revision,\n          instance,\n          data.container.containerId),\n        \"\")\n      // remove existing key so MemoryQueue can be terminated when timeout\n      dataManagementService ! UnregisterData(\n        s\"${ContainerKeys.existingContainers(data.invocationNamespace, data.action.fullyQualifiedName(true), data.action.rev, Some(instance), Some(data.container.containerId))}\")\n      context.parent ! ContainerIsPaused(data)\n      goto(Paused)\n\n    case Event(_: FailureMessage, data: WarmData) =>\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(false),\n        data.action.rev,\n        Some(data.clientProxy))\n\n    case x: Event if x.event != PingCache => delay\n  }\n\n  when(Paused) {\n    case Event(job: Initialize, data: WarmData) =>\n      implicit val transId = job.transId\n      val parent = context.parent\n      cancelTimer(IdleTimeoutName)\n      cancelTimer(KeepingTimeoutName)\n      cancelTimer(DetermineKeepContainer.toString)\n      data.container\n        .resume()\n        .map { _ =>\n          logging.info(this, s\"Resumed container ${data.container.containerId}\")\n          // put existing key again\n          dataManagementService ! RegisterData(\n            s\"${ContainerKeys.existingContainers(data.invocationNamespace, data.action.fullyQualifiedName(true), data.action.rev, Some(instance), Some(data.container.containerId))}\",\n            \"\")\n          parent ! Resumed(data)\n          // the new queue may locates on an different scheduler, so recreate the activation client when necessary\n          // since pekko port will no be used, we can put any value except 0 here\n          data.clientProxy ! RequestActivation(\n            newScheduler = Some(SchedulerEndpoints(job.schedulerHost, job.rpcPort, 10)))\n          startSingleTimer(UnusedTimeoutName, StateTimeout, unusedTimeout)\n          timedOut = false\n        }\n        .recover {\n          case t: Throwable =>\n            logging.error(this, s\"Failed to resume container ${data.container.containerId}, error: $t\")\n            parent ! ResumeFailed(data)\n            self ! ResumeFailed(data)\n        }\n\n      // always clean data in etcd regardless of success and failure\n      dataManagementService ! UnregisterData(\n        ContainerKeys.warmedContainers(\n          data.invocationNamespace,\n          data.action.fullyQualifiedName(false),\n          data.revision,\n          instance,\n          data.container.containerId))\n      goto(Running)\n    case Event(StateTimeout, _: WarmData) =>\n      self ! DetermineKeepContainer(0)\n      stay\n    case Event(DetermineKeepContainer(attempt), data: WarmData) =>\n      getLiveContainerCount(data.invocationNamespace, data.action.fullyQualifiedName(false), data.revision)\n        .flatMap(count => {\n          getWarmedContainerLimit(data.invocationNamespace).map(warmedContainerInfo => {\n            logging.info(\n              this,\n              s\"Live container count: $count, warmed container keeping count configuration: ${warmedContainerInfo._1} in namespace: ${data.invocationNamespace}\")\n            if (count <= warmedContainerInfo._1) {\n              self ! Keep(warmedContainerInfo._2)\n            } else {\n              self ! Remove\n            }\n          })\n        })\n        .recover({\n          case t: Throwable =>\n            logging.error(\n              this,\n              s\"Failed to determine whether to keep or remove container on pause timeout for ${data.container.containerId}, retrying. Caused by: $t\")\n            if (attempt < 5) {\n              startSingleTimer(DetermineKeepContainer.toString, DetermineKeepContainer(attempt + 1), 500.milli)\n            } else {\n              self ! Remove\n            }\n        })\n      stay\n    case Event(Keep(warmedContainerKeepingTimeout), data: WarmData) =>\n      logging.info(\n        this,\n        s\"This is the remaining container for ${data.action}. The container will stop after $warmedContainerKeepingTimeout.\")\n      startSingleTimer(KeepingTimeoutName, Remove, warmedContainerKeepingTimeout)\n      stay\n    case Event(Remove | GracefulShutdown, data: WarmData) =>\n      cancelTimer(DetermineKeepContainer.toString)\n      dataManagementService ! UnregisterData(\n        ContainerKeys.warmedContainers(\n          data.invocationNamespace,\n          data.action.fullyQualifiedName(false),\n          data.revision,\n          instance,\n          data.container.containerId))\n      cleanUp(\n        data.container,\n        data.invocationNamespace,\n        data.action.fullyQualifiedName(false),\n        data.action.rev,\n        Some(data.clientProxy))\n\n    case x: Event if x.event != PingCache => delay\n  }\n\n  when(Removing, unusedTimeout) {\n    // only if ClientProxy is closed, ContainerProxy stops. So it is important for ClientProxy to send ClientClosed.\n    case Event(ClientClosed, _) =>\n      stop()\n\n    // even if any error occurs, it still waits for ClientClosed event in order to be stopped after the client is closed.\n    case Event(t: FailureMessage, _) =>\n      logging.error(this, s\"unable to delete a container due to ${t}\")\n\n      stay\n\n    case Event(StateTimeout, _) =>\n      logging.error(this, s\"could not receive ClientClosed for ${unusedTimeout}, so just stop the container proxy.\")\n\n      stop()\n\n    case Event(Remove | GracefulShutdown, _) =>\n      stay()\n\n    case Event(DetermineKeepContainer(_), _) =>\n      stay()\n  }\n\n  whenUnhandled {\n    case Event(PingCache, data: WarmData) =>\n      val actionId = data.action.fullyQualifiedName(false).toDocId.asDocInfo(data.revision)\n      get(entityStore, actionId.id, actionId.rev, true, false).map(_ => {\n        logging.debug(\n          this,\n          s\"Refreshed function cache for action ${data.action} from container ${data.container.containerId}.\")\n      })\n      stay\n    case Event(PingCache, _) =>\n      logging.debug(this, \"Container is not warm, ignore function cache ping.\")\n      stay\n  }\n\n  onTransition {\n    case _ -> Uninitialized     => unstashAll()\n    case _ -> CreatingContainer => unstashAll()\n    case _ -> ContainerCreated =>\n      if (healtCheckConfig.enabled) {\n        nextStateData.getContainer.foreach { c =>\n          logging.info(this, s\"enabling health ping for ${c.containerId.asString} on ContainerCreated\")\n          enableHealthPing(c)\n        }\n      }\n      unstashAll()\n    case _ -> CreatingClient => unstashAll()\n    case _ -> ClientCreated  => unstashAll()\n    case _ -> Running =>\n      if (healtCheckConfig.enabled && healthPingActor.isDefined) {\n        nextStateData.getContainer.foreach { c =>\n          logging.info(this, s\"disabling health ping for ${c.containerId.asString} on Running\")\n          disableHealthPing()\n        }\n      }\n      unstashAll()\n    case _ -> Paused   => startSingleTimer(IdleTimeoutName, StateTimeout, idleTimeout)\n    case _ -> Removing => unstashAll()\n  }\n\n  initialize()\n\n  /** Delays all incoming messages until unstashAll() is called */\n  def delay = {\n    stash()\n    stay\n  }\n\n  /**\n   * Only change the state if the currentState is not the newState.\n   *\n   * @param newState of the InvokerActor\n   */\n  private def gotoIfNotThere(newState: ProxyState) = {\n    if (stateName == newState) stay() else goto(newState)\n  }\n\n  /**\n   * Clean up all meta data of invoking action\n   *\n   * @param container the container to destroy\n   * @param fqn the action to stop\n   * @param clientProxy the client to destroy\n   * @return\n   */\n  private def cleanUp(container: Container,\n                      invocationNamespace: String,\n                      fqn: FullyQualifiedEntityName,\n                      revision: DocRevision,\n                      clientProxy: Option[ActorRef]): State = {\n    cancelTimer(PingCacheName)\n    dataManagementService ! UnregisterData(\n      s\"${ContainerKeys.existingContainers(invocationNamespace, fqn, revision, Some(instance), Some(container.containerId))}\")\n\n    cleanUp(container, clientProxy)\n  }\n\n  private def cleanUp(container: Container, clientProxy: Option[ActorRef], replacePrewarm: Boolean = true): State = {\n    context.parent ! ContainerRemoved(replacePrewarm)\n    val unpause = stateName match {\n      case Paused => container.resume()(TransactionId.invokerNanny)\n      case _      => Future.successful(())\n    }\n    unpause.andThen {\n      case Success(_) => destroyContainer(container)\n      case Failure(t) =>\n        // docker may hang when try to remove a paused container, so we shouldn't remove it\n        logging.error(this, s\"Failed to resume container ${container.containerId}, error: $t\")\n    }\n    clientProxy match {\n      case Some(clientProxy) => clientProxy ! StopClientProxy\n      case None              => self ! ClientClosed\n    }\n    gotoIfNotThere(Removing)\n  }\n\n  /**\n   * Destroys the container\n   *\n   * @param container the container to destroy\n   */\n  private def destroyContainer(container: Container) = {\n    container\n      .destroy()(TransactionId.invokerNanny)\n      .andThen {\n        case Failure(t) =>\n          logging.error(this, s\"Failed to destroy container: ${container.containerId.asString} caused by ${t}\")\n      }\n  }\n\n  private def handleActivationMessage(msg: ActivationMessage, action: ExecutableWhiskAction): Future[RunActivation] = {\n    implicit val transid = msg.transid\n    logging.info(this, s\"received a message ${msg.activationId} for ${msg.action} in $stateName\")\n    if (!namespaceBlacklist.isBlacklisted(msg.user)) {\n      logging.debug(this, s\"namespace ${msg.user.namespace.name} is not in the namespaceBlacklist\")\n      val namespace = msg.action.path\n      val name = msg.action.name\n      val actionid = FullyQualifiedEntityName(namespace, name).toDocId.asDocInfo(msg.revision)\n      val subject = msg.user.subject\n\n      logging.debug(this, s\"${actionid.id} $subject ${msg.activationId}\")\n\n      // set trace context to continue tracing\n      WhiskTracerProvider.tracer.setTraceContext(transid, msg.traceContext)\n\n      // caching is enabled since actions have revision id and an updated\n      // action will not hit in the cache due to change in the revision id;\n      // if the doc revision is missing, then bypass cache\n      if (actionid.rev == DocRevision.empty)\n        logging.warn(this, s\"revision was not provided for ${actionid.id}\")\n\n      get(entityStore, actionid.id, actionid.rev, actionid.rev != DocRevision.empty, false)\n        .flatMap { action =>\n          {\n            // action that exceed the limit cannot be executed\n            action.limits.checkLimits(msg.user)\n            action.toExecutableWhiskAction match {\n              case Some(executable) =>\n                Future.successful(RunActivation(executable, msg))\n              case None =>\n                logging\n                  .error(this, s\"non-executable action reached the invoker ${action.fullyQualifiedName(false)}\")\n                Future.failed(new IllegalStateException(\"non-executable action reached the invoker\"))\n            }\n          }\n        }\n        .recoverWith {\n          case DocumentRevisionMismatchException(_) =>\n            // if revision is mismatched, the action may have been updated,\n            // so try again with the latest code\n            logging.warn(\n              this,\n              s\"msg ${msg.activationId} for ${msg.action} in $stateName is updated, fetching latest code\")\n            handleActivationMessage(msg.copy(revision = DocRevision.empty), action)\n          case t =>\n            // If the action cannot be found, the user has concurrently deleted it,\n            // making this an application error. All other errors are considered system\n            // errors and should cause the invoker to be considered unhealthy.\n            val response = t match {\n              case _: NoDocumentException =>\n                ExecutionResponse.applicationError(Messages.actionRemovedWhileInvoking)\n              case e: ActionLimitsException =>\n                ExecutionResponse.applicationError(e.getMessage) // return generated failed message\n              case _: DocumentTypeMismatchException | _: DocumentUnreadable =>\n                ExecutionResponse.whiskError(Messages.actionMismatchWhileInvoking)\n              case e: Throwable =>\n                logging.error(this, s\"An unknown DB connection error occurred while fetching an action: $e.\")\n                ExecutionResponse.whiskError(Messages.actionFetchErrorWhileInvoking)\n            }\n            val errMsg = s\"Error to fetch action ${msg.action} for msg ${msg.activationId}, error is ${t.getMessage}\"\n            logging.error(this, errMsg)\n\n            val context = UserContext(msg.user)\n            val activation = generateFallbackActivation(action, msg, response)\n            sendActiveAck(\n              transid,\n              activation,\n              msg.blocking,\n              msg.rootControllerIndex,\n              msg.user.namespace.uuid,\n              CombinedCompletionAndResultMessage(transid, activation, instance))\n            storeActivation(msg.transid, activation, msg.blocking, context)\n\n            // in case action is removed container proxy should be terminated\n            Future.failed(new IllegalStateException(errMsg))\n        }\n    } else {\n      // Iff the current namespace is blacklisted, an active-ack is only produced to keep the loadbalancer protocol\n      // Due to the protective nature of the blacklist, a database entry is not written.\n      val activation =\n        generateFallbackActivation(action, msg, ExecutionResponse.applicationError(Messages.namespacesBlacklisted))\n      sendActiveAck(\n        msg.transid,\n        activation,\n        false,\n        msg.rootControllerIndex,\n        msg.user.namespace.uuid,\n        CombinedCompletionAndResultMessage(msg.transid, activation, instance))\n      logging.warn(\n        this,\n        s\"namespace ${msg.user.namespace.name} was blocked in containerProxy, complete msg ${msg.activationId} with error.\")\n      Future.failed(new IllegalStateException(s\"namespace ${msg.user.namespace.name} was blocked in containerProxy.\"))\n    }\n\n  }\n\n  private def enableHealthPing(c: Container) = {\n    val hpa = healthPingActor.getOrElse {\n      logging.info(this, s\"creating health ping actor for ${c.addr.asString()}\")\n      val hp = context.actorOf(\n        TCPPingClient\n          .props(tcp, c.toString(), healtCheckConfig, new InetSocketAddress(c.addr.host, c.addr.port)))\n      healthPingActor = Some(hp)\n      hp\n    }\n    hpa ! HealthPingEnabled(true)\n  }\n\n  private def disableHealthPing() = {\n    healthPingActor.foreach(_ ! HealthPingEnabled(false))\n  }\n\n  def fallbackActivationForReschedulingData(data: ReschedulingData): Unit = {\n    val context = UserContext(data.resumeRun.msg.user)\n    val activation =\n      generateFallbackActivation(data.action, data.resumeRun.msg, ExecutionResponse.whiskError(Messages.abnormalRun))\n\n    sendActiveAck(\n      data.resumeRun.msg.transid,\n      activation,\n      data.resumeRun.msg.blocking,\n      data.resumeRun.msg.rootControllerIndex,\n      data.resumeRun.msg.user.namespace.uuid,\n      CombinedCompletionAndResultMessage(data.resumeRun.msg.transid, activation, instance))\n\n    storeActivation(data.resumeRun.msg.transid, activation, data.resumeRun.msg.blocking, context)\n  }\n\n  /**\n   * Runs the job, initialize first if necessary.\n   * Completes the job by:\n   * 1. sending an activate ack,\n   * 2. fetching the logs for the run,\n   * 3. indicating the resource is free to the parent pool,\n   * 4. recording the result to the data store\n   *\n   * @param container the container to run the job on\n   * @param job the job to run\n   * @return a future completing after logs have been collected and\n   *         added to the WhiskActivation\n   */\n  private def initializeAndRunActivation(\n    container: Container,\n    clientProxy: ActorRef,\n    action: ExecutableWhiskAction,\n    msg: ActivationMessage,\n    resumeRun: Option[RunActivation] = None)(implicit tid: TransactionId): Future[WhiskActivation] = {\n    // Add the activation to runningActivations set\n    runningActivations.put(msg.activationId.asString, true)\n\n    val actionTimeout = action.limits.timeout.duration\n\n    val (env, parameters) = ContainerProxy.partitionArguments(msg.content, msg.initArgs)\n\n    val environment = Map(\n      \"namespace\" -> msg.user.namespace.name.toJson,\n      \"action_name\" -> msg.action.qualifiedNameWithLeadingSlash.toJson,\n      \"action_version\" -> msg.action.version.toJson,\n      \"activation_id\" -> msg.activationId.toString.toJson,\n      \"transaction_id\" -> msg.transid.id.toJson)\n\n    // if the action requests the api key to be injected into the action context, add it here;\n    // treat a missing annotation as requesting the api key for backward compatibility\n    val authEnvironment = {\n      if (action.annotations.isTruthy(Annotations.ProvideApiKeyAnnotationName, valueForNonExistent = true)) {\n        msg.user.authkey.toEnvironment.fields\n      } else Map.empty\n    }\n\n    // Only initialize iff we haven't yet warmed the container\n    val initialize = stateData match {\n      case _: WarmData =>\n        Future.successful(None)\n      case _ =>\n        val owEnv = (authEnvironment ++ environment ++ Map(\n          \"deadline\" -> (Instant.now.toEpochMilli + actionTimeout.toMillis).toString.toJson)) map {\n          case (key, value) => \"__OW_\" + key.toUpperCase -> value\n        }\n        container\n          .initialize(action.containerInitializer(env ++ owEnv), actionTimeout, action.limits.concurrency.maxConcurrent)\n          .map(Some(_))\n    }\n\n    val activation: Future[WhiskActivation] = initialize\n      .flatMap { initInterval =>\n        // immediately setup warmedData for use (before first execution) so that concurrent actions can use it asap\n        if (initInterval.isDefined) {\n          stateData match {\n            case _: InitializedData =>\n              self ! InitCodeCompleted(\n                WarmData(container, msg.user.namespace.name.asString, action, msg.revision, Instant.now, clientProxy))\n\n            case _ =>\n              Future.failed(new IllegalStateException(\"lease does not exist\"))\n          }\n        }\n        val env = authEnvironment ++ environment ++ Map(\n          // compute deadline on invoker side avoids discrepancies inside container\n          // but potentially under-estimates actual deadline\n          \"deadline\" -> (Instant.now.toEpochMilli + actionTimeout.toMillis).toString.toJson)\n\n        container\n          .run(\n            parameters,\n            env.toJson.asJsObject,\n            actionTimeout,\n            action.limits.concurrency.maxConcurrent,\n            msg.user.limits.allowedMaxPayloadSize,\n            msg.user.limits.allowedTruncationSize,\n            resumeRun.isDefined)(msg.transid)\n          .map {\n            case (runInterval, response) =>\n              val initRunInterval = initInterval\n                .map(i => Interval(runInterval.start.minusMillis(i.duration.toMillis), runInterval.end))\n                .getOrElse(runInterval)\n              constructWhiskActivation(\n                action,\n                msg,\n                initInterval,\n                initRunInterval,\n                runInterval.duration >= actionTimeout,\n                response)\n          }\n      }\n      .recoverWith {\n        case h: ContainerHealthError if resumeRun.isDefined =>\n          // health error occurs\n          logging.error(this, s\"caught healthchek check error while running activation\")\n          Future.failed(ContainerHealthErrorWithResumedRun(h.tid, h.msg, resumeRun.get))\n\n        case InitializationError(interval, response) =>\n          Future.successful(\n            constructWhiskActivation(\n              action,\n              msg,\n              Some(interval),\n              interval,\n              interval.duration >= actionTimeout,\n              response))\n\n        case t =>\n          // Actually, this should never happen - but we want to make sure to not miss a problem\n          logging.error(this, s\"caught unexpected error while running activation: $t\")\n          Future.successful(\n            constructWhiskActivation(\n              action,\n              msg,\n              None,\n              Interval.zero,\n              false,\n              ExecutionResponse.whiskError(Messages.abnormalRun)))\n      }\n\n    val splitAckMessagesPendingLogCollection = collectLogs.logsToBeCollected(action)\n    // Sending an active ack is an asynchronous operation. The result is forwarded as soon as\n    // possible for blocking activations so that dependent activations can be scheduled. The\n    // completion message which frees a load balancer slot is sent after the active ack future\n    // completes to ensure proper ordering.\n    val sendResult = if (msg.blocking) {\n      activation.map { result =>\n        val ackMsg =\n          if (splitAckMessagesPendingLogCollection) ResultMessage(tid, result)\n          else CombinedCompletionAndResultMessage(tid, result, instance)\n        sendActiveAck(tid, result, msg.blocking, msg.rootControllerIndex, msg.user.namespace.uuid, ackMsg)\n      }\n    } else {\n      // For non-blocking request, do not forward the result.\n      if (splitAckMessagesPendingLogCollection) Future.successful(())\n      else\n        activation.map { result =>\n          val ackMsg = CompletionMessage(tid, result, instance)\n          sendActiveAck(tid, result, msg.blocking, msg.rootControllerIndex, msg.user.namespace.uuid, ackMsg)\n        }\n    }\n\n    activation.foreach { activation =>\n      val healthMessage = HealthMessage(!activation.response.isWhiskError)\n      invokerHealthManager ! healthMessage\n    }\n\n    val context = UserContext(msg.user)\n\n    // Adds logs to the raw activation.\n    val activationWithLogs: Future[Either[ActivationLogReadingError, WhiskActivation]] = activation\n      .flatMap { activation =>\n        // Skips log collection entirely, if the limit is set to 0\n        if (action.limits.logs.asMegaBytes == 0.MB) {\n          Future.successful(Right(activation))\n        } else {\n          val start = tid.started(this, LoggingMarkers.INVOKER_COLLECT_LOGS, logLevel = InfoLevel)\n          collectLogs(tid, msg.user, activation, container, action)\n            .andThen {\n              case Success(_) => tid.finished(this, start)\n              case Failure(t) => tid.failed(this, start, s\"reading logs failed: $t\")\n            }\n            .map(logs => Right(activation.withLogs(logs)))\n            .recover {\n              case LogCollectingException(logs) =>\n                Left(ActivationLogReadingError(activation.withLogs(logs)))\n              case _ =>\n                Left(ActivationLogReadingError(activation.withLogs(ActivationLogs(Vector(Messages.logFailure)))))\n            }\n        }\n      }\n\n    activationWithLogs\n      .map(_.fold(_.activation, identity))\n      .foreach { activation =>\n        // Sending the completion message to the controller after the active ack ensures proper ordering\n        // (result is received before the completion message for blocking invokes).\n        if (splitAckMessagesPendingLogCollection) {\n          sendResult.onComplete(\n            _ =>\n              sendActiveAck(\n                tid,\n                activation,\n                msg.blocking,\n                msg.rootControllerIndex,\n                msg.user.namespace.uuid,\n                CompletionMessage(tid, activation, instance)))\n        }\n\n        // Storing the record. Entirely asynchronous and not waited upon.\n        storeActivation(tid, activation, msg.blocking, context)\n      }\n\n    // Disambiguate activation errors and transform the Either into a failed/successful Future respectively.\n    activationWithLogs\n      .andThen {\n        // remove activationId from runningActivations in any case\n        case _ => runningActivations.remove(msg.activationId.asString)\n      }\n      .flatMap {\n        case Right(act) if !act.response.isSuccess && !act.response.isApplicationError =>\n          Future.failed(ActivationUnsuccessfulError(act))\n        case Left(error) => Future.failed(error)\n        case Right(act)  => Future.successful(act)\n      }\n  }\n\n  /** Generates an activation with zero runtime. Usually used for error cases */\n  private def generateFallbackActivation(action: ExecutableWhiskAction,\n                                         msg: ActivationMessage,\n                                         response: ExecutionResponse): WhiskActivation = {\n    val now = Instant.now\n    val causedBy = if (msg.causedBySequence) {\n      Some(Parameters(WhiskActivation.causedByAnnotation, JsString(Exec.SEQUENCE)))\n    } else None\n\n    WhiskActivation(\n      activationId = msg.activationId,\n      namespace = msg.user.namespace.name.toPath,\n      subject = msg.user.subject,\n      cause = msg.cause,\n      name = msg.action.name,\n      version = msg.action.version.getOrElse(SemVer()),\n      start = now,\n      end = now,\n      duration = Some(0),\n      response = response,\n      annotations = {\n        Parameters(WhiskActivation.pathAnnotation, JsString(msg.action.copy(version = None).asString)) ++\n          Parameters(WhiskActivation.kindAnnotation, JsString(action.exec.kind)) ++\n          causedBy\n      })\n  }\n\n}\n\nobject FunctionPullingContainerProxy {\n\n  def props(factory: (TransactionId,\n                      String,\n                      ImageName,\n                      Boolean,\n                      ByteSize,\n                      Int,\n                      Option[Double],\n                      Option[ExecutableWhiskAction]) => Future[Container],\n            entityStore: ArtifactStore[WhiskEntity],\n            namespaceBlacklist: NamespaceBlacklist,\n            get: (ArtifactStore[WhiskEntity], DocId, DocRevision, Boolean, Boolean) => Future[WhiskAction],\n            dataManagementService: ActorRef,\n            clientProxyFactory: (ActorRefFactory,\n                                 String,\n                                 FullyQualifiedEntityName,\n                                 DocRevision,\n                                 String,\n                                 Int,\n                                 ContainerId) => ActorRef,\n            ack: ActiveAck,\n            store: (TransactionId, WhiskActivation, Boolean, UserContext) => Future[Any],\n            collectLogs: LogsCollector,\n            getLiveContainerCount: (String, FullyQualifiedEntityName, DocRevision) => Future[Long],\n            getWarmedContainerLimit: (String) => Future[(Int, FiniteDuration)],\n            instance: InvokerInstanceId,\n            invokerHealthManager: ActorRef,\n            poolConfig: ContainerPoolConfig,\n            timeoutConfig: ContainerProxyTimeoutConfig,\n            healthCheckConfig: ContainerProxyHealthCheckConfig =\n              loadConfigOrThrow[ContainerProxyHealthCheckConfig](ConfigKeys.containerProxyHealth),\n            tcp: Option[ActorRef] = None)(implicit actorSystem: ActorSystem, logging: Logging) =\n    Props(\n      new FunctionPullingContainerProxy(\n        factory,\n        entityStore,\n        namespaceBlacklist,\n        get,\n        dataManagementService,\n        clientProxyFactory,\n        ack,\n        store,\n        collectLogs,\n        getLiveContainerCount,\n        getWarmedContainerLimit,\n        instance,\n        invokerHealthManager,\n        poolConfig,\n        timeoutConfig,\n        healthCheckConfig,\n        tcp))\n\n  private val containerCount = new Counter\n\n  /**\n   * Generates a unique container name.\n   *\n   * @param prefix the container name's prefix\n   * @param suffix the container name's suffix\n   * @return a unique container name\n   */\n  def containerName(instance: InvokerInstanceId, prefix: String, suffix: String): String = {\n    def isAllowed(c: Char): Boolean = c.isLetterOrDigit || c == '_'\n\n    val sanitizedPrefix = prefix.filter(isAllowed)\n    val sanitizedSuffix = suffix.filter(isAllowed)\n\n    s\"${ContainerFactory.containerNamePrefix(instance)}_${containerCount.next()}_${sanitizedPrefix}_${sanitizedSuffix}\"\n  }\n\n  /**\n   * Creates a WhiskActivation ready to be sent via active ack.\n   *\n   * @param job the job that was executed\n   * @param interval the time it took to execute the job\n   * @param response the response to return to the user\n   * @return a WhiskActivation to be sent to the user\n   */\n  def constructWhiskActivation(action: ExecutableWhiskAction,\n                               msg: ActivationMessage,\n                               initInterval: Option[Interval],\n                               totalInterval: Interval,\n                               isTimeout: Boolean,\n                               response: ExecutionResponse) = {\n\n    val causedBy = if (msg.causedBySequence) {\n      Some(Parameters(WhiskActivation.causedByAnnotation, JsString(Exec.SEQUENCE)))\n    } else None\n\n    val waitTime = {\n      val end = initInterval.map(_.start).getOrElse(totalInterval.start)\n      Parameters(WhiskActivation.waitTimeAnnotation, Interval(msg.transid.meta.start, end).duration.toMillis.toJson)\n    }\n\n    val initTime = {\n      initInterval.map(initTime => Parameters(WhiskActivation.initTimeAnnotation, initTime.duration.toMillis.toJson))\n    }\n\n    val binding =\n      msg.action.binding.map(f => Parameters(WhiskActivation.bindingAnnotation, JsString(f.asString)))\n\n    WhiskActivation(\n      activationId = msg.activationId,\n      namespace = msg.user.namespace.name.toPath,\n      subject = msg.user.subject,\n      cause = msg.cause,\n      name = action.name,\n      version = action.version,\n      start = totalInterval.start,\n      end = totalInterval.end,\n      duration = Some(totalInterval.duration.toMillis),\n      response = response,\n      annotations = {\n        Parameters(WhiskActivation.limitsAnnotation, action.limits.toJson) ++\n          Parameters(WhiskActivation.pathAnnotation, JsString(action.fullyQualifiedName(false).asString)) ++\n          Parameters(WhiskActivation.kindAnnotation, JsString(action.exec.kind)) ++\n          Parameters(WhiskActivation.timeoutAnnotation, JsBoolean(isTimeout)) ++\n          causedBy ++ initTime ++ waitTime ++ binding\n      })\n  }\n\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/v2/InvokerHealthManager.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.v2\n\nimport org.apache.pekko.actor.Status.{Failure => FailureMessage}\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, ActorSystem, FSM, Props, Stash}\nimport org.apache.pekko.util.Timeout\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Offline, Unhealthy}\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.containerpool.ContainerRemoved\nimport org.apache.openwhisk.core.database.{ArtifactStore, NoDocumentException}\nimport org.apache.openwhisk.core.entitlement.Privilege\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.types.EntityStore\nimport org.apache.openwhisk.core.entity.{ActivationResponse => _, _}\nimport org.apache.openwhisk.core.etcd.EtcdKV.InvokerKeys\nimport org.apache.openwhisk.core.service.UpdateDataOnChange\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success}\n\nclass InvokerHealthManager(instanceId: InvokerInstanceId,\n                           healthContainerProxyFactory: (ActorRefFactory, ActorRef) => ActorRef,\n                           dataManagementService: ActorRef,\n                           entityStore: ArtifactStore[WhiskEntity])(implicit actorSystem: ActorSystem, logging: Logging)\n    extends FSM[InvokerState, InvokerHealthData]\n    with Stash {\n\n  implicit val requestTimeout = Timeout(5.seconds)\n  implicit val ec: ExecutionContext = actorSystem.dispatcher\n  implicit val transid: TransactionId = TransactionId.invokerHealth\n\n  private[containerpool] var healthActionProxy: Option[ActorRef] = None\n\n  startWith(\n    Offline,\n    InvokerInfo(\n      new RingBuffer[Boolean](InvokerHealthManager.bufferSize),\n      memory = MemoryInfo(instanceId.userMemory.toMB, 0, 0)))\n\n  when(Offline) {\n    case Event(GracefulShutdown, _: InvokerInfo) =>\n      logging.warn(this, \"Received a graceful shutdown flag, stopping the invoker.\")\n      stay\n\n    case Event(Enable, _) =>\n      InvokerHealthManager.prepare(entityStore, instanceId).map { _ =>\n        startTestAction(self)\n      }\n      goto(Unhealthy)\n  }\n\n  when(Unhealthy) {\n    case Event(ContainerRemoved(_), _) =>\n      healthActionProxy = None\n      startTestAction(self)\n      stay\n\n    case Event(msg: FailureMessage, _) =>\n      logging.error(this, s\"invoker${instanceId}, status:${stateName} got a failure message: ${msg}\")\n      stay\n\n    case Event(ContainerCreationFailed(_), _) =>\n      stay\n  }\n\n  when(Healthy) {\n    case Event(msg: FailureMessage, _) =>\n      logging.error(this, s\"invoker${instanceId}, status:${stateName} got a failure message: ${msg}\")\n      goto(Unhealthy)\n  }\n\n  whenUnhandled {\n    case Event(_: Initialized, _) =>\n      // Initialized messages sent by ContainerProxy for HealthManger\n      stay()\n\n    case Event(ContainerRemoved(_), _) =>\n      // Drop messages sent by ContainerProxy for HealthManger\n      healthActionProxy = None\n      stay()\n\n    case Event(GracefulShutdown, _) =>\n      self ! GracefulShutdown\n      goto(Offline)\n\n    case Event(healthMsg: HealthMessage, data: InvokerInfo) =>\n      if (stateName != Offline) {\n        handleHealthMessage(healthMsg.state, data.buffer)\n      } else {\n        stay\n      }\n\n    case Event(memoryInfo: MemoryInfo, data: InvokerInfo) =>\n      publishHealthStatusAndStay(stateName, data.copy(memory = memoryInfo))\n\n    // in case of StatusRuntimeException: NOT_FOUND: etcdserver: requested lease not found, we need to get the lease again.\n    case Event(t: FailureMessage, _) =>\n      logging.error(this, s\"Failure happens, restart InvokerHealthManager: ${t}\")\n      goto(Offline)\n  }\n\n  // It is important to note that stateName and the stateData in onTransition callback refer to the previous one.\n  // We should access to the next data with nextStateData\n  onTransition {\n    case Offline -> Unhealthy =>\n      publishHealthStatusAndStay(Unhealthy, nextStateData)\n\n    case Healthy -> Unhealthy =>\n      unstashAll()\n      transid.mark(\n        this,\n        LoggingMarkers.LOADBALANCER_INVOKER_STATUS_CHANGE(Unhealthy.asString),\n        s\"invoker${instanceId.toInt} is unhealthy\",\n        org.apache.pekko.event.Logging.WarningLevel)\n      startTestAction(self)\n      publishHealthStatusAndStay(Unhealthy, nextStateData)\n\n    case _ -> Healthy =>\n      logging.info(this, s\"invoker became healthy, stop health action proxy.\")\n      unstashAll()\n      stopTestAction()\n\n      publishHealthStatusAndStay(Healthy, nextStateData)\n\n    case oldState -> newState if oldState != newState =>\n      publishHealthStatusAndStay(newState, nextStateData)\n      unstashAll()\n  }\n\n  private def publishHealthStatusAndStay(state: InvokerState, stateData: InvokerHealthData) = {\n    stateData match {\n      case data: InvokerInfo =>\n        val invokerResourceMessage = InvokerResourceMessage(\n          state.asString,\n          data.memory.freeMemory,\n          data.memory.busyMemory,\n          data.memory.inProgressMemory,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces)\n        dataManagementService ! UpdateDataOnChange(InvokerKeys.health(instanceId), invokerResourceMessage.serialize)\n        stay using data.copy(currentInvokerResource = Some(invokerResourceMessage))\n\n      case data =>\n        logging.error(this, s\"unexpected data is found: $data\")\n        stay\n    }\n  }\n\n  initialize()\n\n  private def startTestAction(manager: ActorRef): Unit = {\n    val namespace = InvokerHealthManager.healthActionIdentity.namespace.name.asString\n    val docId = InvokerHealthManager.healthAction(instanceId).get.docid\n\n    WhiskAction.get(entityStore, docId).onComplete {\n      case Success(action) =>\n        val initialize =\n          Initialize(namespace, action.fullyQualifiedName(true), action.toExecutableWhiskAction.get, \"\", 0, transid)\n        startHealthAction(initialize, manager)\n      case Failure(t) => logging.error(this, s\"get health action error: ${t.getMessage}\")\n    }\n  }\n\n  private def startHealthAction(initialize: Initialize, manager: ActorRef): Unit = {\n    healthActionProxy match {\n      case Some(proxy) =>\n        // make healthContainerProxy's status is Running, then healthContainerProxy can fetch the activation using ActivationServiceClient\n        proxy ! initialize\n      case None =>\n        val proxy = healthContainerProxyFactory(context, manager)\n        proxy ! initialize\n        healthActionProxy = Some(proxy)\n    }\n  }\n\n  def stopTestAction(): Unit = {\n    healthActionProxy.foreach {\n      healthActionProxy = None\n      _ ! GracefulShutdown\n    }\n  }\n\n  /**\n   * This method is to handle health message from ContainerProxy.pub\n   * It can induce status change.\n   *\n   * @param state  activation result state\n   * @param buffer RingBuffer to track status\n   * @return\n   */\n  def handleHealthMessage(state: Boolean, buffer: RingBuffer[Boolean]): State = {\n    buffer.add(state)\n    val falseStateCount = buffer.toList.count(_ == false)\n    if (falseStateCount < InvokerHealthManager.bufferErrorTolerance) {\n      gotoIfNotThere(Healthy)\n    } else {\n      logging.warn(\n        this,\n        s\"become unhealthy because system error exceeded the error tolerance, falseStateCount $falseStateCount, errorTolerance ${InvokerHealthManager.bufferErrorTolerance}\")\n      gotoIfNotThere(Unhealthy)\n    }\n  }\n\n  /**\n   * This is to decide weather to change from the newState or not.\n   * If current state is already newState, it will stay, otherwise it will change its state.\n   *\n   * @param newState the desired state to change.\n   * @return\n   */\n  private def gotoIfNotThere(newState: InvokerState) = {\n    if (stateName == newState) {\n      stay()\n    } else {\n      goto(newState)\n    }\n  }\n\n  /** Delays all incoming messages until unstashAll() is called */\n  def delay = {\n    stash()\n    stay\n  }\n\n}\n\ncase class HealthActivationServiceClient() extends Actor {\n\n  private var closed: Boolean = false\n\n  override def receive: Receive = {\n    case StartClient => sender() ! ClientCreationCompleted\n    case _: RequestActivation =>\n      InvokerHealthManager.healthActivation match {\n        case Some(activation) if !closed =>\n          sender() ! activation.copy(\n            transid = TransactionId.invokerHealthActivation,\n            activationId = ActivationId.generate())\n\n        case _ if closed =>\n          context.parent ! ClientClosed\n          context.stop(self)\n\n        case _ => // do nothing\n      }\n\n    case GracefulShutdown =>\n      closed = true\n\n  }\n}\n\nobject InvokerHealthManager {\n  val healthActionNamePrefix = \"invokerHealthTestAction\"\n  val bufferSize = 10\n  val bufferErrorTolerance = 3\n  val healthActionIdentity: Identity = {\n    val whiskSystem = \"whisk.system\"\n    val uuid = UUID()\n    Identity(\n      Subject(whiskSystem),\n      Namespace(EntityName(whiskSystem), uuid),\n      BasicAuthenticationAuthKey(uuid, Secret()),\n      Set[Privilege]())\n  }\n\n  def healthAction(i: InvokerInstanceId): Option[WhiskAction] =\n    ExecManifest.runtimesManifest.resolveDefaultRuntime(\"nodejs:default\").map { manifest =>\n      new WhiskAction(\n        namespace = InvokerHealthManager.healthActionIdentity.namespace.name.toPath,\n        name = EntityName(s\"$healthActionNamePrefix${i.toInt}\"),\n        exec = CodeExecAsString(manifest, \"\"\"function main(params) { return params; }\"\"\", None),\n        limits = ActionLimits(memory = MemoryLimit(MemoryLimit.MIN_MEMORY), logs = LogLimit(0.B)))\n    }\n\n  var healthActivation: Option[ActivationMessage] = None\n\n  private def createTestActionForInvokerHealth(db: EntityStore, action: WhiskAction): Future[DocInfo] = {\n    implicit val tid: TransactionId = TransactionId.invokerHealthManager\n    implicit val ec: ExecutionContext = db.executionContext\n    implicit val logging: Logging = db.logging\n\n    WhiskAction\n      .get(db, action.docid)\n      .flatMap { oldAction =>\n        WhiskAction.put(db, action.revision(oldAction.rev), Some(oldAction))(tid, notifier = None)\n      }\n      .recoverWith {\n        case _: NoDocumentException => WhiskAction.put(db, action, old = None)(tid, notifier = None)\n      }\n      .andThen {\n        case Success(_) => logging.info(this, \"test action for invoker health now exists\")\n        case Failure(e) => logging.error(this, s\"error creating test action for invoker health: $e\")\n      }\n  }\n\n  private def createHealthActivation(entityStore: ArtifactStore[WhiskEntity],\n                                     docInfo: DocInfo)(implicit ec: ExecutionContext, logging: Logging) = {\n    implicit val transId = TransactionId.invokerHealth\n\n    WhiskAction.get(entityStore, docInfo.id).onComplete {\n      case Success(action) =>\n        healthActivation = Some(\n          ActivationMessage(\n            TransactionId.invokerHealth,\n            action.toExecutableWhiskAction.get.fullyQualifiedName(true),\n            action.rev,\n            healthActionIdentity,\n            ActivationId.generate(),\n            ControllerInstanceId(\"health\"),\n            blocking = false,\n            content = None))\n      case Failure(t) => logging.error(this, s\"get health action error: ${t.getMessage}\")\n    }\n  }\n\n  def prepare(entityStore: ArtifactStore[WhiskEntity],\n              invokerInstanceId: InvokerInstanceId)(implicit ec: ExecutionContext, logging: Logging): Future[Unit] = {\n    InvokerHealthManager.healthAction(invokerInstanceId) match {\n      case Some(action) =>\n        createTestActionForInvokerHealth(entityStore, action)\n          .map(docId => createHealthActivation(entityStore, docId))\n      case None =>\n        throw new IllegalStateException(\n          \"cannot create test action for invoker health because runtime manifest is not valid\")\n    }\n  }\n\n  def props(instanceId: InvokerInstanceId,\n            childFactory: (ActorRefFactory, ActorRef) => ActorRef,\n            dataManagementService: ActorRef,\n            entityStore: ArtifactStore[WhiskEntity])(implicit actorSystem: ActorSystem, logging: Logging): Props = {\n    Props(new InvokerHealthManager(instanceId, childFactory, dataManagementService, entityStore))\n  }\n}\n\n//recevied from ContainerProxy actor\ncase class HealthMessage(state: Boolean)\n\n//rereived from ContainerPool actor\ncase class MemoryInfo(freeMemory: Long, busyMemory: Long, inProgressMemory: Long)\n\n// Data stored in the Invoker\nsealed class InvokerHealthData\n\ncase class InvokerInfo(buffer: RingBuffer[Boolean],\n                       memory: MemoryInfo = MemoryInfo(0, 0, 0),\n                       currentInvokerResource: Option[InvokerResourceMessage] = None)\n    extends InvokerHealthData\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/invoker/ContainerMessageConsumer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker\n\nimport org.apache.pekko.actor.{ActorRef, ActorSystem, Props}\nimport org.apache.openwhisk.common.{GracefulShutdown, Logging, TransactionId}\nimport org.apache.openwhisk.core.WarmUp.isWarmUpAction\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.ContainerCreationError.{DBFetchError, InvalidActionLimitError}\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.containerpool.v2.{CreationContainer, DeletionContainer}\nimport org.apache.openwhisk.core.database.{ArtifactStore, DocumentRevisionMismatchException, NoDocumentException}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.Messages\n\nimport java.nio.charset.StandardCharsets\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success}\n\nclass ContainerMessageConsumer(\n  invokerInstanceId: InvokerInstanceId,\n  containerPool: ActorRef,\n  entityStore: ArtifactStore[WhiskEntity],\n  config: WhiskConfig,\n  msgProvider: MessagingProvider,\n  longPollDuration: FiniteDuration,\n  maxPeek: Int,\n  sendAckToScheduler: (SchedulerInstanceId, ContainerCreationAckMessage) => Future[ResultMetadata])(\n  implicit actorSystem: ActorSystem,\n  executionContext: ExecutionContext,\n  logging: Logging) {\n\n  private val topic = s\"${Invoker.topicPrefix}invoker${invokerInstanceId.toInt}\"\n  private val consumer =\n    msgProvider.getConsumer(config, topic, topic, maxPeek, maxPollInterval = TimeLimit.MAX_DURATION + 1.minute)\n\n  private val authStore = WhiskAuthStore.datastore()\n\n  private def handler(bytes: Array[Byte]): Future[Unit] = Future {\n    val raw = new String(bytes, StandardCharsets.UTF_8)\n    ContainerMessage.parse(raw) match {\n      case Success(creation: ContainerCreationMessage) if isWarmUpAction(creation.action) =>\n        logging.info(\n          this,\n          s\"container creation message for ${creation.invocationNamespace}/${creation.action} is received (creationId: ${creation.creationId})\")\n        feed ! MessageFeed.Processed\n\n      case Success(creation: ContainerCreationMessage) =>\n        implicit val transid: TransactionId = creation.transid\n        logging\n          .info(this, s\"container creation message for ${creation.invocationNamespace}/${creation.action} is received\")\n\n        val createContainer = for {\n          identity <- Identity.get(authStore, EntityName(creation.invocationNamespace))\n          action <- WhiskAction\n            .get(entityStore, creation.action.toDocId, creation.revision, fromCache = true)\n        } yield {\n          // check action limits before creating container\n          action.limits.checkLimits(identity)\n          containerPool ! CreationContainer(creation, action)\n          feed ! MessageFeed.Processed\n        }\n        createContainer.recover {\n          case t =>\n            val creationError = t match {\n              case _: ActionLimitsException => InvalidActionLimitError\n              case _                        => DBFetchError\n            }\n            val message = t match {\n              case _: ActionLimitsException => t.getMessage // return generated failed message\n              case _: NoDocumentException =>\n                Messages.actionRemovedWhileInvoking\n              case _: DocumentRevisionMismatchException =>\n                Messages.actionMismatchWhileInvoking\n              case e: Throwable =>\n                logging.error(\n                  this,\n                  s\"An unknown DB error occurred while fetching action ${creation.invocationNamespace}/${creation.action} for creation ${creation.creationId}, error: $e.\")\n                Messages.actionFetchErrorWhileInvoking\n            }\n            logging.error(\n              this,\n              s\"failed to create a container ${creation.invocationNamespace}/${creation.action}, error: $message (creationId: ${creation.creationId})\")\n\n            val ack = ContainerCreationAckMessage(\n              creation.transid,\n              creation.creationId,\n              creation.invocationNamespace,\n              creation.action,\n              creation.revision,\n              creation.whiskActionMetaData,\n              invokerInstanceId,\n              creation.schedulerHost,\n              creation.rpcPort,\n              creation.retryCount,\n              Some(creationError),\n              Some(message))\n            sendAckToScheduler(creation.rootSchedulerIndex, ack)\n            feed ! MessageFeed.Processed\n        }\n      case Success(deletion: ContainerDeletionMessage) =>\n        implicit val transid: TransactionId = deletion.transid\n        logging.info(this, s\"deletion message for ${deletion.invocationNamespace}/${deletion.action} is received\")\n        containerPool ! DeletionContainer(deletion)\n        feed ! MessageFeed.Processed\n      case Failure(t) =>\n        logging.error(this, s\"Failed to parse $bytes, error: ${t.getMessage}\")\n        feed ! MessageFeed.Processed\n\n      case _ =>\n        logging.error(this, s\"Unexpected message received $raw\")\n        feed ! MessageFeed.Processed\n    }\n  }\n\n  private val feed = actorSystem.actorOf(Props {\n    new MessageFeed(\"containerCreation\", logging, consumer, maxPeek, longPollDuration, handler)\n  })\n\n  def close(): Unit = {\n    feed ! GracefulShutdown\n  }\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/invoker/DefaultInvokerServer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.http.BasicRasService\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport pureconfig.loadConfigOrThrow\nimport spray.json.PrettyPrinter\n\nimport scala.concurrent.ExecutionContext\n\n/**\n * Implements web server to handle certain REST API calls.\n */\nclass DefaultInvokerServer(val invoker: InvokerCore, systemUsername: String, systemPassword: String)(\n  implicit val ec: ExecutionContext,\n  val actorSystem: ActorSystem,\n  val logger: Logging)\n    extends BasicRasService {\n\n  /** Pretty print JSON response. */\n  implicit val jsonPrettyResponsePrinter = PrettyPrinter\n\n  override def routes(implicit transid: TransactionId): Route = {\n    super.routes ~ extractCredentials {\n      case Some(BasicHttpCredentials(username, password)) if username == systemUsername && password == systemPassword =>\n        (path(\"enable\") & post) {\n          complete(invoker.enable())\n        } ~ (path(\"disable\") & post) {\n          complete(invoker.disable())\n        } ~ (path(\"isEnabled\") & get) {\n          complete(invoker.isEnabled())\n        }\n      case _ => terminate(StatusCodes.Unauthorized)\n    }\n  }\n}\n\nobject DefaultInvokerServer extends InvokerServerProvider {\n\n  private val invokerUsername = loadConfigOrThrow[String](ConfigKeys.whiskInvokerUsername)\n  private val invokerPassword = loadConfigOrThrow[String](ConfigKeys.whiskInvokerPassword)\n\n  override def instance(\n    invoker: InvokerCore)(implicit ec: ExecutionContext, actorSystem: ActorSystem, logger: Logging): BasicRasService =\n    new DefaultInvokerServer(invoker, invokerUsername, invokerPassword)\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/invoker/FPCInvokerReactive.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.{ActorRef, ActorRefFactory, ActorSystem, CoordinatedShutdown, Props}\nimport org.apache.pekko.grpc.GrpcClientSettings\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.util.Timeout\nimport com.ibm.etcd.api.Event.EventType\nimport com.ibm.etcd.client.kv.KvClient.Watch\nimport com.ibm.etcd.client.kv.WatchUpdate\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.ack.{ActiveAck, HealthActionAck, MessagingActiveAck, UserEventSender}\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.containerpool.logging.LogStoreProvider\nimport org.apache.openwhisk.core.containerpool.v2._\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.containerPrefix\nimport org.apache.openwhisk.core.etcd.EtcdKV.QueueKeys.queue\nimport org.apache.openwhisk.core.etcd.EtcdKV.{ContainerKeys, SchedulerKeys}\nimport org.apache.openwhisk.core.etcd.EtcdType._\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdConfig, EtcdWorker}\nimport org.apache.openwhisk.core.invoker.Invoker.InvokerEnabled\nimport org.apache.openwhisk.core.scheduler.{SchedulerEndpoints, SchedulerStates}\nimport org.apache.openwhisk.core.service.{DataManagementService, LeaseKeepAliveService, WatcherService}\nimport org.apache.openwhisk.core.{ConfigKeys, WarmUp, WhiskConfig}\nimport org.apache.openwhisk.grpc.{ActivationServiceClient, FetchRequest}\nimport org.apache.openwhisk.spi.SpiLoader\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.collection.JavaConverters._\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}\nimport scala.util.{Failure, Success, Try}\n\ncase class GrpcServiceConfig(tls: Boolean)\n\nobject FPCInvokerReactive extends InvokerProvider {\n\n  override def instance(\n    config: WhiskConfig,\n    instance: InvokerInstanceId,\n    producer: MessageProducer,\n    poolConfig: ContainerPoolConfig,\n    limitsConfig: IntraConcurrencyLimitConfig)(implicit actorSystem: ActorSystem, logging: Logging): InvokerCore =\n    new FPCInvokerReactive(config, instance, producer, poolConfig, limitsConfig)\n}\n\nclass FPCInvokerReactive(config: WhiskConfig,\n                         instance: InvokerInstanceId,\n                         producer: MessageProducer,\n                         poolConfig: ContainerPoolConfig =\n                           loadConfigOrThrow[ContainerPoolConfig](ConfigKeys.containerPool),\n                         limitsConfig: IntraConcurrencyLimitConfig = loadConfigOrThrow[IntraConcurrencyLimitConfig](\n                           ConfigKeys.concurrencyLimit))(implicit actorSystem: ActorSystem, logging: Logging)\n    extends InvokerCore {\n\n  implicit val ec: ExecutionContext = actorSystem.dispatcher\n  implicit val exe: ExecutionContextExecutor = actorSystem.dispatcher\n  implicit val cfg: WhiskConfig = config\n\n  private val logsProvider = SpiLoader.get[LogStoreProvider].instance(actorSystem)\n  logging.info(this, s\"LogStoreProvider: ${logsProvider.getClass}\")\n\n  private val etcdClient = EtcdClient(loadConfigOrThrow[EtcdConfig](ConfigKeys.etcd))\n\n  private val grpcConfig = loadConfigOrThrow[GrpcServiceConfig](ConfigKeys.schedulerGrpcService)\n\n  val watcherService: ActorRef = actorSystem.actorOf(WatcherService.props(etcdClient))\n\n  private val leaseService =\n    actorSystem.actorOf(LeaseKeepAliveService.props(etcdClient, instance, watcherService))\n\n  private val etcdWorkerFactory =\n    (f: ActorRefFactory) => f.actorOf(EtcdWorker.props(etcdClient, leaseService))\n\n  val dataManagementService: ActorRef =\n    actorSystem.actorOf(DataManagementService.props(watcherService, etcdWorkerFactory))\n\n  private val warmedSchedulers = TrieMap[String, String]()\n  private var warmUpWatcher: Option[Watch] = None\n\n  /**\n   * Factory used by the ContainerProxy to physically create a new container.\n   *\n   * Create and initialize the container factory before kicking off any other\n   * task or actor because further operation does not make sense if something\n   * goes wrong here. Initialization will throw an exception upon failure.\n   */\n  private val containerFactory =\n    SpiLoader\n      .get[ContainerFactoryProvider]\n      .instance(\n        actorSystem,\n        logging,\n        config,\n        instance,\n        Map(\n          \"--cap-drop\" -> Set(\"NET_RAW\", \"NET_ADMIN\"),\n          \"--ulimit\" -> Set(\"nofile=1024:1024\"),\n          \"--pids-limit\" -> Set(\"1024\")) ++ logsProvider.containerParameters)\n  containerFactory.init()\n\n  CoordinatedShutdown(actorSystem)\n    .addTask(CoordinatedShutdown.PhaseBeforeActorSystemTerminate, \"cleanup runtime containers\") { () =>\n      containerFactory.cleanup()\n      Future.successful(Done)\n    }\n\n  /** Initialize needed databases */\n  private val entityStore = WhiskEntityStore.datastore()\n  private val activationStore =\n    SpiLoader.get[ActivationStoreProvider].instance(actorSystem, logging)\n\n  private val authStore = WhiskAuthStore.datastore()\n\n  private val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n\n  Scheduler.scheduleWaitAtMost(loadConfigOrThrow[NamespaceBlacklistConfig](ConfigKeys.blacklist).pollInterval) { () =>\n    logging.debug(this, \"running background job to update blacklist\")\n    namespaceBlacklist.refreshBlacklist()(ec, TransactionId.invoker).andThen {\n      case Success(set) => logging.info(this, s\"updated blacklist to ${set.size} entries\")\n      case Failure(t)   => logging.error(this, s\"error on updating the blacklist: ${t.getMessage}\")\n    }\n  }\n\n  val containerProxyTimeoutConfig = loadConfigOrThrow[ContainerProxyTimeoutConfig](ConfigKeys.containerProxyTimeouts)\n\n  private def getWarmedContainerLimit(invocationNamespace: String): Future[(Int, FiniteDuration)] = {\n    implicit val trasnid = TransactionId.unknown\n    Identity\n      .get(authStore, EntityName(invocationNamespace))(trasnid)\n      .map { identity =>\n        val warmedContainerKeepingCount = identity.limits.warmedContainerKeepingCount.getOrElse(0)\n        val warmedContainerKeepingTimeout = Try {\n          identity.limits.warmedContainerKeepingTimeout.map(Duration(_).toSeconds.seconds).get\n        }.getOrElse(containerProxyTimeoutConfig.keepingDuration)\n        (warmedContainerKeepingCount, warmedContainerKeepingTimeout)\n      }\n      .andThen {\n        case Failure(_: NoDocumentException) =>\n          logging.warn(this, s\"namespace does not exist: $invocationNamespace\")(trasnid)\n        case Failure(_: IllegalStateException) =>\n          logging.warn(this, s\"namespace is not unique: $invocationNamespace\")(trasnid)\n      }\n  }\n\n  private val ack = {\n    val sender = if (UserEvents.enabled) Some(new UserEventSender(producer)) else None\n    new MessagingActiveAck(producer, instance, sender)\n  }\n\n  // we don't need to store health action results in normal case\n  private val healthActionAck: ActiveAck = new HealthActionAck(producer)\n\n  private val collectLogs = new LogStoreCollector(logsProvider)\n\n  /** Stores an activation in the database. */\n  private val store = (tid: TransactionId, activation: WhiskActivation, isBlocking: Boolean, context: UserContext) => {\n    implicit val transid: TransactionId = tid\n    activationStore.storeAfterCheck(activation, isBlocking, None, None, context)(tid, notifier = None, logging)\n  }\n\n  private def healthActivationClientFactory(f: ActorRefFactory,\n                                            invocationNamespace: String,\n                                            fqn: FullyQualifiedEntityName,\n                                            rev: DocRevision,\n                                            schedulerHost: String,\n                                            rpcPort: Int,\n                                            containerId: ContainerId): ActorRef =\n    f.actorOf(Props(HealthActivationServiceClient()))\n\n  private def healthContainerProxyFactory(f: ActorRefFactory, healthManger: ActorRef): ActorRef = {\n    implicit val transId = TransactionId.invokerNanny\n    f.actorOf(\n      FunctionPullingContainerProxy\n        .props(\n          containerFactory.createContainer,\n          entityStore,\n          namespaceBlacklist,\n          WhiskAction.get,\n          dataManagementService,\n          healthActivationClientFactory,\n          healthActionAck,\n          store,\n          collectLogs,\n          getLiveContainerCount,\n          getWarmedContainerLimit,\n          instance,\n          healthManger,\n          poolConfig,\n          containerProxyTimeoutConfig))\n  }\n\n  private val invokerHealthManager =\n    actorSystem.actorOf(\n      InvokerHealthManager.props(instance, healthContainerProxyFactory, dataManagementService, entityStore))\n\n  invokerHealthManager ! Enable\n\n  private def activationClientFactory(etcd: EtcdClient)(\n    invocationNamespace: String,\n    fqn: FullyQualifiedEntityName,\n    schedulerHost: String,\n    rpcPort: Int,\n    tryOtherScheduler: Boolean = false): Future[ActivationServiceClient] = {\n\n    if (!tryOtherScheduler) {\n      val setting =\n        GrpcClientSettings\n          .connectToServiceAt(schedulerHost, rpcPort)\n          .withTls(grpcConfig.tls)\n      Future {\n        ActivationServiceClient(setting)\n      }.andThen {\n        case Failure(t) =>\n          logging.error(\n            this,\n            s\"unable to create activation client for action ${fqn}: ${t} on original scheduler: ${schedulerHost}:${rpcPort}\")\n      }\n    } else {\n      val leaderKey = queue(invocationNamespace, fqn, leader = true)\n      etcd\n        .get(leaderKey)\n        .flatMap { res =>\n          require(!res.getKvsList.isEmpty)\n\n          val endpoint: String = res.getKvsList.get(0).getValue\n          Future(SchedulerEndpoints.parse(endpoint))\n            .flatMap(Future.fromTry)\n            .map { schedulerEndpoint =>\n              val setting =\n                GrpcClientSettings\n                  .connectToServiceAt(schedulerEndpoint.host, schedulerEndpoint.rpcPort)\n                  .withTls(grpcConfig.tls)\n\n              ActivationServiceClient(setting)\n            }\n            .andThen {\n              case Failure(t) =>\n                logging.error(this, s\"unable to create activation client for action ${fqn}: ${t}\")\n            }\n        }\n    }\n\n  }\n\n  private def sendAckToScheduler(schedulerInstanceId: SchedulerInstanceId,\n                                 creationAckMessage: ContainerCreationAckMessage): Future[ResultMetadata] = {\n    val topic = s\"${Invoker.topicPrefix}creationAck${schedulerInstanceId.asString}\"\n    val reschedulable =\n      creationAckMessage.error.map(ContainerCreationError.whiskErrors.contains(_)).getOrElse(false)\n    if (reschedulable) {\n      MetricEmitter.emitCounterMetric(\n        LoggingMarkers.INVOKER_CONTAINER_CREATE(creationAckMessage.action.toString, \"reschedule\"))\n    } else if (creationAckMessage.error.nonEmpty) {\n      MetricEmitter.emitCounterMetric(\n        LoggingMarkers.INVOKER_CONTAINER_CREATE(creationAckMessage.action.toString, \"failed\"))\n    }\n\n    producer.send(topic, creationAckMessage).andThen {\n      case Success(_) =>\n        logging.info(\n          this,\n          s\"Posted ${if (reschedulable) \"rescheduling\"\n          else if (creationAckMessage.error.nonEmpty) \"failed\"\n          else \"success\"} ack of container creation ${creationAckMessage.creationId} for ${creationAckMessage.invocationNamespace}/${creationAckMessage.action}\")\n      case Failure(t) =>\n        logging.error(\n          this,\n          s\"failed to send container creation ack message(${creationAckMessage.creationId}) for ${creationAckMessage.invocationNamespace}/${creationAckMessage.action} to scheduler: ${t.getMessage}\")\n    }\n  }\n\n  /** Creates a ContainerProxy Actor when being called. */\n  private val childFactory = (f: ActorRefFactory) => {\n    implicit val transId = TransactionId.invokerNanny\n    f.actorOf(\n      FunctionPullingContainerProxy\n        .props(\n          containerFactory.createContainer,\n          entityStore,\n          namespaceBlacklist,\n          WhiskAction.get,\n          dataManagementService,\n          clientProxyFactory,\n          ack,\n          store,\n          collectLogs,\n          getLiveContainerCount,\n          getWarmedContainerLimit,\n          instance,\n          invokerHealthManager,\n          poolConfig,\n          containerProxyTimeoutConfig))\n  }\n\n  /** Creates a ActivationClientProxy Actor when being called. */\n  private def clientProxyFactory(f: ActorRefFactory,\n                                 invocationNamespace: String,\n                                 fqn: FullyQualifiedEntityName,\n                                 rev: DocRevision,\n                                 schedulerHost: String,\n                                 rpcPort: Int,\n                                 containerId: ContainerId): ActorRef = {\n    implicit val transId = TransactionId.invokerNanny\n    f.actorOf(\n      ActivationClientProxy\n        .props(invocationNamespace, fqn, rev, schedulerHost, rpcPort, containerId, activationClientFactory(etcdClient)))\n  }\n\n  val prewarmingConfigs: List[PrewarmingConfig] = {\n    ExecManifest.runtimesManifest.stemcells.flatMap {\n      case (mf, cells) =>\n        cells.map { cell =>\n          PrewarmingConfig(cell.initialCount, new CodeExecAsString(mf, \"\", None), cell.memory)\n        }\n    }.toList\n  }\n\n  private val pool =\n    actorSystem.actorOf(\n      ContainerPoolV2\n        .props(childFactory, invokerHealthManager, poolConfig, instance, prewarmingConfigs, sendAckToScheduler))\n\n  private def getLiveContainerCount(invocationNamespace: String,\n                                    fqn: FullyQualifiedEntityName,\n                                    revision: DocRevision): Future[Long] = {\n    val namespacePrefix = containerPrefix(ContainerKeys.namespacePrefix, invocationNamespace, fqn, Some(revision))\n    val inProgressPrefix = containerPrefix(ContainerKeys.inProgressPrefix, invocationNamespace, fqn, Some(revision))\n    val warmedPrefix = containerPrefix(ContainerKeys.warmedPrefix, invocationNamespace, fqn, Some(revision))\n    for {\n      namespaceCount <- etcdClient.getCount(namespacePrefix)\n      inProgressCount <- etcdClient.getCount(inProgressPrefix)\n      warmedCount <- etcdClient.getCount(warmedPrefix)\n    } yield {\n      namespaceCount + inProgressCount + warmedCount\n    }\n  }\n\n  /** Initialize message consumers */\n  private val msgProvider = SpiLoader.get[MessagingProvider]\n  //number of peeked messages - increasing the concurrentPeekFactor improves concurrent usage, but adds risk for message loss in case of crash\n  private val maxPeek = loadConfigOrThrow[Int](ConfigKeys.containerCreationMaxPeek)\n  private var consumer: Option[ContainerMessageConsumer] = Some(\n    new ContainerMessageConsumer(\n      instance,\n      pool,\n      entityStore,\n      cfg,\n      msgProvider,\n      longPollDuration = 1.second,\n      maxPeek,\n      sendAckToScheduler))\n\n  override def enable(): String = {\n    invokerHealthManager ! Enable\n    pool ! Enable\n    warmUp()\n    s\"${instance.toString} is now enabled.\"\n  }\n\n  override def disable(): String = {\n    invokerHealthManager ! GracefulShutdown\n    pool ! GracefulShutdown\n    warmUpWatcher.foreach(_.close())\n    warmUpWatcher = None\n    s\"${instance.toString} is now disabled.\"\n  }\n\n  override def getPoolState(): Future[Either[NotSupportedPoolState, TotalContainerPoolState]] = {\n    implicit val timeout: Timeout = 5.seconds\n    (pool ? GetState).mapTo[TotalContainerPoolState].map(Right(_))\n  }\n\n  override def isEnabled(): String = {\n    InvokerEnabled(warmUpWatcher.nonEmpty).serialize()\n  }\n\n  override def backfillPrewarm(): String = {\n    pool ! AdjustPrewarmedContainer\n    \"backfilling prewarm container\"\n  }\n\n  private val warmUpFetchRequest = FetchRequest(\n    TransactionId(TransactionId.generateTid()).serialize,\n    InvokerHealthManager.healthActionIdentity.namespace.name.asString,\n    WarmUp.warmUpAction.serialize,\n    DocRevision.empty.serialize) // a warm up fetch request which contains nothing\n\n  // warm up grpc connection with scheduler\n  private def warmUpScheduler(scheduler: SchedulerEndpoints) = {\n    val setting =\n      GrpcClientSettings\n        .connectToServiceAt(scheduler.host, scheduler.rpcPort)\n        .withTls(grpcConfig.tls)\n    val client = ActivationServiceClient(setting)\n    client.fetchActivation(warmUpFetchRequest).andThen {\n      case _ =>\n        logging.info(this, s\"Warmed up scheduler $scheduler\")\n        client.close()\n    }\n  }\n\n  private def warmUp(): Unit = {\n    implicit val transId = TransactionId.warmUp\n    if (warmUpWatcher.isEmpty)\n      warmUpWatcher = Some(etcdClient.watch(SchedulerKeys.prefix, true) { res: WatchUpdate =>\n        res.getEvents.asScala.foreach {\n          event =>\n            event.getType match {\n              case EventType.DELETE =>\n                val key = event.getPrevKv.getKey\n                warmedSchedulers.remove(key)\n              case EventType.PUT =>\n                val key = event.getKv.getKey\n                val value = event.getKv.getValue\n                SchedulerStates\n                  .parse(value)\n                  .map { state =>\n                    // warm up new scheduler\n                    warmedSchedulers.getOrElseUpdate(key, {\n                      logging.info(this, s\"Warm up scheduler ${state.sid}\")\n                      warmUpScheduler(state.endpoints)\n                      value\n                    })\n                  }\n              case _ =>\n            }\n        }\n      })\n\n    etcdClient.getPrefix(SchedulerKeys.prefix).map { res =>\n      res.getKvsList.asScala.map { kv =>\n        val scheduler = kv.getKey\n        warmedSchedulers.getOrElseUpdate(\n          scheduler, {\n            logging.info(this, s\"Warm up scheduler $scheduler\")\n            SchedulerStates\n              .parse(kv.getValue)\n              .map { state =>\n                warmUpScheduler(state.endpoints)\n              }\n              .recover {\n                case t =>\n                  logging.error(this, s\"Unexpected error $t\")\n              }\n\n            kv.getValue\n          })\n\n      }\n    }\n  }\n  warmUp()\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/invoker/FPCInvokerServer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.http.BasicRasService\nimport org.apache.openwhisk.http.CorsSettings.RespondWithServerCorsHeaders\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport pureconfig.loadConfigOrThrow\nimport spray.json.PrettyPrinter\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.concurrent.ExecutionContext\n\n/**\n * Implements web server to handle certain REST API calls.\n */\nclass FPCInvokerServer(val invoker: InvokerCore, systemUsername: String, systemPassword: String)(\n  implicit val ec: ExecutionContext,\n  val actorSystem: ActorSystem,\n  val logger: Logging)\n    extends BasicRasService\n    with RespondWithServerCorsHeaders {\n\n  /** Pretty print JSON response. */\n  implicit val jsonPrettyResponsePrinter = PrettyPrinter\n\n  override def routes(implicit transid: TransactionId): Route = {\n    super.routes ~ sendCorsHeaders {\n      options {\n        complete(OK)\n      } ~ extractCredentials {\n        case Some(BasicHttpCredentials(username, password))\n            if username == systemUsername && password == systemPassword =>\n          (path(\"enable\") & post) {\n            complete(invoker.enable())\n          } ~ (path(\"disable\") & post) {\n            complete(invoker.disable())\n          } ~ (path(\"isEnabled\") & get) {\n            complete(invoker.isEnabled())\n          } ~ (pathPrefix(\"pool\") & get) {\n            pathEndOrSingleSlash {\n              complete {\n                invoker.getPoolState().map {\n                  case Right(poolState) =>\n                    poolState.serialize()\n                  case Left(value) =>\n                    value.serialize()\n                }\n              }\n            } ~ (path(\"count\") & get) {\n              complete {\n                invoker.getPoolState().map {\n                  case Right(poolState) =>\n                    (poolState.busyPool.total + poolState.pausedPool.total + poolState.inProgressCount).toJson.compactPrint\n                  case Left(value) =>\n                    value.serialize()\n                }\n              }\n            }\n          }\n        case _ => terminate(StatusCodes.Unauthorized)\n      }\n    }\n  }\n}\n\nobject FPCInvokerServer extends InvokerServerProvider {\n\n  private val invokerUsername = loadConfigOrThrow[String](ConfigKeys.whiskInvokerUsername)\n  private val invokerPassword = loadConfigOrThrow[String](ConfigKeys.whiskInvokerPassword)\n\n  override def instance(\n    invoker: InvokerCore)(implicit ec: ExecutionContext, actorSystem: ActorSystem, logger: Logging): BasicRasService =\n    new FPCInvokerServer(invoker, invokerUsername, invokerPassword)\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/invoker/InstanceIdAssigner.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker\n\nimport org.apache.curator.framework.CuratorFrameworkFactory\nimport org.apache.curator.framework.recipes.shared.SharedCount\nimport org.apache.curator.retry.RetryUntilElapsed\nimport org.apache.openwhisk.common.Logging\n\nimport scala.collection.JavaConverters._\n\n/**\n * Computes the instanceId for invoker\n *\n * @param connectionString zooKeeper connection string\n */\nprivate[invoker] class InstanceIdAssigner(connectionString: String)(implicit logger: Logging) {\n\n  def setAndGetId(name: String, overwriteId: Option[Int] = None): Int = {\n    logger.info(this, s\"invokerReg: creating zkClient to $connectionString\")\n    val retryPolicy = new RetryUntilElapsed(5000, 500) // retry at 500ms intervals until 5 seconds have elapsed\n    val zkClient = CuratorFrameworkFactory.newClient(connectionString, retryPolicy)\n    zkClient.start()\n    zkClient.blockUntilConnected()\n    logger.info(this, \"invokerReg: connected to zookeeper\")\n\n    val rootPath = \"/invokers/idAssignment/mapping\"\n    val myIdPath = s\"$rootPath/$name\"\n    val assignedId = overwriteId\n      .map(newId => {\n        val invokers = zkClient.getChildren.forPath(rootPath).asScala\n\n        if (invokers.size < newId)\n          throw new IllegalArgumentException(s\"invokerReg: cannot assign $newId to $name: not enough invokers\")\n\n        //check if the invokerId already exists for another unique name and delete if it does\n        invokers\n          .map(uniqueName => {\n            val idPath = s\"$rootPath/$uniqueName\"\n            (idPath, BigInt(zkClient.getData.forPath(idPath)).intValue)\n          })\n          .find(_._2 == newId)\n          .map(id => zkClient.delete().forPath(id._1))\n\n        zkClient.create().orSetData().forPath(myIdPath, BigInt(newId).toByteArray)\n\n        logger.info(this, s\"invokerReg: invoker $name was assigned invokerId $newId\")\n        newId\n      })\n      .getOrElse({\n        Option(zkClient.checkExists().forPath(myIdPath)) match {\n          case None =>\n            // path doesn't exist -> no previous mapping for this invoker\n            logger.info(this, s\"invokerReg: no prior assignment of id for invoker $name\")\n            val idCounter = new SharedCount(zkClient, \"/invokers/idAssignment/counter\", 0)\n            idCounter.start()\n\n            def assignId(): Int = {\n              val current = idCounter.getVersionedValue()\n              val numInvokers = Option(zkClient.checkExists().forPath(rootPath))\n                .map(_ => zkClient.getChildren.forPath(rootPath).size())\n                .getOrElse(0)\n              if (idCounter.trySetCount(current, numInvokers + 1)) {\n                numInvokers\n              } else {\n                assignId()\n              }\n            }\n\n            val newId = assignId()\n            idCounter.close()\n            zkClient.create().creatingParentContainersIfNeeded().forPath(myIdPath, BigInt(newId).toByteArray)\n            logger.info(this, s\"invokerReg: invoker $name was assigned invokerId $newId\")\n            newId\n\n          case Some(_) =>\n            // path already exists -> there is a previous mapping for this invoker we should use\n            val rawOldId = zkClient.getData.forPath(myIdPath)\n            val oldId = BigInt(rawOldId).intValue\n            logger.info(this, s\"invokerReg: invoker $name was assigned its previous invokerId $oldId\")\n            oldId\n        }\n      })\n\n    zkClient.close()\n    assignedId\n  }\n\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/invoker/Invoker.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.{ActorSystem, CoordinatedShutdown}\nimport com.typesafe.config.ConfigValueFactory\nimport kamon.Kamon\nimport org.apache.openwhisk.common.Https.HttpsConfig\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.WhiskConfig._\nimport org.apache.openwhisk.core.connector.{MessageProducer, MessagingProvider}\nimport org.apache.openwhisk.core.containerpool.v2.{NotSupportedPoolState, TotalContainerPoolState}\nimport org.apache.openwhisk.core.containerpool.{Container, ContainerPoolConfig}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.http.{BasicHttpService, BasicRasService}\nimport org.apache.openwhisk.spi.{Spi, SpiLoader}\nimport org.apache.openwhisk.utils.ExecutionContextFactory\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext, Future}\nimport scala.util.Try\n\ncase class CmdLineArgs(uniqueName: Option[String] = None,\n                       id: Option[Int] = None,\n                       displayedName: Option[String] = None,\n                       overwriteId: Option[Int] = None)\n\nobject Invoker {\n\n  /**\n   * Collect logs after the activation has finished.\n   *\n   * This method is called after an activation has finished. The logs gathered here are stored along the activation\n   * record in the database.\n   *\n   * @param transid transaction the activation ran in\n   * @param user the user who ran the activation\n   * @param activation the activation record\n   * @param container container used by the activation\n   * @param action action that was activated\n   * @return logs for the given activation\n   */\n  trait LogsCollector {\n    def logsToBeCollected(action: ExecutableWhiskAction): Boolean = action.limits.logs.asMegaBytes != 0.MB\n\n    def apply(transid: TransactionId,\n              user: Identity,\n              activation: WhiskActivation,\n              container: Container,\n              action: ExecutableWhiskAction): Future[ActivationLogs]\n  }\n\n  protected val protocol = loadConfigOrThrow[String](\"whisk.invoker.protocol\")\n\n  val topicPrefix = loadConfigOrThrow[String](ConfigKeys.kafkaTopicsPrefix)\n\n  object InvokerEnabled extends DefaultJsonProtocol {\n    def parseJson(string: String) = Try(serdes.read(string.parseJson))\n    implicit val serdes = jsonFormat(InvokerEnabled.apply _, \"enabled\")\n  }\n\n  case class InvokerEnabled(isEnabled: Boolean) {\n    def serialize(): String = InvokerEnabled.serdes.write(this).compactPrint\n  }\n\n  /**\n   * An object which records the environment variables required for this component to run.\n   */\n  def requiredProperties =\n    Map(servicePort -> 8080.toString) ++\n      ExecManifest.requiredProperties ++\n      kafkaHosts ++\n      wskApiHost\n\n  def optionalProperties = zookeeperHosts.keys.toSet\n\n  def initKamon(instance: Int): Unit = {\n    // Replace the hostname of the invoker to the assigned id of the invoker.\n    val newKamonConfig = Kamon.config\n      .withValue(\"kamon.environment.host\", ConfigValueFactory.fromAnyRef(s\"invoker$instance\"))\n    Kamon.init(newKamonConfig)\n  }\n\n  def main(args: Array[String]): Unit = {\n    ConfigMXBean.register()\n    implicit val ec = ExecutionContextFactory.makeCachedThreadPoolExecutionContext()\n    implicit val actorSystem: ActorSystem =\n      ActorSystem(name = \"invoker-actor-system\", defaultExecutionContext = Some(ec))\n    implicit val logger = new PekkoLogging(org.apache.pekko.event.Logging.getLogger(actorSystem, this))\n    val poolConfig: ContainerPoolConfig = loadConfigOrThrow[ContainerPoolConfig](ConfigKeys.containerPool)\n    val limitConfig: IntraConcurrencyLimitConfig =\n      loadConfigOrThrow[IntraConcurrencyLimitConfig](ConfigKeys.concurrencyLimit)\n    val tags: Seq[String] = Some(loadConfigOrThrow[String](ConfigKeys.invokerResourceTags))\n      .map(_.trim())\n      .filter(_ != \"\")\n      .map(_.split(\",\").toSeq)\n      .getOrElse(Seq.empty[String])\n    val dedicatedNamespaces: Seq[String] = Some(loadConfigOrThrow[String](ConfigKeys.invokerDedicatedNamespaces))\n      .map(_.trim())\n      .filter(_ != \"\")\n      .map(_.split(\",\").toSeq)\n      .getOrElse(Seq.empty[String])\n\n    logger.info(this, s\"invoker tags: (${tags.mkString(\", \")})\")\n    // Prepare Kamon shutdown\n    CoordinatedShutdown(actorSystem).addTask(CoordinatedShutdown.PhaseActorSystemTerminate, \"shutdownKamon\") { () =>\n      logger.info(this, s\"Shutting down Kamon with coordinated shutdown\")\n      Kamon.stopModules().map(_ => Done)\n    }\n\n    // load values for the required properties from the environment\n    implicit val config = new WhiskConfig(requiredProperties, optionalProperties)\n\n    def abort(message: String) = {\n      logger.error(this, message)(TransactionId.invoker)\n      actorSystem.terminate()\n      Await.result(actorSystem.whenTerminated, 30.seconds)\n      sys.exit(1)\n    }\n\n    if (!config.isValid) {\n      abort(\"Bad configuration, cannot start.\")\n    }\n\n    val execManifest = ExecManifest.initialize(config)\n    if (execManifest.isFailure) {\n      logger.error(this, s\"Invalid runtimes manifest: ${execManifest.failed.get}\")\n      abort(\"Bad configuration, cannot start.\")\n    }\n\n    /** Returns Some(s) if the string is not empty with trimmed whitespace, None otherwise. */\n    def nonEmptyString(s: String): Option[String] = {\n      val trimmed = s.trim\n      if (trimmed.nonEmpty) Some(trimmed) else None\n    }\n\n    // process command line arguments\n    // We accept the command line grammar of:\n    // Usage: invoker [options] [<proposedInvokerId>]\n    //    --uniqueName <value>   a unique name to dynamically assign Kafka topics from Zookeeper\n    //    --displayedName <value> a name to identify this invoker via invoker health protocol\n    //    --id <value>     proposed invokerId\n    //    --overwriteId <value> proposed invokerId to re-write with uniqueName in Zookeeper,\n    //    DO NOT USE overwriteId unless sure invokerId does not exist for other uniqueName\n    def parse(ls: List[String], c: CmdLineArgs): CmdLineArgs = {\n      ls match {\n        case \"--uniqueName\" :: uniqueName :: tail =>\n          parse(tail, c.copy(uniqueName = nonEmptyString(uniqueName)))\n        case \"--displayedName\" :: displayedName :: tail =>\n          parse(tail, c.copy(displayedName = nonEmptyString(displayedName)))\n        case \"--id\" :: id :: tail if Try(id.toInt).isSuccess =>\n          parse(tail, c.copy(id = Some(id.toInt)))\n        case \"--overwriteId\" :: overwriteId :: tail if Try(overwriteId.toInt).isSuccess =>\n          parse(tail, c.copy(overwriteId = Some(overwriteId.toInt)))\n        case Nil => c\n        case _   => abort(s\"Error processing command line arguments $ls\")\n      }\n    }\n    val cmdLineArgs = parse(args.toList, CmdLineArgs())\n    logger.info(this, \"Command line arguments parsed to yield \" + cmdLineArgs)\n\n    val assignedInvokerId = cmdLineArgs match {\n      // --id is defined with a valid value, use this id directly.\n      case CmdLineArgs(_, Some(id), _, _) =>\n        logger.info(this, s\"invokerReg: using proposedInvokerId $id\")\n        id\n\n      // --uniqueName is defined with a valid value, id is empty, assign an id via zookeeper\n      case CmdLineArgs(Some(unique), None, _, overwriteId) =>\n        if (config.zookeeperHosts.startsWith(\":\") || config.zookeeperHosts.endsWith(\":\") ||\n            config.zookeeperHosts.equals(\"\")) {\n          abort(s\"Must provide valid zookeeper host and port to use dynamicId assignment (${config.zookeeperHosts})\")\n        }\n        new InstanceIdAssigner(config.zookeeperHosts).setAndGetId(unique, overwriteId)\n\n      case _ => abort(s\"Either --id or --uniqueName must be configured with correct values\")\n    }\n\n    initKamon(assignedInvokerId)\n\n    val topicBaseName = \"invoker\"\n    val topicName = topicPrefix + topicBaseName + assignedInvokerId\n\n    val maxMessageBytes = Some(ActivationEntityLimit.MAX_ACTIVATION_LIMIT)\n    val invokerInstance =\n      InvokerInstanceId(\n        assignedInvokerId,\n        cmdLineArgs.uniqueName,\n        cmdLineArgs.displayedName,\n        poolConfig.userMemory,\n        None,\n        tags,\n        dedicatedNamespaces)\n\n    val msgProvider = SpiLoader.get[MessagingProvider]\n    if (msgProvider\n          .ensureTopic(config, topic = topicName, topicConfig = topicBaseName, maxMessageBytes = maxMessageBytes)\n          .isFailure) {\n      abort(s\"failure during msgProvider.ensureTopic for topic $topicName\")\n    }\n\n    val producer = msgProvider.getProducer(config, Some(ActivationEntityLimit.MAX_ACTIVATION_LIMIT))\n    val invoker = try {\n      SpiLoader.get[InvokerProvider].instance(config, invokerInstance, producer, poolConfig, limitConfig)\n    } catch {\n      case e: Exception => abort(s\"Failed to initialize reactive invoker: ${e.getMessage}\")\n    }\n\n    val port = config.servicePort.toInt\n    val httpsConfig =\n      if (Invoker.protocol == \"https\") Some(loadConfigOrThrow[HttpsConfig](\"whisk.invoker.https\")) else None\n\n    val invokerServer = SpiLoader.get[InvokerServerProvider].instance(invoker)\n    BasicHttpService.startHttpService(invokerServer.route, port, httpsConfig)(actorSystem)\n  }\n}\n\n/**\n * An Spi for providing invoker implementation.\n */\ntrait InvokerProvider extends Spi {\n  def instance(\n    config: WhiskConfig,\n    instance: InvokerInstanceId,\n    producer: MessageProducer,\n    poolConfig: ContainerPoolConfig,\n    limitsConfig: IntraConcurrencyLimitConfig)(implicit actorSystem: ActorSystem, logging: Logging): InvokerCore\n}\n\n// this trait can be used to add common implementation\ntrait InvokerCore {\n  def enable(): String\n  def disable(): String\n  def isEnabled(): String\n  def backfillPrewarm(): String\n  def getPoolState(): Future[Either[NotSupportedPoolState, TotalContainerPoolState]]\n}\n\n/**\n * An Spi for providing RestAPI implementation for invoker.\n * The given invoker may require corresponding RestAPI implementation.\n */\ntrait InvokerServerProvider extends Spi {\n  def instance(\n    invoker: InvokerCore)(implicit ec: ExecutionContext, actorSystem: ActorSystem, logger: Logging): BasicRasService\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/invoker/InvokerReactive.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker\n\nimport java.nio.charset.StandardCharsets\nimport java.time.Instant\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.{ActorRef, ActorRefFactory, ActorSystem, CoordinatedShutdown, Props}\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.common.tracing.WhiskTracerProvider\nimport org.apache.openwhisk.core.ack.{MessagingActiveAck, UserEventSender}\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.containerpool.logging.LogStoreProvider\nimport org.apache.openwhisk.core.containerpool.v2.{NotSupportedPoolState, TotalContainerPoolState}\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.invoker.Invoker.InvokerEnabled\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.spi.SpiLoader\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success}\n\nobject InvokerReactive extends InvokerProvider {\n  override def instance(\n    config: WhiskConfig,\n    instance: InvokerInstanceId,\n    producer: MessageProducer,\n    poolConfig: ContainerPoolConfig,\n    limitsConfig: IntraConcurrencyLimitConfig)(implicit actorSystem: ActorSystem, logging: Logging): InvokerCore =\n    new InvokerReactive(config, instance, producer, poolConfig, limitsConfig)\n}\n\nclass InvokerReactive(\n  config: WhiskConfig,\n  instance: InvokerInstanceId,\n  producer: MessageProducer,\n  poolConfig: ContainerPoolConfig = loadConfigOrThrow[ContainerPoolConfig](ConfigKeys.containerPool),\n  limitsConfig: IntraConcurrencyLimitConfig = loadConfigOrThrow[IntraConcurrencyLimitConfig](\n    ConfigKeys.concurrencyLimit))(implicit actorSystem: ActorSystem, logging: Logging)\n    extends InvokerCore {\n\n  implicit val ec: ExecutionContext = actorSystem.dispatcher\n  implicit val cfg: WhiskConfig = config\n\n  private val logsProvider = SpiLoader.get[LogStoreProvider].instance(actorSystem)\n  logging.info(this, s\"LogStoreProvider: ${logsProvider.getClass}\")\n\n  /**\n   * Factory used by the ContainerProxy to physically create a new container.\n   *\n   * Create and initialize the container factory before kicking off any other\n   * task or actor because further operation does not make sense if something\n   * goes wrong here. Initialization will throw an exception upon failure.\n   */\n  private val containerFactory =\n    SpiLoader\n      .get[ContainerFactoryProvider]\n      .instance(\n        actorSystem,\n        logging,\n        config,\n        instance,\n        Map(\n          \"--cap-drop\" -> Set(\"NET_RAW\", \"NET_ADMIN\"),\n          \"--ulimit\" -> Set(\"nofile=1024:1024\"),\n          \"--pids-limit\" -> Set(\"1024\")) ++ logsProvider.containerParameters)\n  containerFactory.init()\n\n  CoordinatedShutdown(actorSystem)\n    .addTask(CoordinatedShutdown.PhaseBeforeActorSystemTerminate, \"cleanup runtime containers\") { () =>\n      containerFactory.cleanup()\n      Future.successful(Done)\n    }\n\n  /** Initialize needed databases */\n  private val entityStore = WhiskEntityStore.datastore()\n  private val activationStore =\n    SpiLoader.get[ActivationStoreProvider].instance(actorSystem, logging)\n\n  private val authStore = WhiskAuthStore.datastore()\n\n  private val namespaceBlacklist = new NamespaceBlacklist(authStore)\n\n  Scheduler.scheduleWaitAtMost(loadConfigOrThrow[NamespaceBlacklistConfig](ConfigKeys.blacklist).pollInterval) { () =>\n    logging.debug(this, \"running background job to update blacklist\")\n    namespaceBlacklist.refreshBlacklist()(ec, TransactionId.invoker).andThen {\n      case Success(set) => logging.info(this, s\"updated blacklist to ${set.size} entries\")\n      case Failure(t)   => logging.error(this, s\"error on updating the blacklist: ${t.getMessage}\")\n    }\n  }\n\n  /** Initialize message consumers */\n  private val topic = s\"${Invoker.topicPrefix}invoker${instance.toInt}\"\n  private val maximumContainers = (poolConfig.userMemory / MemoryLimit.MIN_MEMORY).toInt\n  private val msgProvider = SpiLoader.get[MessagingProvider]\n\n  //number of peeked messages - increasing the concurrentPeekFactor improves concurrent usage, but adds risk for message loss in case of crash\n  private val maxPeek =\n    math.max(maximumContainers, (maximumContainers * limitsConfig.max * poolConfig.concurrentPeekFactor).toInt)\n\n  private val consumer =\n    msgProvider.getConsumer(config, topic, topic, maxPeek, maxPollInterval = TimeLimit.MAX_DURATION + 1.minute)\n\n  private val activationFeed = actorSystem.actorOf(Props {\n    new MessageFeed(\"activation\", logging, consumer, maxPeek, 1.second, processActivationMessage)\n  })\n\n  private val ack = {\n    val sender = if (UserEvents.enabled) Some(new UserEventSender(producer)) else None\n    new MessagingActiveAck(producer, instance, sender)\n  }\n\n  private val collectLogs = new LogStoreCollector(logsProvider)\n\n  /** Stores an activation in the database. */\n  private val store = (tid: TransactionId, activation: WhiskActivation, isBlocking: Boolean, context: UserContext) => {\n    implicit val transid: TransactionId = tid\n    activationStore.storeAfterCheck(activation, isBlocking, None, None, context)(tid, notifier = None, logging)\n  }\n\n  /** Creates a ContainerProxy Actor when being called. */\n  private val childFactory = (f: ActorRefFactory) =>\n    f.actorOf(\n      ContainerProxy\n        .props(containerFactory.createContainer, ack, store, collectLogs, instance, poolConfig))\n\n  val prewarmingConfigs: List[PrewarmingConfig] = {\n    ExecManifest.runtimesManifest.stemcells.flatMap {\n      case (mf, cells) =>\n        cells.map { cell =>\n          PrewarmingConfig(cell.initialCount, new CodeExecAsString(mf, \"\", None), cell.memory, cell.reactive)\n        }\n    }.toList\n  }\n\n  private val pool =\n    actorSystem.actorOf(ContainerPool.props(childFactory, poolConfig, activationFeed, prewarmingConfigs))\n\n  def handleActivationMessage(msg: ActivationMessage)(implicit transid: TransactionId): Future[Unit] = {\n    val namespace = msg.action.path\n    val name = msg.action.name\n    val actionid = FullyQualifiedEntityName(namespace, name).toDocId.asDocInfo(msg.revision)\n    val subject = msg.user.subject\n\n    logging.debug(this, s\"${actionid.id} $subject ${msg.activationId}\")\n\n    // caching is enabled since actions have revision id and an updated\n    // action will not hit in the cache due to change in the revision id;\n    // if the doc revision is missing, then bypass cache\n    if (actionid.rev == DocRevision.empty) logging.warn(this, s\"revision was not provided for ${actionid.id}\")\n\n    WhiskAction\n      .get(entityStore, actionid.id, actionid.rev, fromCache = actionid.rev != DocRevision.empty)\n      .flatMap(action => {\n        // action that exceed the limit cannot be executed.\n        action.limits.checkLimits(msg.user)\n        action.toExecutableWhiskAction match {\n          case Some(executable) =>\n            pool ! Run(executable, msg)\n            Future.successful(())\n          case None =>\n            logging.error(this, s\"non-executable action reached the invoker ${action.fullyQualifiedName(false)}\")\n            Future.failed(new IllegalStateException(\"non-executable action reached the invoker\"))\n        }\n      })\n      .recoverWith {\n        case DocumentRevisionMismatchException(_) =>\n          // if revision is mismatched, the action may have been updated,\n          // so try again with the latest code\n          handleActivationMessage(msg.copy(revision = DocRevision.empty))\n        case t =>\n          val response = t match {\n            case _: NoDocumentException =>\n              ActivationResponse.applicationError(Messages.actionRemovedWhileInvoking)\n            case e: ActionLimitsException =>\n              ActivationResponse.applicationError(e.getMessage) // return generated failed message\n            case _: DocumentTypeMismatchException | _: DocumentUnreadable =>\n              ActivationResponse.whiskError(Messages.actionMismatchWhileInvoking)\n            case _ =>\n              ActivationResponse.whiskError(Messages.actionFetchErrorWhileInvoking)\n          }\n          activationFeed ! MessageFeed.Processed\n\n          val activation = generateFallbackActivation(msg, response)\n          ack(\n            msg.transid,\n            activation,\n            msg.blocking,\n            msg.rootControllerIndex,\n            msg.user.namespace.uuid,\n            CombinedCompletionAndResultMessage(transid, activation, instance))\n\n          store(msg.transid, activation, msg.blocking, UserContext(msg.user))\n          Future.successful(())\n      }\n  }\n\n  /** Is called when an ActivationMessage is read from Kafka */\n  def processActivationMessage(bytes: Array[Byte]): Future[Unit] = {\n    Future(ActivationMessage.parse(new String(bytes, StandardCharsets.UTF_8)))\n      .flatMap(Future.fromTry)\n      .flatMap { msg =>\n        // The message has been parsed correctly, thus the following code needs to *always* produce at least an\n        // active-ack.\n\n        implicit val transid: TransactionId = msg.transid\n\n        //set trace context to continue tracing\n        WhiskTracerProvider.tracer.setTraceContext(transid, msg.traceContext)\n\n        if (!namespaceBlacklist.isBlacklisted(msg.user)) {\n          val start = transid.started(this, LoggingMarkers.INVOKER_ACTIVATION, logLevel = InfoLevel)\n          handleActivationMessage(msg)\n        } else {\n          // Iff the current namespace is blacklisted, an active-ack is only produced to keep the loadbalancer protocol\n          // Due to the protective nature of the blacklist, a database entry is not written.\n          activationFeed ! MessageFeed.Processed\n\n          val activation =\n            generateFallbackActivation(msg, ActivationResponse.applicationError(Messages.namespacesBlacklisted))\n          ack(\n            msg.transid,\n            activation,\n            false,\n            msg.rootControllerIndex,\n            msg.user.namespace.uuid,\n            CombinedCompletionAndResultMessage(transid, activation, instance))\n\n          logging.warn(this, s\"namespace ${msg.user.namespace.name} was blocked in invoker.\")\n          Future.successful(())\n        }\n      }\n      .recoverWith {\n        case t =>\n          // Iff everything above failed, we have a terminal error at hand. Either the message failed\n          // to deserialize, or something threw an error where it is not expected to throw.\n          activationFeed ! MessageFeed.Processed\n          logging.error(this, s\"terminal failure while processing message: $t\")\n          Future.successful(())\n      }\n  }\n\n  /**\n   * Generates an activation with zero runtime. Usually used for error cases.\n   *\n   * Set the kind annotation to `Exec.UNKNOWN` since it is not known to the invoker because the action fetch failed.\n   */\n  private def generateFallbackActivation(msg: ActivationMessage, response: ActivationResponse): WhiskActivation = {\n    val now = Instant.now\n    val causedBy = if (msg.causedBySequence) {\n      Some(Parameters(WhiskActivation.causedByAnnotation, JsString(Exec.SEQUENCE)))\n    } else None\n\n    WhiskActivation(\n      activationId = msg.activationId,\n      namespace = msg.user.namespace.name.toPath,\n      subject = msg.user.subject,\n      cause = msg.cause,\n      name = msg.action.name,\n      version = msg.action.version.getOrElse(SemVer()),\n      start = now,\n      end = now,\n      duration = Some(0),\n      response = response,\n      annotations = {\n        Parameters(WhiskActivation.pathAnnotation, JsString(msg.action.copy(version = None).asString)) ++\n          Parameters(WhiskActivation.kindAnnotation, JsString(Exec.UNKNOWN)) ++ causedBy\n      })\n  }\n\n  private val healthProducer = msgProvider.getProducer(config)\n\n  private def getHealthScheduler: ActorRef =\n    Scheduler.scheduleWaitAtMost(1.seconds)(() => pingController(isEnabled = true))\n\n  private def pingController(isEnabled: Boolean) = {\n    healthProducer.send(s\"${Invoker.topicPrefix}health\", PingMessage(instance, isEnabled = Some(isEnabled))).andThen {\n      case Failure(t) => logging.error(this, s\"failed to ping the controller: $t\")\n    }\n  }\n\n  private var healthScheduler: Option[ActorRef] = Some(getHealthScheduler)\n\n  override def enable(): String = {\n    if (healthScheduler.isEmpty) {\n      healthScheduler = Some(getHealthScheduler)\n      s\"${instance.toString} is now enabled.\"\n    } else {\n      s\"${instance.toString} is already enabled.\"\n    }\n  }\n\n  override def disable(): String = {\n    pingController(isEnabled = false)\n    if (healthScheduler.nonEmpty) {\n      actorSystem.stop(healthScheduler.get)\n      healthScheduler = None\n      s\"${instance.toString} is now disabled.\"\n    } else {\n      s\"${instance.toString} is already disabled.\"\n    }\n  }\n\n  override def isEnabled(): String = {\n    InvokerEnabled(healthScheduler.nonEmpty).serialize()\n  }\n\n  override def backfillPrewarm(): String = {\n    \"not supported\"\n  }\n\n  override def getPoolState(): Future[Either[NotSupportedPoolState, TotalContainerPoolState]] = {\n    Future.successful(Left(NotSupportedPoolState()))\n  }\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/invoker/LogStoreCollector.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.Container\nimport org.apache.openwhisk.core.containerpool.logging.LogStore\nimport org.apache.openwhisk.core.entity.{ActivationLogs, ExecutableWhiskAction, Identity, WhiskActivation}\nimport org.apache.openwhisk.core.invoker.Invoker.LogsCollector\n\nimport scala.concurrent.Future\n\nclass LogStoreCollector(store: LogStore) extends LogsCollector {\n\n  override def logsToBeCollected(action: ExecutableWhiskAction): Boolean =\n    super.logsToBeCollected(action) && !store.logCollectionOutOfBand\n\n  override def apply(transid: TransactionId,\n                     user: Identity,\n                     activation: WhiskActivation,\n                     container: Container,\n                     action: ExecutableWhiskAction): Future[ActivationLogs] =\n    store.collectLogs(transid, user, activation, container, action)\n}\n"
  },
  {
    "path": "core/invoker/src/main/scala/org/apache/openwhisk/core/invoker/NamespaceBlacklist.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.StaleParameter\nimport org.apache.openwhisk.core.entity.{Identity, View}\nimport org.apache.openwhisk.core.entity.types.AuthStore\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport spray.json.DefaultJsonProtocol._\n\nimport scala.concurrent.duration.FiniteDuration\n\n/**\n * The namespace blacklist gets all namespaces that are throttled to 0 or blocked from the database.\n *\n * The caller is responsible for periodically updating the blacklist with `refreshBlacklist`.\n *\n * @param authStore Subjects database with the limit-documents.\n */\nclass NamespaceBlacklist(authStore: AuthStore) {\n\n  private var blacklist: Set[String] = Set.empty\n\n  /**\n   * Check if the identity, who invoked the activation is in the blacklist.\n   *\n   * @param identity which invoked the action.\n   * @return whether or not the current identity is considered blacklisted\n   */\n  def isBlacklisted(identity: Identity): Boolean = blacklist.contains(identity.namespace.name.asString)\n\n  /** Refreshes the current blacklist from the database. */\n  /** Limit query parameter set to 0 for limitless record query. */\n  def refreshBlacklist()(implicit ec: ExecutionContext, tid: TransactionId): Future[Set[String]] = {\n    authStore\n      .query(\n        table = NamespaceBlacklist.view.name,\n        startKey = List.empty,\n        endKey = List.empty,\n        skip = 0,\n        limit = 0,\n        includeDocs = false,\n        descending = true,\n        reduce = false,\n        stale = StaleParameter.UpdateAfter)\n      .map(_.map(_.fields(\"key\").convertTo[String]).toSet)\n      .map { newBlacklist =>\n        blacklist = newBlacklist\n        newBlacklist\n      }\n  }\n}\n\nobject NamespaceBlacklist {\n  val view = View(\"namespaceThrottlings\", \"blockedNamespaces\")\n}\n\n/** Configuration relevant to the namespace blacklist */\ncase class NamespaceBlacklistConfig(pollInterval: FiniteDuration)\n"
  },
  {
    "path": "core/monitoring/user-events/.dockerignore",
    "content": "*\n!transformEnvironment.sh\n!init.sh\n!build/distributions\n!Dockerfile"
  },
  {
    "path": "core/monitoring/user-events/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nARG BASE=scala\nFROM ${BASE}\n\nENV UID=1001 \\\n    NOT_ROOT_USER=owuser\n\n# Copy app jars\nADD build/distributions/user-events.tar /\n\nCOPY init.sh /\nRUN chmod +x init.sh\n\nRUN useradd -m -u 1001 -d /home/${NOT_ROOT_USER} -s /bin/bash ${NOT_ROOT_USER}\nUSER ${NOT_ROOT_USER}\n\n# Prometheus port\nEXPOSE 9095\nCMD [\"./init.sh\", \"0\"]\n"
  },
  {
    "path": "core/monitoring/user-events/Dockerfile-debian",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM scala\n\nENV UID=1001 \\\n    NOT_ROOT_USER=owuser\n\n# Copy app jars\nADD build/distributions/user-events.tar /\n\nCOPY init.sh /\nRUN chmod +x init.sh\n\nRUN adduser --disabled-password --disabled-login --gecos '' --uid ${UID} --home /home/${NOT_ROOT_USER} ${NOT_ROOT_USER}\nUSER ${NOT_ROOT_USER}\n\n# Prometheus port\nEXPOSE 9095\nCMD [\"./init.sh\", \"0\"]\n"
  },
  {
    "path": "core/monitoring/user-events/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# ![OpenWhisk User Events](https://raw.githubusercontent.com/apache/openwhisk/master/core/monitoring/user-events/images/demo_landing.png)\n\n# OpenWhisk User Events\n\nThis service connects to `events` topic and publishes the events to various services like Prometheus, Datadog etc via Kamon. Refer to [user specific metrics][1] on how to enable them.\n\n\n## Local Run\n>First configure and run `openwhisk docker-compose` that can be found in the [openwhisk-tools][2] project.\n\n- Start service inside the cluster (on the same docker-compose network: `openwhisk_default`)\n- The service will be available on port `9095`\n- The endpoint for exposing the metrics for Prometheus can be found on `/metrics`.\n\n## Usage\n\nThe service needs the following env variables to be set\n\n- `KAFKA_HOSTS` - For local env it can be set to `172.17.0.1:9093`. When using [OpenWhisk Devtools][2] based setup use `kafka`\n- Namespaces can be removed from reports by listing them inside the `reference.conf` using the `whisk.user-events.ignored-namespaces` configuration.\ne.g:\n```\nwhisk {\n  user-events {\n    ignored-namespaces = [\"canary\",\"testing\"]\n  }\n}\n```\n- To rename metrics tags, use the below configuration. Currently, this configuration only applies to the Prometheus\nMetrics. For example, here `namespace` tag name will be replaced by `ow_namespace` in all metrics.\n\n```\nwhisk {\n  user-events {\n    rename-tags {\n      # rename/relabel prometheus metrics tags\n      \"namespace\" = \"ow_namespae\"\n     }\n  }\n}\n```\n\nIntegrations\n------------\n\n#### Prometheus\nThe docker container would run the service and expose the metrics in format required by [Prometheus][3] at `9095` port\n\n#### Grafana\nThe `Openwhisk - Action Performance Metrics` Grafana[4] dashboard is available on localhost port `3000` at this address:\nhttp://localhost:3000/d/Oew1lvymk/openwhisk-action-performance-metrics\n\nThe latest version of the dashboard can be found in the \"compose/dashboard/openwhisk_events.json\"\n\n[1]: https://github.com/apache/openwhisk/blob/master/docs/metrics.md#user-specific-metrics\n[2]: https://github.com/apache/openwhisk-devtools/tree/master/docker-compose\n[3]: https://hub.docker.com/r/prom/prometheus/\n[4]: https://hub.docker.com/r/grafana/grafana/\n"
  },
  {
    "path": "core/monitoring/user-events/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'application'\n    id 'eclipse'\n    id 'maven-publish'\n    id 'org.scoverage'\n    id 'scala'\n}\n\next.dockerImageName = 'user-events'\napply from: \"../../../gradle/docker.gradle\"\ndistDocker.dependsOn ':common:scala:distDocker', 'distTar'\n\nproject.archivesBaseName = \"openwhisk-user-events\"\n\nscoverage {\n    scoverageVersion.set(\"${gradle.scala.scoverageVersion}\")\n    scoverageScalaVersion.set(\"${gradle.scala.scoverageScalaVersion}\")\n}\n\ndependencies {\n    implementation \"org.scala-lang:scala-library:${gradle.scala.version}\"\n    implementation project(':common:scala')\n\n    implementation \"org.apache.pekko:pekko-connectors-kafka_${gradle.scala.depVersion}:${gradle.pekko_kafka.version}\"\n\n    implementation \"io.prometheus:simpleclient:0.6.0\"\n    implementation \"io.prometheus:simpleclient_common:0.6.0\"\n\n    testImplementation \"junit:junit:4.11\"\n    testImplementation \"org.scalatest:scalatest_${gradle.scala.depVersion}:3.2.14\"\n    testImplementation \"org.scalatestplus:junit-4-13_${gradle.scala.depVersion}:3.2.14.0\"\n    testImplementation \"io.github.embeddedkafka:embedded-kafka_${gradle.scala.depVersion}:3.9.1\"\n    constraints {\n        testImplementation(\"io.github.embeddedkafka:embedded-kafka_${gradle.scala.depVersion}:3.9.1\")\n        testImplementation(\"org.apache.kafka:kafka-clients:3.9.1\")\n    }\n    testImplementation (\"org.apache.zookeeper:zookeeper:3.9.3\") {\n        exclude group: 'org.slf4j'\n        exclude group: 'log4j'\n        exclude group: 'jline'\n    }\n    testImplementation \"org.apache.pekko:pekko-connectors-kafka-testkit_${gradle.scala.depVersion}:${gradle.pekko_kafka.version}\"\n    testImplementation \"org.apache.pekko:pekko-testkit_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    testImplementation \"org.apache.pekko:pekko-stream-testkit_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    testImplementation \"org.apache.pekko:pekko-http-testkit_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n}\n\nmainClassName = \"org.apache.openwhisk.core.monitoring.metrics.Main\"\n\ngradle.projectsEvaluated {\n    tasks.withType(Test) {\n        testLogging {\n            events \"passed\", \"skipped\", \"failed\"\n            showStandardStreams = true\n            exceptionFormat = 'full'\n        }\n    }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/compose/grafana/dashboards/global-metrics.json",
    "content": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"gnetId\": null,\n  \"graphTooltip\": 0,\n  \"iteration\": 1580164337194,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": true,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#d44a3a\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#299c46\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"description\": \"Total number of successful activations executed\",\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 4,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 4,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": false,\n        \"lineColor\": \"#9ac48a\",\n        \"show\": true\n      },\n      \"tableColumn\": \"Value\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_status{status=\\\"success\\\"}[${__range_s}s]))\",\n          \"format\": \"time_series\",\n          \"instant\": true,\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"0,1\",\n      \"title\": \"Successful Activations\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"total\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": true,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#3274D9\",\n        \"#3274D9\",\n        \"#3274D9\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 0,\n      \"description\": \"Total number of cold starts\",\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 4,\n        \"x\": 4,\n        \"y\": 0\n      },\n      \"id\": 6,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"#1F60C4\",\n        \"full\": false,\n        \"lineColor\": \"#8AB8FF\",\n        \"show\": true\n      },\n      \"tableColumn\": \"Value\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_coldStarts_total[${__range_s}s]))\",\n          \"format\": \"time_series\",\n          \"instant\": true,\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"1\",\n      \"title\": \"Cold starts\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": true,\n      \"colorPrefix\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#3274D9\",\n        \"#3274D9\",\n        \"#3274D9\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 0,\n      \"description\": \"Total number of error due to Runtime implementation\",\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 4,\n        \"x\": 8,\n        \"y\": 0\n      },\n      \"id\": 5,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"#1F60C4\",\n        \"full\": false,\n        \"lineColor\": \"#8AB8FF\",\n        \"show\": true\n      },\n      \"tableColumn\": \"Value\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_status{status=\\\"internal_error\\\"}[${__range_s}s]))\",\n          \"format\": \"time_series\",\n          \"instant\": true,\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"1\",\n      \"title\": \"System errors\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"folderId\": null,\n      \"gridPos\": {\n        \"h\": 3,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 0\n      },\n      \"headings\": true,\n      \"id\": 8,\n      \"limit\": 10,\n      \"links\": [],\n      \"query\": \"\",\n      \"recent\": false,\n      \"search\": true,\n      \"starred\": false,\n      \"tags\": [\n        \"openwhisk\"\n      ],\n      \"title\": \"Related Dashboards\",\n      \"type\": \"dashlist\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 3\n      },\n      \"id\": 2,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": false,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [\n        {\n          \"dashboard\": \"OpenWhisk - Top Namespaces\",\n          \"keepTime\": true,\n          \"title\": \"OpenWhisk - Top Namespaces\",\n          \"type\": \"dashboard\",\n          \"url\": \"/d/RnvlchiZk/openwhisk-top-namespaces\"\n        }\n      ],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_activations_total[$interval]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Activations [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"description\": \"\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 11\n      },\n      \"id\": 10,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"hideEmpty\": true,\n        \"hideZero\": true,\n        \"max\": false,\n        \"min\": false,\n        \"show\": false,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"rate(openwhisk_action_waitTime_seconds_sum[$interval]) * 1000 / rate(openwhisk_action_waitTime_seconds_count[$interval]) \",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{namespace}}/{{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Wait time [$interval]\",\n      \"tooltip\": {\n        \"shared\": false,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"ms\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"description\": \"\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 11\n      },\n      \"id\": 11,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"hideEmpty\": true,\n        \"hideZero\": true,\n        \"max\": false,\n        \"min\": false,\n        \"show\": false,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"rate(openwhisk_action_response_size_bytes_sum[$interval]) / rate(openwhisk_action_response_size_bytes_count[$interval]) \",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{namespace}}/{{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Response size [$interval]\",\n      \"tooltip\": {\n        \"shared\": false,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"decbytes\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 19\n      },\n      \"id\": 13,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(counter_invoker_containerStart_counter_total{containerState!=\\\"warmed\\\"}[$interval])) by (containerState)\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{containerState}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Non-warm container starts [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 19\n      },\n      \"id\": 14,\n      \"interval\": \"\",\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(counter_invoker_containerStart_counter_total{containerState=\\\"warmed\\\"}[$interval])) by (containerState)\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{ containerState }}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Warm container starts [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 27\n      },\n      \"id\": 16,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum by (kind, memory) (increase(counter_containerPool_prewarmExpired_counter_total[$interval]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{kind}} {{memory}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Expired Prewarms [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 27\n      },\n      \"id\": 17,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum by (kind, memory) (increase(counter_containerPool_prewarmColdstart_counter_total[$interval]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{kind}} {{memory}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Prewarm Coldstarts [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 35\n      },\n      \"id\": 19,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"gauge_containerPool_prewarmCount_counter\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{instance}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Prewarm Container Counts\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 35\n      },\n      \"id\": 20,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"gauge_containerPool_activeCount_counter\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{instance}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Active Container Counts\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    }\n  ],\n  \"refresh\": false,\n  \"schemaVersion\": 18,\n  \"style\": \"dark\",\n  \"tags\": [\n    \"openwhisk\"\n  ],\n  \"templating\": {\n    \"list\": [\n      {\n        \"auto\": true,\n        \"auto_count\": 30,\n        \"auto_min\": \"30s\",\n        \"current\": {\n          \"text\": \"auto\",\n          \"value\": \"$__auto_interval_interval\"\n        },\n        \"hide\": 2,\n        \"label\": null,\n        \"name\": \"interval\",\n        \"options\": [\n          {\n            \"selected\": true,\n            \"text\": \"auto\",\n            \"value\": \"$__auto_interval_interval\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"30s\",\n            \"value\": \"30s\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1m\",\n            \"value\": \"1m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"10m\",\n            \"value\": \"10m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"30m\",\n            \"value\": \"30m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1h\",\n            \"value\": \"1h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"6h\",\n            \"value\": \"6h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"12h\",\n            \"value\": \"12h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1d\",\n            \"value\": \"1d\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"7d\",\n            \"value\": \"7d\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"14d\",\n            \"value\": \"14d\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"30d\",\n            \"value\": \"30d\"\n          }\n        ],\n        \"query\": \"30s,1m,10m,30m,1h,6h,12h,1d,7d,14d,30d\",\n        \"refresh\": 2,\n        \"skipUrlSync\": false,\n        \"type\": \"interval\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-15m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ],\n    \"time_options\": [\n      \"5m\",\n      \"15m\",\n      \"1h\",\n      \"6h\",\n      \"12h\",\n      \"24h\",\n      \"2d\",\n      \"7d\",\n      \"30d\"\n    ]\n  },\n  \"timezone\": \"\",\n  \"title\": \"OpenWhisk - Global Metrics\",\n  \"uid\": \"Kw4jl2iZz\",\n  \"version\": 9\n}\n"
  },
  {
    "path": "core/monitoring/user-events/compose/grafana/dashboards/openwhisk_events.json",
    "content": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"description\": \"Action performance metrics available for the users of Openwhisk.\",\n  \"editable\": true,\n  \"gnetId\": 9564,\n  \"graphTooltip\": 0,\n  \"iteration\": 1584194893683,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 0,\n      \"description\": \"Total number of activation in the selected time interval\",\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 2,\n        \"w\": 5,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 28,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(249, 186, 143, 0.15)\",\n        \"full\": false,\n        \"lineColor\": \"#ef843c\",\n        \"show\": false\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_activations_total{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$__range]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"\",\n      \"title\": \"Total activations\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"100%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": true,\n      \"colorValue\": false,\n      \"colors\": [\n        \"rgba(212, 74, 58, 0)\",\n        \"#508642\",\n        \"#299c46\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 0,\n      \"description\": \"Total number of successful activations executed\",\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 2,\n        \"w\": 5,\n        \"x\": 5,\n        \"y\": 0\n      },\n      \"id\": 32,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(136, 253, 150, 0.18)\",\n        \"full\": false,\n        \"lineColor\": \"#7eb26d\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_status{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",status=\\\"success\\\",initiator=~\\\"$initiator\\\"}[$__range]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"1\",\n      \"title\": \"Successful activations\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": true,\n      \"colorPostfix\": false,\n      \"colorPrefix\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"rgba(41, 156, 70, 0)\",\n        \"#FA6400\",\n        \"#FA6400\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 0,\n      \"description\": \"Total number of application and developer errors: \\n\\n[application_error] =  action ran but there was an error and it was handled\\n\\n[action_developer_error] = action ran but failed to handle an error, or action did not run and failed to initialize\",\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 2,\n        \"w\": 4,\n        \"x\": 10,\n        \"y\": 0\n      },\n      \"id\": 34,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"#FF780A\",\n        \"full\": false,\n        \"lineColor\": \"#FFB357\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_status{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",status=~\\\"application_error|action_developer_error\\\",initiator=~\\\"$initiator\\\"}[$__range]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"1\",\n      \"title\": \"Development errors\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": true,\n      \"colorPostfix\": false,\n      \"colorPrefix\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"rgba(41, 156, 70, 0)\",\n        \"#e24d42\",\n        \"#e24d42\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 0,\n      \"description\": \"Total number of system activation errors: \\n\\n[whisk_internal_error] = internal system error\",\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 2,\n        \"w\": 5,\n        \"x\": 14,\n        \"y\": 0\n      },\n      \"id\": 39,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgb(243, 113, 104)\",\n        \"full\": false,\n        \"lineColor\": \"rgb(255, 194, 190)\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_status{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",status=~\\\"whisk_internal_error\\\",initiator=~\\\"$initiator\\\"}[$__range]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"1\",\n      \"title\": \"System errors\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": true,\n      \"colorValue\": false,\n      \"colors\": [\n        \"rgba(41, 156, 70, 0)\",\n        \"#1f78c1\",\n        \"#1f78c1\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 0,\n      \"description\": \"Total number of cold starts in the selected time interval\",\n      \"format\": \"none\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": false,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 2,\n        \"w\": 5,\n        \"x\": 19,\n        \"y\": 0\n      },\n      \"id\": 30,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(81, 149, 206, 0.48)\",\n        \"full\": false,\n        \"lineColor\": \"rgb(122, 181, 231)\",\n        \"show\": true\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_coldStarts_total{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$__range]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"1\",\n      \"title\": \"Cold starts\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 2\n      },\n      \"id\": 16,\n      \"panels\": [],\n      \"title\": \"General gauges\",\n      \"type\": \"row\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#d44a3a\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#299c46\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 1,\n      \"format\": \"percent\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": true,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 6,\n        \"x\": 0,\n        \"y\": 3\n      },\n      \"id\": 6,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": false,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": false\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_status{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",status=\\\"success\\\",initiator=~\\\"$initiator\\\"}[$__range])) * 100 / sum(increase(openwhisk_action_status{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\"}[$__range]))\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"refId\": \"A\",\n          \"target\": \"\"\n        }\n      ],\n      \"thresholds\": \"50,75,100\",\n      \"title\": \"Activation success rate\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#299c46\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#d44a3a\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 1,\n      \"format\": \"s\",\n      \"gauge\": {\n        \"maxValue\": 60,\n        \"minValue\": 0,\n        \"show\": true,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 6,\n        \"x\": 6,\n        \"y\": 3\n      },\n      \"id\": 8,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": false,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": false\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"max(rate(openwhisk_action_duration_seconds_sum{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$__range]) / rate(openwhisk_action_duration_seconds_count{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\"}[$__range]) > 0)\",\n          \"format\": \"time_series\",\n          \"instant\": false,\n          \"intervalFactor\": 1,\n          \"refId\": \"A\",\n          \"target\": \"\"\n        }\n      ],\n      \"thresholds\": \"20,40,60\",\n      \"title\": \"Action duration current\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"cacheTimeout\": null,\n      \"colorBackground\": false,\n      \"colorValue\": false,\n      \"colors\": [\n        \"#d44a3a\",\n        \"rgba(237, 129, 40, 0.89)\",\n        \"#299c46\"\n      ],\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 1,\n      \"format\": \"s\",\n      \"gauge\": {\n        \"maxValue\": 100,\n        \"minValue\": 0,\n        \"show\": true,\n        \"thresholdLabels\": false,\n        \"thresholdMarkers\": true\n      },\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 6,\n        \"x\": 12,\n        \"y\": 3\n      },\n      \"id\": 26,\n      \"interval\": null,\n      \"links\": [],\n      \"mappingType\": 1,\n      \"mappingTypes\": [\n        {\n          \"name\": \"value to text\",\n          \"value\": 1\n        },\n        {\n          \"name\": \"range to text\",\n          \"value\": 2\n        }\n      ],\n      \"maxDataPoints\": 100,\n      \"nullPointMode\": \"connected\",\n      \"nullText\": null,\n      \"postfix\": \"\",\n      \"postfixFontSize\": \"50%\",\n      \"prefix\": \"\",\n      \"prefixFontSize\": \"50%\",\n      \"rangeMaps\": [\n        {\n          \"from\": \"null\",\n          \"text\": \"N/A\",\n          \"to\": \"null\"\n        }\n      ],\n      \"sparkline\": {\n        \"fillColor\": \"rgba(31, 118, 189, 0.18)\",\n        \"full\": false,\n        \"lineColor\": \"rgb(31, 120, 193)\",\n        \"show\": false\n      },\n      \"tableColumn\": \"\",\n      \"targets\": [\n        {\n          \"expr\": \"max(rate(openwhisk_action_waitTime_seconds_sum{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$__range]) / rate(openwhisk_action_waitTime_seconds_count{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\"}[$__range]) > 0)\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": \"1000,2500,5000\",\n      \"title\": \"Action wait time current\",\n      \"type\": \"singlestat\",\n      \"valueFontSize\": \"80%\",\n      \"valueMaps\": [\n        {\n          \"op\": \"=\",\n          \"text\": \"N/A\",\n          \"value\": \"null\"\n        }\n      ],\n      \"valueName\": \"current\"\n    },\n    {\n      \"columns\": [\n        {\n          \"text\": \"Current\",\n          \"value\": \"current\"\n        }\n      ],\n      \"datasource\": \"Prometheus\",\n      \"fontSize\": \"100%\",\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 6,\n        \"x\": 18,\n        \"y\": 3\n      },\n      \"id\": 37,\n      \"links\": [],\n      \"pageSize\": null,\n      \"scroll\": true,\n      \"showHeader\": true,\n      \"sort\": {\n        \"col\": 0,\n        \"desc\": true\n      },\n      \"styles\": [\n        {\n          \"alias\": \"Action name\",\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"pattern\": \"Metric\",\n          \"type\": \"string\"\n        },\n        {\n          \"alias\": \"Memory\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 0,\n          \"mappingType\": 1,\n          \"pattern\": \"Current\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"decmbytes\"\n        },\n        {\n          \"alias\": \"\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"decimals\": 2,\n          \"pattern\": \"/.*/\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"short\"\n        }\n      ],\n      \"targets\": [\n        {\n          \"expr\": \"openwhisk_action_memory{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}\",\n          \"format\": \"time_series\",\n          \"instant\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Action memory\",\n      \"transform\": \"timeseries_aggregations\",\n      \"type\": \"table\"\n    },\n    {\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 9\n      },\n      \"id\": 14,\n      \"title\": \"Activation result graph\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 0,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 9,\n        \"x\": 0,\n        \"y\": 10\n      },\n      \"id\": 4,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_activations_total{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$interval])) by (action)\",\n          \"format\": \"time_series\",\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Activations [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"short\",\n          \"label\": \"activations\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 9,\n        \"y\": 10\n      },\n      \"id\": 18,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"hideEmpty\": true,\n        \"hideZero\": true,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_status{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",status=\\\"success\\\",initiator=~\\\"$initiator\\\"}[$interval])) by (action)\",\n          \"format\": \"time_series\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Activation success [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"short\",\n          \"label\": \"activations\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"decimals\": 1,\n      \"description\": \"Number of application, developer and internal errors: \\n\\n[application_error] =  action ran but there was an error and it was handled\\n\\n[action_developer_error] = action ran but failed to handle an error, or action did not run and failed to initialize\\n\\n[whisk_internal_error] = internal system error\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 7,\n        \"x\": 17,\n        \"y\": 10\n      },\n      \"id\": 20,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_status{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",status!=\\\"success\\\",initiator=~\\\"$initiator\\\"}[$interval])) by (action,status)\",\n          \"format\": \"time_series\",\n          \"instant\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{action}}: {{status}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Activation errors [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"short\",\n          \"label\": \"activations\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 9,\n        \"x\": 0,\n        \"y\": 16\n      },\n      \"id\": 22,\n      \"legend\": {\n        \"alignAsTable\": false,\n        \"avg\": false,\n        \"current\": false,\n        \"hideEmpty\": true,\n        \"hideZero\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"maxPerRow\": 4,\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"repeat\": null,\n      \"repeatDirection\": \"h\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"rate(openwhisk_action_response_size_bytes_sum{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$interval]) / rate(openwhisk_action_response_size_bytes_count{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$interval]) \",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Response size [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"decbytes\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"decimals\": null,\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": false\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"description\": \"Action statusCode defined by Action Developer.\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 6,\n        \"w\": 8,\n        \"x\": 9,\n        \"y\": 16\n      },\n      \"id\": 44,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(openwhisk_action_status_code{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",status!=\\\"success\\\",initiator=~\\\"$initiator\\\"}[$interval])) by (action,status_code)\",\n          \"format\": \"time_series\",\n          \"instant\": false,\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{action}}: statusCode:{{status_code}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Activation Status Code [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": \"activations\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 22\n      },\n      \"id\": 12,\n      \"panels\": [],\n      \"title\": \"Duration graph\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 23\n      },\n      \"id\": 38,\n      \"legend\": {\n        \"alignAsTable\": false,\n        \"avg\": false,\n        \"current\": false,\n        \"hideEmpty\": true,\n        \"hideZero\": true,\n        \"max\": false,\n        \"min\": false,\n        \"rightSide\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"maxPerRow\": 4,\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"repeatDirection\": \"h\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"rate(openwhisk_action_duration_seconds_sum{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$interval]) * 1000 / rate(openwhisk_action_duration_seconds_count{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$interval]) \",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Duration [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"ms\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 31\n      },\n      \"id\": 10,\n      \"panels\": [],\n      \"title\": \"Init Time Graph\",\n      \"type\": \"row\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 32\n      },\n      \"id\": 24,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"rate(openwhisk_action_initTime_seconds_sum{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$interval]) * 1000 / rate(openwhisk_action_initTime_seconds_count{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$interval]) \",\n          \"format\": \"time_series\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Initialization time [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"ms\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": \"Prometheus\",\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 9,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 41\n      },\n      \"id\": 35,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 5,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"rate(openwhisk_action_waitTime_seconds_sum{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$interval]) * 1000 / rate(openwhisk_action_waitTime_seconds_count{region=~\\\"$region\\\",stack=~\\\"$stack\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\"}[$interval]) \",\n          \"format\": \"time_series\",\n          \"instant\": false,\n          \"interval\": \"\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{action}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Wait time [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"ms\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 50\n      },\n      \"id\": 41,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(counter_invoker_containerStart_counter_total{containerState!=\\\"warmed\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\",region=~\\\"$region\\\",stack=~\\\"$stack\\\"}[$interval])) by (containerState)\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{containerState}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Non-warm container starts [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"fill\": 1,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 50\n      },\n      \"id\": 42,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"links\": [],\n      \"nullPointMode\": \"null\",\n      \"percentage\": false,\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"sum(increase(counter_invoker_containerStart_counter_total{containerState=\\\"warmed\\\",namespace=~\\\"$namespace\\\",action=~\\\"$action\\\",initiator=~\\\"$initiator\\\",region=~\\\"$region\\\",stack=~\\\"$stack\\\"}[$interval])) by (containerState)\",\n          \"format\": \"time_series\",\n          \"intervalFactor\": 1,\n          \"legendFormat\": \"{{containerState}}\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Warm container starts [$interval]\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    }\n  ],\n  \"refresh\": \"5s\",\n  \"schemaVersion\": 18,\n  \"style\": \"dark\",\n  \"tags\": [\n    \"openwhisk\"\n  ],\n  \"templating\": {\n    \"list\": [\n      {\n        \"allValue\": \"\",\n        \"current\": {},\n        \"datasource\": \"Prometheus\",\n        \"definition\": \"query_result(sum(increase(openwhisk_action_activations_total[$__range])) by (region) > 0)\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": null,\n        \"multi\": false,\n        \"name\": \"region\",\n        \"options\": [],\n        \"query\": \"query_result(sum(increase(openwhisk_action_activations_total[$__range])) by (region) > 0)\",\n        \"refresh\": 1,\n        \"regex\": \"/.*region=\\\"(.*)\\\".*/\",\n        \"skipUrlSync\": false,\n        \"sort\": 2,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"allValue\": \"\",\n        \"current\": {},\n        \"datasource\": \"Prometheus\",\n        \"definition\": \"query_result(sum(increase(openwhisk_action_activations_total[$__range])) by (stack) > 0)\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": null,\n        \"multi\": false,\n        \"name\": \"stack\",\n        \"options\": [],\n        \"query\": \"query_result(sum(increase(openwhisk_action_activations_total[$__range])) by (stack) > 0)\",\n        \"refresh\": 1,\n        \"regex\": \"/.*stack=\\\"(.*)\\\".*/\",\n        \"skipUrlSync\": false,\n        \"sort\": 1,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"allValue\": null,\n        \"current\": {},\n        \"datasource\": \"Prometheus\",\n        \"definition\": \"query_result(sum(increase(openwhisk_action_activations_total{namespace=~\\\"$namespace\\\"}[$__range])) by (initiator) > 0)\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": null,\n        \"multi\": false,\n        \"name\": \"initiator\",\n        \"options\": [],\n        \"query\": \"query_result(sum(increase(openwhisk_action_activations_total{namespace=~\\\"$namespace\\\"}[$__range])) by (initiator) > 0)\",\n        \"refresh\": 1,\n        \"regex\": \"/.*initiator=\\\"(.*)\\\".*/\",\n        \"skipUrlSync\": false,\n        \"sort\": 1,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"allValue\": null,\n        \"current\": {},\n        \"datasource\": \"Prometheus\",\n        \"definition\": \"query_result(sum(increase(openwhisk_action_activations_total[$__range])) by (namespace))\",\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": null,\n        \"multi\": false,\n        \"name\": \"namespace\",\n        \"options\": [],\n        \"query\": \"query_result(sum(increase(openwhisk_action_activations_total[$__range])) by (namespace))\",\n        \"refresh\": 1,\n        \"regex\": \"/.*namespace=\\\"(.*)\\\".*/\",\n        \"skipUrlSync\": false,\n        \"sort\": 1,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"allValue\": \"\",\n        \"current\": {},\n        \"datasource\": \"Prometheus\",\n        \"definition\": \"query_result(sum(increase(openwhisk_action_activations_total{namespace=~\\\"$namespace\\\",initiator=~\\\"$initiator\\\"}[$__range])) by (action) > 0)\",\n        \"hide\": 0,\n        \"includeAll\": true,\n        \"label\": null,\n        \"multi\": false,\n        \"name\": \"action\",\n        \"options\": [],\n        \"query\": \"query_result(sum(increase(openwhisk_action_activations_total{namespace=~\\\"$namespace\\\",initiator=~\\\"$initiator\\\"}[$__range])) by (action) > 0)\",\n        \"refresh\": 1,\n        \"regex\": \"/.*action=\\\"(.*)\\\".*/\",\n        \"skipUrlSync\": false,\n        \"sort\": 1,\n        \"tagValuesQuery\": \"\",\n        \"tags\": [],\n        \"tagsQuery\": \"\",\n        \"type\": \"query\",\n        \"useTags\": false\n      },\n      {\n        \"auto\": true,\n        \"auto_count\": 30,\n        \"auto_min\": \"30s\",\n        \"current\": {\n          \"text\": \"auto\",\n          \"value\": \"$__auto_interval_interval\"\n        },\n        \"hide\": 2,\n        \"label\": null,\n        \"name\": \"interval\",\n        \"options\": [\n          {\n            \"selected\": true,\n            \"text\": \"auto\",\n            \"value\": \"$__auto_interval_interval\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"30s\",\n            \"value\": \"30s\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1m\",\n            \"value\": \"1m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"10m\",\n            \"value\": \"10m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"30m\",\n            \"value\": \"30m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1h\",\n            \"value\": \"1h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"6h\",\n            \"value\": \"6h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"12h\",\n            \"value\": \"12h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1d\",\n            \"value\": \"1d\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"7d\",\n            \"value\": \"7d\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"14d\",\n            \"value\": \"14d\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"30d\",\n            \"value\": \"30d\"\n          }\n        ],\n        \"query\": \"30s,1m,10m,30m,1h,6h,12h,1d,7d,14d,30d\",\n        \"refresh\": 2,\n        \"skipUrlSync\": false,\n        \"type\": \"interval\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-15m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ],\n    \"time_options\": [\n      \"5m\",\n      \"15m\",\n      \"1h\",\n      \"6h\",\n      \"12h\",\n      \"24h\",\n      \"2d\",\n      \"7d\",\n      \"30d\"\n    ]\n  },\n  \"timezone\": \"\",\n  \"title\": \"Openwhisk - Action Performance Metrics\",\n  \"uid\": \"Oew1lvymk\",\n  \"version\": 2\n}"
  },
  {
    "path": "core/monitoring/user-events/compose/grafana/dashboards/top-namespaces.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_PROMETHEUS\",\n      \"label\": \"Prometheus\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"prometheus\",\n      \"pluginName\": \"Prometheus\"\n    }\n  ],\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"6.1.6\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"prometheus\",\n      \"name\": \"Prometheus\",\n      \"version\": \"1.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"table\",\n      \"name\": \"Table\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"gnetId\": null,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"columns\": [],\n      \"datasource\": \"Prometheus\",\n      \"description\": \"Top namespaces by activation count\",\n      \"fontSize\": \"100%\",\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 2,\n      \"links\": [],\n      \"pageSize\": null,\n      \"scroll\": true,\n      \"showHeader\": true,\n      \"sort\": {\n        \"col\": 0,\n        \"desc\": false\n      },\n      \"styles\": [\n        {\n          \"alias\": \"Time\",\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"pattern\": \"Time\",\n          \"type\": \"hidden\"\n        },\n        {\n          \"alias\": \"Namespace\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 2,\n          \"link\": true,\n          \"linkTargetBlank\": false,\n          \"linkTooltip\": \"Metrics related to ${__cell}\",\n          \"linkUrl\": \"d/Oew1lvymk/openwhisk-action-performance-metrics?var-namespace=${__cell}&from=${__from}&to=${__to}\",\n          \"mappingType\": 1,\n          \"pattern\": \"namespace\",\n          \"thresholds\": [],\n          \"type\": \"string\",\n          \"unit\": \"short\"\n        },\n        {\n          \"alias\": \"Activation Count\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 2,\n          \"mappingType\": 1,\n          \"pattern\": \"Value\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"short\"\n        },\n        {\n          \"alias\": \"\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"decimals\": 2,\n          \"pattern\": \"/.*/\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"short\"\n        }\n      ],\n      \"targets\": [\n        {\n          \"expr\": \"topk(10, sum by(namespace)(increase(openwhisk_action_activations_total[${__range_s}s])))\",\n          \"format\": \"table\",\n          \"hide\": false,\n          \"instant\": true,\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Top Namespaces\",\n      \"transform\": \"table\",\n      \"type\": \"table\"\n    },\n    {\n      \"columns\": [],\n      \"datasource\": \"Prometheus\",\n      \"description\": \"Top memory sizes specified (in MB)\",\n      \"fontSize\": \"100%\",\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 5,\n        \"x\": 12,\n        \"y\": 0\n      },\n      \"id\": 4,\n      \"links\": [],\n      \"pageSize\": null,\n      \"scroll\": true,\n      \"showHeader\": true,\n      \"sort\": {\n        \"col\": 0,\n        \"desc\": false\n      },\n      \"styles\": [\n        {\n          \"alias\": \"Time\",\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"pattern\": \"Time\",\n          \"type\": \"hidden\"\n        },\n        {\n          \"alias\": \"Namespace\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 2,\n          \"link\": true,\n          \"linkTargetBlank\": false,\n          \"linkUrl\": \"d/Oew1lvymk/openwhisk-action-performance-metrics?var-namespace=${__cell}\",\n          \"mappingType\": 1,\n          \"pattern\": \"namespace\",\n          \"thresholds\": [],\n          \"type\": \"string\",\n          \"unit\": \"short\"\n        },\n        {\n          \"alias\": \"Activation Count\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 2,\n          \"mappingType\": 1,\n          \"pattern\": \"Value\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"short\"\n        },\n        {\n          \"alias\": \"\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"decimals\": 2,\n          \"pattern\": \"/.*/\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"short\"\n        }\n      ],\n      \"targets\": [\n        {\n          \"expr\": \"topk(10, sum by(memory)(increase(openwhisk_action_activations_total[${__range_s}s])))\",\n          \"format\": \"table\",\n          \"hide\": false,\n          \"instant\": true,\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Memory\",\n      \"transform\": \"table\",\n      \"type\": \"table\"\n    },\n    {\n      \"columns\": [],\n      \"datasource\": \"Prometheus\",\n      \"description\": \"Top activation 'kind'\",\n      \"fontSize\": \"100%\",\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 5,\n        \"x\": 17,\n        \"y\": 0\n      },\n      \"id\": 5,\n      \"links\": [],\n      \"pageSize\": null,\n      \"scroll\": true,\n      \"showHeader\": true,\n      \"sort\": {\n        \"col\": 0,\n        \"desc\": false\n      },\n      \"styles\": [\n        {\n          \"alias\": \"Time\",\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"pattern\": \"Time\",\n          \"type\": \"hidden\"\n        },\n        {\n          \"alias\": \"Namespace\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 2,\n          \"link\": true,\n          \"linkTargetBlank\": false,\n          \"linkUrl\": \"d/Oew1lvymk/openwhisk-action-performance-metrics?var-namespace=${__cell}\",\n          \"mappingType\": 1,\n          \"pattern\": \"namespace\",\n          \"thresholds\": [],\n          \"type\": \"string\",\n          \"unit\": \"short\"\n        },\n        {\n          \"alias\": \"Activation Count\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 2,\n          \"mappingType\": 1,\n          \"pattern\": \"Value\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"short\"\n        },\n        {\n          \"alias\": \"\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"decimals\": 2,\n          \"pattern\": \"/.*/\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"short\"\n        }\n      ],\n      \"targets\": [\n        {\n          \"expr\": \"topk(10, sum by(kind)(increase(openwhisk_action_activations_total[${__range_s}s])))\",\n          \"format\": \"table\",\n          \"hide\": false,\n          \"instant\": true,\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Kind\",\n      \"transform\": \"table\",\n      \"type\": \"table\"\n    },\n    {\n      \"columns\": [],\n      \"datasource\": \"Prometheus\",\n      \"fontSize\": \"100%\",\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 22,\n        \"x\": 0,\n        \"y\": 10\n      },\n      \"id\": 3,\n      \"links\": [],\n      \"pageSize\": null,\n      \"scroll\": true,\n      \"showHeader\": true,\n      \"sort\": {\n        \"col\": 0,\n        \"desc\": false\n      },\n      \"styles\": [\n        {\n          \"alias\": \"Time\",\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"pattern\": \"Time\",\n          \"type\": \"hidden\"\n        },\n        {\n          \"alias\": \"Namespace\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 2,\n          \"mappingType\": 1,\n          \"pattern\": \"namespace\",\n          \"thresholds\": [],\n          \"type\": \"string\",\n          \"unit\": \"short\"\n        },\n        {\n          \"alias\": \"Action\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 2,\n          \"link\": true,\n          \"linkTooltip\": \"Action ${__cell} details\",\n          \"linkUrl\": \"d/Oew1lvymk/openwhisk-action-performance-metrics?var-namespace=${__cell_2}&var-action=${__cell}&from=${__from}&to=${__to}\",\n          \"mappingType\": 1,\n          \"pattern\": \"action\",\n          \"thresholds\": [],\n          \"type\": \"string\",\n          \"unit\": \"short\"\n        },\n        {\n          \"alias\": \"Activation Count\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"dateFormat\": \"YYYY-MM-DD HH:mm:ss\",\n          \"decimals\": 2,\n          \"mappingType\": 1,\n          \"pattern\": \"Value\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"short\"\n        },\n        {\n          \"alias\": \"\",\n          \"colorMode\": null,\n          \"colors\": [\n            \"rgba(245, 54, 54, 0.9)\",\n            \"rgba(237, 129, 40, 0.89)\",\n            \"rgba(50, 172, 45, 0.97)\"\n          ],\n          \"decimals\": 2,\n          \"pattern\": \"/.*/\",\n          \"thresholds\": [],\n          \"type\": \"number\",\n          \"unit\": \"short\"\n        }\n      ],\n      \"targets\": [\n        {\n          \"expr\": \"topk(10, sum by(namespace,action,kind,memory)(increase(openwhisk_action_activations_total[${__range_s}s])))\",\n          \"format\": \"table\",\n          \"hide\": false,\n          \"instant\": true,\n          \"intervalFactor\": 1,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Top Actions\",\n      \"transform\": \"table\",\n      \"type\": \"table\"\n    }\n  ],\n  \"schemaVersion\": 18,\n  \"style\": \"dark\",\n  \"tags\": [\n    \"openwhisk\"\n  ],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-15m\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {\n    \"refresh_intervals\": [\n      \"5s\",\n      \"10s\",\n      \"30s\",\n      \"1m\",\n      \"5m\",\n      \"15m\",\n      \"30m\",\n      \"1h\",\n      \"2h\",\n      \"1d\"\n    ],\n    \"time_options\": [\n      \"5m\",\n      \"15m\",\n      \"1h\",\n      \"6h\",\n      \"12h\",\n      \"24h\",\n      \"2d\",\n      \"7d\",\n      \"30d\"\n    ]\n  },\n  \"timezone\": \"\",\n  \"title\": \"OpenWhisk - Top Namespaces\",\n  \"uid\": \"RnvlchiZk\",\n  \"version\": 1\n}"
  },
  {
    "path": "core/monitoring/user-events/compose/grafana/provisioning/dashboards/dashboard.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\napiVersion: 1\n\nproviders:\n- name: 'Prometheus'\n  orgId: 1\n  folder: ''\n  type: file\n  disableDeletion: false\n  editable: true\n  options:\n    path: /var/lib/grafana/dashboards\n"
  },
  {
    "path": "core/monitoring/user-events/compose/grafana/provisioning/datasources/datasource.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# config file version\napiVersion: 1\n\n# list of datasources that should be deleted from the database\ndeleteDatasources:\n  - name: Prometheus\n    orgId: 1\n\n# list of datasources to insert/update depending\n# whats available in the database\ndatasources:\n  # <string, required> name of the datasource. Required\n- name: Prometheus\n  # <string, required> datasource type. Required\n  type: prometheus\n  # <string, required> access mode. direct or proxy. Required\n  access: proxy\n  # <int> org id. will default to orgId 1 if not specified\n  orgId: 1\n  # <string> url\n  url: http://prometheus:9090\n  # <string> database password, if used\n  password:\n  # <string> database user, if used\n  user:\n  # <string> database name, if used\n  database:\n  # <bool> enable/disable basic auth\n  basicAuth: true\n  # <string> basic auth username\n  basicAuthUser: admin\n  # <string> basic auth password\n  basicAuthPassword: foobar\n  # <bool> enable/disable with credentials headers\n  withCredentials:\n  # <bool> mark as default datasource. Max one per org\n  isDefault: true\n  # <map> fields that will be converted to json and stored in json_data\n  jsonData:\n     graphiteVersion: \"1.1\"\n     tlsAuth: false\n     tlsAuthWithCACert: false\n  # <string> json object of data that will be encrypted.\n  secureJsonData:\n    tlsCACert: \"...\"\n    tlsClientCert: \"...\"\n    tlsClientKey: \"...\"\n  version: 1\n  # <bool> allow users to edit datasources from the UI.\n  editable: true\n"
  },
  {
    "path": "core/monitoring/user-events/compose/prometheus/prometheus.yml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nglobal:\n  scrape_interval: 10s\n  evaluation_interval: 10s\n\nscrape_configs:\n  - job_name: 'prometheus-server'\n    static_configs:\n      - targets: ['localhost:9090']\n\n  - job_name: 'openwhisk-metrics'\n    static_configs:\n      - targets: ['user-events:9095']\n\n"
  },
  {
    "path": "core/monitoring/user-events/init.sh",
    "content": "#!/bin/bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n./copyJMXFiles.sh\n\nexport USER_EVENTS_OPTS\nUSER_EVENTS_OPTS=\"$USER_EVENTS_OPTS $(./transformEnvironment.sh)\"\n\nexec user-events/bin/user-events \"$@\"\n"
  },
  {
    "path": "core/monitoring/user-events/src/main/resources/application.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\npekko.kafka.committer {\n\n  max-batch = 20\n\n}\n\npekko.kafka.consumer {\n  # Properties defined by org.apache.kafka.clients.consumer.ConsumerConfig\n  # can be defined in this configuration section.\n  kafka-clients {\n    group.id = \"kamon\"\n\n    auto.offset.reset = \"earliest\"\n\n    # Disable auto-commit by default\n    enable.auto.commit = false\n\n    bootstrap.servers = ${?KAFKA_HOSTS}\n  }\n}\n\nkamon {\n  metric {\n    tick-interval = 15 seconds\n  }\n  prometheus {\n    # We expose the metrics endpoint over pekko http. So default server is disabled\n    start-embedded-http-server = no\n  }\n\n  system-metrics {\n    # disable the host metrics as we are only interested in JVM metrics\n    host.enabled = false\n  }\n\n  environment {\n    # Identifier for this service. For keeping it backward compatible setting to natch previous\n    # statsd name\n    service = \"user-events\"\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/main/resources/reference.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nwhisk {\n  user-events {\n    # Server port\n    port = 9095\n\n    # Enables KamonRecorder so as to enable sending metrics to Kamon supported backends\n    # like DataDog\n    enable-kamon = false\n\n    # Namespaces that should not be monitored\n    ignored-namespaces = []\n\n    rename-tags {\n      # rename/relabel prometheus metrics tags\n      # \"namespace\" = \"ow_namespae\"\n    }\n\n    retry {\n      # minimum (initial) duration until the Kafka consumer is started again if it is terminated\n      min-backoff = 3 secs\n\n      # the exponential back-off is capped to this duration\n      max-backoff = 30 secs\n\n      # after calculation of the exponential back-off an additional\n      # random delay based on this factor is added, e.g. `0.2` adds up to `20%` delay\n      random-factor = 0.2\n\n      # the amount of restarts is capped to this amount within a time frame of minBackoff\n      max-restarts = 10\n    }\n\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/main/resources/whisk-logback.xml",
    "content": "<!--\n  ~ Licensed to the Apache Software Foundation (ASF) under one or more\n  ~ contributor license agreements.  See the NOTICE file distributed with\n  ~ this work for additional information regarding copyright ownership.\n  ~ The ASF licenses this file to You under the Apache License, Version 2.0\n  ~ (the \"License\"); you may not use this file except in compliance with\n  ~ the License.  You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<included>\n    <contextListener class=\"ch.qos.logback.classic.jul.LevelChangePropagator\">\n        <resetJUL>true</resetJUL>\n    </contextListener>\n\n    <!-- Kafka -->\n    <logger name=\"org.apache.kafka\" level=\"ERROR\" />\n</included>"
  },
  {
    "path": "core/monitoring/user-events/src/main/scala/org/apache/openwhisk/core/monitoring/metrics/EventConsumer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport java.lang.management.ManagementFactory\nimport java.util.concurrent.atomic.AtomicReference\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.kafka.scaladsl.{Committer, Consumer}\nimport org.apache.pekko.kafka.{CommitterSettings, ConsumerSettings, Subscriptions}\nimport org.apache.pekko.stream.RestartSettings\nimport org.apache.pekko.stream.scaladsl.{RestartSource, Sink}\nimport javax.management.ObjectName\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport kamon.Kamon\nimport kamon.metric.MeasurementUnit\nimport kamon.tag.TagSet\nimport org.apache.kafka.common\nimport org.apache.kafka.common.MetricName\nimport org.apache.openwhisk.connector.kafka.{KafkaMetricsProvider, KamonMetricsReporter}\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.concurrent.duration._\nimport org.apache.openwhisk.core.connector.{Activation, EventMessage, Metric}\nimport org.apache.openwhisk.core.entity.ActivationResponse\nimport org.apache.openwhisk.core.monitoring.metrics.OpenWhiskEvents.MetricConfig\n\ntrait MetricRecorder {\n  def processActivation(activation: Activation, initiatorNamespace: String): Unit\n  def processMetric(metric: Metric, initiatorNamespace: String): Unit\n}\n\ncase class EventConsumer(settings: ConsumerSettings[String, String],\n                         recorders: Seq[MetricRecorder],\n                         metricConfig: MetricConfig)(implicit system: ActorSystem)\n    extends KafkaMetricsProvider {\n  import EventConsumer._\n\n  private implicit val ec: ExecutionContext = system.dispatcher\n\n  //Record the rate of events received\n  private val activationNamespaceCounter = Kamon.counter(\"openwhisk.namespace.activations\")\n  private val activationCounter = Kamon.counter(\"openwhisk.userevents.global.activations\").withoutTags()\n  private val metricCounter = Kamon.counter(\"openwhisk.userevents.global.metric\").withoutTags()\n\n  private val statusCounter = Kamon.counter(\"openwhisk.userevents.global.status\")\n  private val coldStartCounter = Kamon.counter(\"openwhisk.userevents.global.coldStarts\").withoutTags()\n\n  private val statusSuccess = statusCounter.withTag(\"status\", ActivationResponse.statusSuccess)\n  private val statusFailure = statusCounter.withTag(\"status\", \"failure\")\n  private val statusApplicationError = statusCounter.withTag(\"status\", ActivationResponse.statusApplicationError)\n  private val statusDeveloperError = statusCounter.withTag(\"status\", ActivationResponse.statusDeveloperError)\n  private val statusInternalError = statusCounter.withTag(\"status\", ActivationResponse.statusWhiskError)\n\n  private val waitTime =\n    Kamon.histogram(\"openwhisk.userevents.global.waitTime\", MeasurementUnit.time.milliseconds).withoutTags()\n  private val initTime =\n    Kamon.histogram(\"openwhisk.userevents.global.initTime\", MeasurementUnit.time.milliseconds).withoutTags()\n  private val duration =\n    Kamon.histogram(\"openwhisk.userevents.global.duration\", MeasurementUnit.time.milliseconds).withoutTags()\n\n  private val lagGauge = Kamon.gauge(\"openwhisk.userevents.consumer.lag\").withoutTags()\n\n  def shutdown(): Future[Done] = {\n    lagRecorder.cancel()\n    control.get().drainAndShutdown(result)(system.dispatcher)\n  }\n\n  def isRunning: Boolean = !control.get().isShutdown.isCompleted\n\n  override def metrics(): Future[Map[MetricName, common.Metric]] = control.get().metrics\n\n  private val committerSettings = CommitterSettings(system)\n  private val control = new AtomicReference[Consumer.Control](Consumer.NoopControl)\n\n  private val result = RestartSource\n    .onFailuresWithBackoff(\n      RestartSettings(\n        minBackoff = metricConfig.retry.minBackoff,\n        maxBackoff = metricConfig.retry.maxBackoff,\n        randomFactor = metricConfig.retry.randomFactor)\n        .withMaxRestarts(metricConfig.retry.maxRestarts, metricConfig.retry.minBackoff)) { () =>\n      Consumer\n        .committableSource(updatedSettings, Subscriptions.topics(userEventTopic))\n        // this is to access to the Consumer.Control\n        // instances of the latest Kafka Consumer source\n        .mapMaterializedValue(c => control.set(c))\n        .map { msg =>\n          processEvent(msg.record.value())\n          msg.committableOffset\n        }\n        .via(Committer.flow(committerSettings))\n    }\n    .runWith(Sink.ignore)\n\n  private val lagRecorder =\n    system.scheduler.scheduleAtFixedRate(10.seconds, 10.seconds)(() => lagGauge.update(consumerLag))\n\n  private def processEvent(value: String): Unit = {\n    EventMessage\n      .parse(value)\n      .map { e =>\n        e.eventType match {\n          case Activation.typeName => activationCounter.increment()\n          case Metric.typeName     => metricCounter.increment()\n        }\n        e\n      }\n      .foreach { e =>\n        e.body match {\n          case a: Activation =>\n            // only record activation if not executed in an ignored namespace\n            if (!metricConfig.ignoredNamespaces.contains(e.namespace)) {\n              recorders.foreach(_.processActivation(a, e.namespace))\n            }\n            updateGlobalMetrics(a, e.namespace)\n          case m: Metric =>\n            recorders.foreach(_.processMetric(m, e.namespace))\n        }\n      }\n  }\n\n  private def updateGlobalMetrics(a: Activation, e: String): Unit = {\n    val namespaceTag: String = metricConfig.renameTags.getOrElse(\"namespace\", \"namespace\")\n    val initiatorTag: String = metricConfig.renameTags.getOrElse(\"initiator\", \"initiator\")\n    val tagSet = TagSet.from(Map(initiatorTag -> e, namespaceTag -> e))\n    activationNamespaceCounter.withTags(tagSet).increment()\n    a.status match {\n      case ActivationResponse.statusSuccess          => statusSuccess.increment()\n      case ActivationResponse.statusApplicationError => statusApplicationError.increment()\n      case ActivationResponse.statusDeveloperError   => statusDeveloperError.increment()\n      case ActivationResponse.statusWhiskError       => statusInternalError.increment()\n      case _                                         => //Ignore for now\n    }\n\n    if (a.status != ActivationResponse.statusSuccess) statusFailure.increment()\n    if (a.isColdStart) {\n      coldStartCounter.increment()\n      initTime.record(a.initTime.toMillis)\n    }\n\n    waitTime.record(a.waitTime.toMillis)\n    duration.record(a.duration.toMillis)\n  }\n\n  private def updatedSettings =\n    settings\n      .withProperty(ConsumerConfig.CLIENT_ID_CONFIG, id)\n      .withProperty(ConsumerConfig.METRIC_REPORTER_CLASSES_CONFIG, KamonMetricsReporter.name)\n      .withStopTimeout(Duration.Zero) // https://pekko.apache.org/api/pekko-connectors-kafka/current/org/apache/pekko/kafka/scaladsl/Consumer$$DrainingControl$.html\n}\n\nobject EventConsumer {\n  val userEventTopic = \"events\"\n  val id = \"event-consumer\"\n\n  private val server = ManagementFactory.getPlatformMBeanServer\n  private val name = new ObjectName(s\"kafka.consumer:type=consumer-fetch-manager-metrics,client-id=$id\")\n\n  def consumerLag: Long = server.getAttribute(name, \"records-lag-max\").asInstanceOf[Double].toLong.max(0)\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/main/scala/org/apache/openwhisk/core/monitoring/metrics/KamonRecorder.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport org.apache.pekko.event.slf4j.SLF4JLogging\nimport org.apache.openwhisk.core.connector.{Activation, Metric}\nimport kamon.Kamon\nimport kamon.metric.MeasurementUnit\nimport kamon.tag.TagSet\n\nimport scala.collection.concurrent.TrieMap\n\ntrait KamonMetricNames extends MetricNames {\n  val namespaceActivationMetric = \"openwhisk.namespace.activations\"\n  val activationMetric = \"openwhisk.action.activations\"\n  val coldStartMetric = \"openwhisk.action.coldStarts\"\n  val waitTimeMetric = \"openwhisk.action.waitTime\"\n  val initTimeMetric = \"openwhisk.action.initTime\"\n  val durationMetric = \"openwhisk.action.duration\"\n  val responseSizeMetric = \"openwhisk.action.responseSize\"\n  val statusMetric = \"openwhisk.action.status\"\n  val userDefinedStatusCodeMetric = \"openwhisk.action.statusCode\"\n\n  val concurrentLimitMetric = \"openwhisk.action.limit.concurrent\"\n  val timedLimitMetric = \"openwhisk.action.limit.timed\"\n}\n\nobject KamonRecorder extends MetricRecorder with KamonMetricNames with SLF4JLogging {\n  private val activationMetrics = new TrieMap[String, ActivationKamonMetrics]\n  private val limitMetrics = new TrieMap[String, LimitKamonMetrics]\n\n  override def processActivation(activation: Activation, initiator: String): Unit = {\n    lookup(activation, initiator).record(activation)\n  }\n\n  override def processMetric(metric: Metric, initiator: String): Unit = {\n    val limitMetric = limitMetrics.getOrElseUpdate(initiator, LimitKamonMetrics(initiator))\n    limitMetric.record(metric)\n  }\n\n  def lookup(activation: Activation, initiator: String): ActivationKamonMetrics = {\n    val name = activation.name\n    val kind = activation.kind\n    val memory = activation.memory.toString\n    val namespace = activation.namespace\n    val action = activation.action\n    activationMetrics.getOrElseUpdate(name, {\n      ActivationKamonMetrics(namespace, action, kind, memory, initiator)\n    })\n  }\n\n  case class LimitKamonMetrics(namespace: String) {\n    private val concurrentLimit = Kamon.counter(concurrentLimitMetric).withTag(`actionNamespace`, namespace)\n    private val timedLimit = Kamon.counter(timedLimitMetric).withTag(`actionNamespace`, namespace)\n\n    def record(m: Metric): Unit = {\n      m.metricName match {\n        case \"ConcurrentRateLimit\"   => concurrentLimit.increment()\n        case \"TimedRateLimit\"        => timedLimit.increment()\n        case \"ConcurrentInvocations\" => //TODO Handle ConcurrentInvocations\n        case x                       => log.warn(s\"Unknown limit $x\")\n      }\n    }\n  }\n\n  case class ActivationKamonMetrics(namespace: String,\n                                    action: String,\n                                    kind: String,\n                                    memory: String,\n                                    initiator: String) {\n    private val activationTags =\n      TagSet.from(\n        Map(\n          `actionNamespace` -> namespace,\n          `initiatorNamespace` -> initiator,\n          `actionName` -> action,\n          `actionKind` -> kind,\n          `actionMemory` -> memory))\n    private val namespaceActivationsTags =\n      TagSet.from(Map(`actionNamespace` -> namespace, `initiatorNamespace` -> initiator))\n    private val tags =\n      TagSet.from(Map(`actionNamespace` -> namespace, `initiatorNamespace` -> initiator, `actionName` -> action))\n\n    private val activations = Kamon.counter(activationMetric).withTags(activationTags)\n    private val coldStarts = Kamon.counter(coldStartMetric).withTags(tags)\n    private val waitTime = Kamon.histogram(waitTimeMetric, MeasurementUnit.time.milliseconds).withTags(tags)\n    private val initTime = Kamon.histogram(initTimeMetric, MeasurementUnit.time.milliseconds).withTags(tags)\n    private val duration = Kamon.histogram(durationMetric, MeasurementUnit.time.milliseconds).withTags(tags)\n    private val responseSize = Kamon.histogram(responseSizeMetric, MeasurementUnit.information.bytes).withTags(tags)\n    private val userDefinedStatusCode = Kamon.counter(userDefinedStatusCodeMetric).withTags(tags)\n\n    def record(a: Activation): Unit = {\n      recordActivation(a)\n    }\n\n    def recordActivation(a: Activation): Unit = {\n      activations.increment()\n\n      if (a.isColdStart) {\n        coldStarts.increment()\n        initTime.record(a.initTime.toMillis)\n      }\n\n      //waitTime may be zero for activations which are part of sequence\n      waitTime.record(a.waitTime.toMillis)\n      duration.record(a.duration.toMillis)\n\n      Kamon.counter(statusMetric).withTags(tags.withTag(\"status\", a.status)).increment()\n\n      a.size.foreach(responseSize.record(_))\n      a.userDefinedStatusCode.foreach(\n        value =>\n          Kamon\n            .counter(userDefinedStatusCodeMetric)\n            .withTags(tags.withTag(\"userDefinedStatusCode\", value.toString))\n            .increment())\n    }\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/main/scala/org/apache/openwhisk/core/monitoring/metrics/Main.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.Http\nimport kamon.Kamon\n\nimport scala.concurrent.duration.DurationInt\nimport scala.concurrent.{Await, ExecutionContextExecutor, Future}\n\nobject Main {\n  def main(args: Array[String]): Unit = {\n    Kamon.init()\n    implicit val system: ActorSystem = ActorSystem(\"events-actor-system\")\n    val binding = OpenWhiskEvents.start(system.settings.config)\n    addShutdownHook(binding)\n  }\n\n  private def addShutdownHook(binding: Future[Http.ServerBinding])(implicit actorSystem: ActorSystem): Unit = {\n    implicit val ec: ExecutionContextExecutor = actorSystem.dispatcher\n    sys.addShutdownHook {\n      Await.result(binding.map(_.unbind()), 30.seconds)\n      Await.result(actorSystem.whenTerminated, 30.seconds)\n    }\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/main/scala/org/apache/openwhisk/core/monitoring/metrics/MetricNames.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\ntrait MetricNames {\n  val actionNamespace = \"namespace\"\n  val initiatorNamespace = \"initiator\"\n  val actionName = \"action\"\n  val actionStatus = \"status\"\n  val actionMemory = \"memory\"\n  val actionKind = \"kind\"\n  val userDefinedStatusCode = \"status_code\"\n\n  def activationMetric: String\n  def coldStartMetric: String\n  def waitTimeMetric: String\n  def initTimeMetric: String\n  def durationMetric: String\n  def statusMetric: String\n  def responseSizeMetric: String\n  def userDefinedStatusCodeMetric: String\n\n  def concurrentLimitMetric: String\n  def timedLimitMetric: String\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/main/scala/org/apache/openwhisk/core/monitoring/metrics/OpenWhiskEvents.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport org.apache.pekko.actor.{ActorSystem, CoordinatedShutdown}\nimport org.apache.pekko.event.slf4j.SLF4JLogging\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.kafka.ConsumerSettings\nimport com.typesafe.config.Config\nimport kamon.Kamon\nimport kamon.prometheus.PrometheusReporter\nimport org.apache.kafka.common.serialization.StringDeserializer\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.duration.FiniteDuration\nimport scala.concurrent.{ExecutionContext, Future}\n\nobject OpenWhiskEvents extends SLF4JLogging {\n\n  case class MetricConfig(port: Int,\n                          enableKamon: Boolean,\n                          ignoredNamespaces: Set[String],\n                          renameTags: Map[String, String],\n                          retry: RetryConfig)\n\n  case class RetryConfig(minBackoff: FiniteDuration, maxBackoff: FiniteDuration, randomFactor: Double, maxRestarts: Int)\n\n  def start(config: Config)(implicit system: ActorSystem): Future[Http.ServerBinding] = {\n    implicit val ec: ExecutionContext = system.dispatcher\n\n    val prometheusReporter = new PrometheusReporter()\n    Kamon.registerModule(\"prometheus\", prometheusReporter)\n    Kamon.init(config)\n\n    val metricConfig = loadConfigOrThrow[MetricConfig](config, \"whisk.user-events\")\n\n    val prometheusRecorder = PrometheusRecorder(prometheusReporter, metricConfig)\n    val recorders = if (metricConfig.enableKamon) Seq(prometheusRecorder, KamonRecorder) else Seq(prometheusRecorder)\n    val eventConsumer = EventConsumer(eventConsumerSettings(defaultConsumerConfig(config)), recorders, metricConfig)\n\n    CoordinatedShutdown(system).addTask(CoordinatedShutdown.PhaseBeforeServiceUnbind, \"shutdownConsumer\") { () =>\n      eventConsumer.shutdown()\n    }\n    val port = metricConfig.port\n    val api = new PrometheusEventsApi(eventConsumer, prometheusRecorder)\n    val httpBinding = Http().newServerAt(\"0.0.0.0\", port).bindFlow(api.routes)\n    httpBinding.foreach(_ => log.info(s\"Started the http server on http://localhost:$port\"))(system.dispatcher)\n    httpBinding\n  }\n\n  def eventConsumerSettings(config: Config): ConsumerSettings[String, String] =\n    ConsumerSettings(config, new StringDeserializer, new StringDeserializer)\n\n  def defaultConsumerConfig(globalConfig: Config): Config = globalConfig.getConfig(\"pekko.kafka.consumer\")\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/main/scala/org/apache/openwhisk/core/monitoring/metrics/PrometheusEventsApi.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.ServiceUnavailable\nimport org.apache.pekko.http.scaladsl.model.{ContentType, HttpCharsets, MediaType, MessageEntity}\nimport org.apache.pekko.http.scaladsl.server.Directives._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.openwhisk.connector.kafka.KafkaMetricRoute\n\nimport scala.concurrent.ExecutionContext\n\ntrait PrometheusExporter {\n  def getReport(): MessageEntity\n}\n\nobject PrometheusExporter {\n  val textV4: ContentType = ContentType.apply(\n    MediaType.textWithFixedCharset(\"plain\", HttpCharsets.`UTF-8`).withParams(Map(\"version\" -> \"0.0.4\")))\n}\n\nclass PrometheusEventsApi(consumer: EventConsumer, prometheus: PrometheusExporter)(implicit ec: ExecutionContext) {\n  val routes: Route = {\n    get {\n      path(\"ping\") {\n        if (consumer.isRunning) {\n          complete(\"pong\")\n        } else {\n          complete(ServiceUnavailable -> \"Consumer not running\")\n        }\n      } ~ path(\"metrics\") {\n        encodeResponse {\n          complete(prometheus.getReport())\n        }\n      } ~ KafkaMetricRoute(consumer)\n    }\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/main/scala/org/apache/openwhisk/core/monitoring/metrics/PrometheusRecorder.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport java.io.StringWriter\nimport java.util\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.pekko.event.slf4j.SLF4JLogging\nimport org.apache.pekko.http.scaladsl.model.{HttpEntity, MessageEntity}\nimport org.apache.pekko.stream.scaladsl.{Concat, Source}\nimport org.apache.pekko.util.ByteString\nimport io.prometheus.client.exporter.common.TextFormat\nimport io.prometheus.client.{CollectorRegistry, Counter, Gauge, Histogram}\nimport kamon.prometheus.PrometheusReporter\nimport org.apache.openwhisk.core.connector.{Activation, Metric}\nimport org.apache.openwhisk.core.entity.{ActivationEntityLimit, ActivationResponse}\nimport org.apache.openwhisk.core.monitoring.metrics.OpenWhiskEvents.MetricConfig\n\nimport scala.collection.JavaConverters._\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.duration.Duration\n\ntrait PrometheusMetricNames extends MetricNames {\n  val activationMetric = \"openwhisk_action_activations_total\"\n  val coldStartMetric = \"openwhisk_action_coldStarts_total\"\n  val waitTimeMetric = \"openwhisk_action_waitTime_seconds\"\n  val initTimeMetric = \"openwhisk_action_initTime_seconds\"\n  val durationMetric = \"openwhisk_action_duration_seconds\"\n  val responseSizeMetric = \"openwhisk_action_response_size_bytes\"\n  val statusMetric = \"openwhisk_action_status\"\n  val memoryMetric = \"openwhisk_action_memory\"\n  val userDefinedStatusCodeMetric = \"openwhisk_action_status_code\"\n\n  val concurrentLimitMetric = \"openwhisk_action_limit_concurrent_total\"\n  val timedLimitMetric = \"openwhisk_action_limit_timed_total\"\n}\n\ncase class PrometheusRecorder(kamon: PrometheusReporter, config: MetricConfig)\n    extends MetricRecorder\n    with PrometheusExporter\n    with SLF4JLogging {\n  private val activationMetrics = new TrieMap[String, ActivationPromMetrics]\n  private val limitMetrics = new TrieMap[String, LimitPromMetrics]\n  private val promMetrics = PrometheusMetrics()\n\n  override def processActivation(activation: Activation, initiator: String): Unit = {\n    lookup(activation, initiator).record(activation, initiator)\n  }\n\n  override def processMetric(metric: Metric, initiator: String): Unit = {\n    val limitMetric = limitMetrics.getOrElseUpdate(initiator, LimitPromMetrics(initiator))\n    limitMetric.record(metric)\n  }\n\n  override def getReport(): MessageEntity =\n    HttpEntity(PrometheusExporter.textV4, createSource())\n\n  private def lookup(activation: Activation, initiator: String): ActivationPromMetrics = {\n    //TODO Unregister unused actions\n    val name = activation.name\n    val kind = activation.kind\n    val memory = activation.memory.toString\n    val namespace = activation.namespace\n    val action = activation.action\n    activationMetrics.getOrElseUpdate(name, {\n      ActivationPromMetrics(namespace, action, kind, memory, initiator)\n    })\n  }\n\n  case class LimitPromMetrics(namespace: String) {\n    private val concurrentLimit = promMetrics.concurrentLimitCounter.labels(namespace)\n    private val timedLimit = promMetrics.timedLimitCounter.labels(namespace)\n\n    def record(m: Metric): Unit = {\n      m.metricName match {\n        case \"ConcurrentRateLimit\"   => concurrentLimit.inc()\n        case \"TimedRateLimit\"        => timedLimit.inc()\n        case \"ConcurrentInvocations\" => //TODO Handle ConcurrentInvocations\n        case x                       => log.warn(s\"Unknown limit $x\")\n      }\n    }\n  }\n\n  case class ActivationPromMetrics(namespace: String,\n                                   action: String,\n                                   kind: String,\n                                   memory: String,\n                                   initiatorNamespace: String) {\n\n    private val activations = promMetrics.activationCounter.labels(namespace, initiatorNamespace, action, kind, memory)\n    private val coldStarts = promMetrics.coldStartCounter.labels(namespace, initiatorNamespace, action)\n    private val waitTime = promMetrics.waitTimeHisto.labels(namespace, initiatorNamespace, action)\n    private val initTime = promMetrics.initTimeHisto.labels(namespace, initiatorNamespace, action)\n    private val duration = promMetrics.durationHisto.labels(namespace, initiatorNamespace, action)\n    private val responseSize = promMetrics.responseSizeHisto.labels(namespace, initiatorNamespace, action)\n\n    private val gauge = promMetrics.memoryGauge.labels(namespace, initiatorNamespace, action)\n\n    private val statusSuccess =\n      promMetrics.statusCounter.labels(namespace, initiatorNamespace, action, ActivationResponse.statusSuccess)\n    private val statusApplicationError =\n      promMetrics.statusCounter.labels(namespace, initiatorNamespace, action, ActivationResponse.statusApplicationError)\n    private val statusDeveloperError =\n      promMetrics.statusCounter.labels(namespace, initiatorNamespace, action, ActivationResponse.statusDeveloperError)\n    private val statusInternalError =\n      promMetrics.statusCounter.labels(namespace, initiatorNamespace, action, ActivationResponse.statusWhiskError)\n\n    def record(a: Activation, initiator: String): Unit = {\n      recordActivation(a, initiator)\n    }\n\n    def recordActivation(a: Activation, initiator: String): Unit = {\n      gauge.set(a.memory)\n\n      activations.inc()\n\n      if (a.isColdStart) {\n        coldStarts.inc()\n        initTime.observe(seconds(a.initTime))\n      }\n\n      //waitTime may be zero for activations which are part of sequence\n      waitTime.observe(seconds(a.waitTime))\n      duration.observe(seconds(a.duration))\n\n      a.status match {\n        case ActivationResponse.statusSuccess          => statusSuccess.inc()\n        case ActivationResponse.statusApplicationError => statusApplicationError.inc()\n        case ActivationResponse.statusDeveloperError   => statusDeveloperError.inc()\n        case ActivationResponse.statusWhiskError       => statusInternalError.inc()\n        case x                                         => promMetrics.statusCounter.labels(namespace, initiator, action, x).inc()\n      }\n\n      a.size.foreach(responseSize.observe(_))\n      a.userDefinedStatusCode.foreach(value =>\n        promMetrics.userDefinedStatusCodeCounter.labels(namespace, initiator, action, value.toString).inc())\n    }\n  }\n\n  case class PrometheusMetrics() extends PrometheusMetricNames {\n\n    private val namespace = config.renameTags.getOrElse(actionNamespace, actionNamespace)\n    private val initiator = config.renameTags.getOrElse(initiatorNamespace, initiatorNamespace)\n    private val action = config.renameTags.getOrElse(actionName, actionName)\n    private val kind = config.renameTags.getOrElse(actionKind, actionKind)\n    private val memory = config.renameTags.getOrElse(actionMemory, actionMemory)\n    private val status = config.renameTags.getOrElse(actionStatus, actionStatus)\n    private val statusCode = config.renameTags.getOrElse(userDefinedStatusCode, userDefinedStatusCode)\n\n    val activationCounter =\n      counter(activationMetric, \"Activation Count\", namespace, initiator, action, kind, memory)\n\n    val coldStartCounter =\n      counter(coldStartMetric, \"Cold start counts\", namespace, initiator, action)\n\n    val statusCounter =\n      counter(statusMetric, \"Activation failure status type\", namespace, initiator, action, status)\n\n    val userDefinedStatusCodeCounter =\n      counter(\n        userDefinedStatusCodeMetric,\n        \"status code returned in action result response set by developer\",\n        namespace,\n        initiator,\n        action,\n        statusCode)\n\n    val waitTimeHisto =\n      histogram(waitTimeMetric, \"Internal system hold time\", namespace, initiator, action)\n\n    val initTimeHisto =\n      histogram(initTimeMetric, \"Time it took to initialize an action, e.g. docker init\", namespace, initiator, action)\n\n    val durationHisto =\n      histogram(durationMetric, \"Actual time the action code was running\", namespace, initiator, action)\n\n    val responseSizeHisto =\n      Histogram\n        .build()\n        .name(responseSizeMetric)\n        .help(\"Activation Response size\")\n        .labelNames(namespace, initiator, action)\n        .linearBuckets(0, ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT.toBytes.toDouble, 10)\n        .register()\n\n    val memoryGauge =\n      gauge(memoryMetric, \"Memory consumption of the action containers\", namespace, initiator, action)\n\n    val concurrentLimitCounter =\n      counter(concurrentLimitMetric, \"a user has exceeded its limit for concurrent invocations\", namespace)\n\n    val timedLimitCounter =\n      counter(timedLimitMetric, \"the user has reached its per minute limit for the number of invocations\", namespace)\n\n    private def counter(name: String, help: String, tags: String*) =\n      Counter\n        .build()\n        .name(name)\n        .help(help)\n        .labelNames(tags: _*)\n        .register()\n\n    private def gauge(name: String, help: String, tags: String*) =\n      Gauge\n        .build()\n        .name(name)\n        .help(help)\n        .labelNames(tags: _*)\n        .register()\n\n    private def histogram(name: String, help: String, tags: String*) =\n      Histogram\n        .build()\n        .name(name)\n        .help(help)\n        .labelNames(tags: _*)\n        .register()\n  }\n\n  //Returns a floating point number\n  private def seconds(time: Duration): Double = time.toUnit(TimeUnit.SECONDS)\n\n  private def createSource() =\n    Source.combine(createJavaClientSource(), createKamonSource())(Concat(_)).map(ByteString(_))\n\n  /**\n   * Enables streaming the prometheus metric data without building the whole report in memory\n   */\n  private def createJavaClientSource() =\n    Source\n      .fromIterator(() => CollectorRegistry.defaultRegistry.metricFamilySamples().asScala)\n      .map { sample =>\n        //Stream string representation of one sample at a time\n        val writer = new StringWriter()\n        TextFormat.write004(writer, singletonEnumeration(sample))\n        writer.toString\n      }\n\n  private def createKamonSource() = Source.single(kamon.scrapeData())\n\n  private def singletonEnumeration[A](value: A) = new util.Enumeration[A] {\n    private var done = false\n    override def hasMoreElements: Boolean = !done\n    override def nextElement(): A = {\n      if (done) throw new NoSuchElementException\n      done = true\n      value\n    }\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/test/resources/application.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nuser-events {\n  # Server port\n  port = 9095\n\n  # Enables KamonRecorder so as to enable sending metrics to Kamon supported backends\n  # like DataDog\n  enable-kamon = false\n\n  # Namespaces that should not be monitored\n  ignored-namespaces = [\"guest\"]\n\n  rename-tags {\n    #namespace = \"ow_namespace\"\n  }\n\n  retry {\n    # minimum (initial) duration until the Kafka consumer is started again if it is terminated\n    min-backoff = 3 secs\n\n    # the exponential back-off is capped to this duration\n    max-backoff = 30 secs\n\n    # after calculation of the exponential back-off an additional\n    # random delay based on this factor is added, e.g. `0.2` adds up to `20%` delay\n    random-factor = 0.2\n\n    # the amount of restarts is capped to this amount within a time frame of minBackoff\n    max-restarts = 10\n  }\n\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  ~ Licensed to the Apache Software Foundation (ASF) under one or more\n  ~ contributor license agreements.  See the NOTICE file distributed with\n  ~ this work for additional information regarding copyright ownership.\n  ~ The ASF licenses this file to You under the Apache License, Version 2.0\n  ~ (the \"License\"); you may not use this file except in compliance with\n  ~ the License.  You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<configuration>\n    <jmxConfigurator></jmxConfigurator>\n    <appender name=\"console\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>[%d{yyyy-MM-dd'T'HH:mm:ss.SSS'Z'}] [%p] [%logger] %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <!-- Apache HttpClient -->\n    <logger name=\"org.apache.http\" level=\"ERROR\" />\n\n    <!-- Kafka -->\n    <logger name=\"org.apache.kafka\" level=\"ERROR\" />\n    <logger name=\"kafka\" level=\"ERROR\" />\n\n    <!-- Zookeeper -->\n    <logger name=\"org.apache.zookeeper\" level=\"ERROR\" />\n    <logger name=\"org.apache.curator\" level=\"ERROR\" />\n\n    <logger name=\"org.apache.pekko.event.slf4j.Slf4jLogger\" level=\"WARN\" />\n\n    <root level=\"${logback.log.level:-INFO}\">\n        <appender-ref ref=\"console\" />\n    </root>\n</configuration>"
  },
  {
    "path": "core/monitoring/user-events/src/test/scala/org/apache/openwhisk/core/monitoring/metrics/ApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport org.apache.pekko.http.scaladsl.model.headers.HttpEncodings._\nimport org.apache.pekko.http.scaladsl.model.headers.{`Accept-Encoding`, `Content-Encoding`, HttpEncoding, HttpEncodings}\nimport org.apache.pekko.http.scaladsl.model.{HttpCharsets, HttpEntity, HttpResponse}\nimport org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest\nimport kamon.prometheus.PrometheusReporter\nimport org.apache.openwhisk.core.monitoring.metrics.OpenWhiskEvents.MetricConfig\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.matchers.Matcher\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport pureconfig.loadConfigOrThrow\nimport io.prometheus.client.CollectorRegistry\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.duration.DurationInt\n\n@RunWith(classOf[JUnitRunner])\nclass ApiTests\n    extends AnyFlatSpec\n    with Matchers\n    with ScalatestRouteTest\n    with EventsTestHelper\n    with ScalaFutures\n    with BeforeAndAfterAll {\n  implicit val timeoutConfig = PatienceConfig(1.minute)\n\n  private var api: PrometheusEventsApi = _\n  private var consumer: EventConsumer = _\n\n  override protected def beforeAll(): Unit = {\n    super.beforeAll()\n    CollectorRegistry.defaultRegistry.clear()\n    val metricConfig = loadConfigOrThrow[MetricConfig](system.settings.config, \"user-events\")\n    val mericRecorder = PrometheusRecorder(new PrometheusReporter, metricConfig)\n    consumer = createConsumer(56754, system.settings.config, mericRecorder)\n    api = new PrometheusEventsApi(consumer, createExporter())\n  }\n\n  protected override def afterAll(): Unit = {\n    consumer.shutdown().futureValue\n    super.afterAll()\n  }\n\n  behavior of \"EventsApi\"\n\n  it should \"respond ping request\" in {\n    Get(\"/ping\") ~> api.routes ~> check {\n      //Due to retries using a random port does not immediately result in failure\n      handled shouldBe true\n    }\n  }\n\n  it should \"respond metrics request\" in {\n    Get(\"/metrics\") ~> `Accept-Encoding`(gzip) ~> api.routes ~> check {\n      contentType.charsetOption shouldBe Some(HttpCharsets.`UTF-8`)\n      contentType.mediaType.params(\"version\") shouldBe \"0.0.4\"\n      response should haveContentEncoding(gzip)\n    }\n  }\n\n  private def haveContentEncoding(encoding: HttpEncoding): Matcher[HttpResponse] =\n    be(encoding) compose {\n      (_: HttpResponse).header[`Content-Encoding`].map(_.encodings.head).getOrElse(HttpEncodings.identity)\n    }\n\n  private def createExporter(): PrometheusExporter = () => HttpEntity(PrometheusExporter.textV4, \"foo\".getBytes)\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/test/scala/org/apache/openwhisk/core/monitoring/metrics/EventsTestHelper.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport java.net.ServerSocket\n\nimport org.apache.pekko.actor.ActorSystem\nimport com.typesafe.config.Config\nimport org.apache.openwhisk.core.monitoring.metrics.OpenWhiskEvents.MetricConfig\nimport pureconfig._\nimport pureconfig.generic.auto._\n\ntrait EventsTestHelper {\n\n  protected def createConsumer(kport: Int, globalConfig: Config, recorder: MetricRecorder)(\n    implicit system: ActorSystem) = {\n    val settings = OpenWhiskEvents\n      .eventConsumerSettings(OpenWhiskEvents.defaultConsumerConfig(globalConfig))\n      .withBootstrapServers(s\"localhost:$kport\")\n    val metricConfig = loadConfigOrThrow[MetricConfig](globalConfig, \"user-events\")\n    EventConsumer(settings, Seq(recorder), metricConfig)\n  }\n\n  protected def freePort(): Int = {\n    val socket = new ServerSocket(0)\n    try socket.getLocalPort\n    finally if (socket != null) socket.close()\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/test/scala/org/apache/openwhisk/core/monitoring/metrics/KafkaSpecBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport org.apache.pekko.kafka.testkit.scaladsl.ScalatestKafkaSpec\nimport io.github.embeddedkafka.{EmbeddedKafka, EmbeddedKafkaConfig}\nimport org.scalatest.Suite\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.{Eventually, IntegrationPatience, ScalaFutures}\n\nimport scala.concurrent.duration.{DurationInt, FiniteDuration}\n\nabstract class KafkaSpecBase\n    extends ScalatestKafkaSpec(0)\n    with Matchers\n    with ScalaFutures\n    with AnyFlatSpecLike\n    with EmbeddedKafka\n    with IntegrationPatience\n    with Eventually\n    with EventsTestHelper { this: Suite =>\n  implicit val timeoutConfig: PatienceConfig = PatienceConfig(1.minute)\n  override val sleepAfterProduce: FiniteDuration = 10.seconds\n  override protected val topicCreationTimeout = 60.seconds\n  override protected val producerPublishTimeout: FiniteDuration = 60.seconds\n\n  implicit val embeddedKafkaConfig: EmbeddedKafkaConfig =\n    EmbeddedKafkaConfig(kafkaPort = freePort(), zooKeeperPort = freePort())\n\n  override def bootstrapServers = s\"localhost:${embeddedKafkaConfig.kafkaPort}\"\n\n  override def setUp(): Unit = {\n    EmbeddedKafka.start()(embeddedKafkaConfig)\n    super.setUp()\n  }\n\n  override def cleanUp(): Unit = {\n    super.cleanUp()\n    EmbeddedKafka.stop()\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/test/scala/org/apache/openwhisk/core/monitoring/metrics/KamonRecorderTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport java.time.Duration\n\nimport com.typesafe.config.{Config, ConfigFactory}\nimport kamon.metric.PeriodSnapshot\nimport kamon.module.MetricReporter\nimport kamon.Kamon\nimport kamon.tag.Lookups\nimport org.apache.openwhisk.core.connector.{Activation, EventMessage}\nimport org.apache.openwhisk.core.entity.{ActivationId, ActivationResponse, Subject, UUID}\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass KamonRecorderTests extends KafkaSpecBase with BeforeAndAfterEach with KamonMetricNames {\n  var reporter: MetricReporter = _\n\n  override protected def beforeEach(): Unit = {\n    super.beforeEach()\n    TestReporter.reset()\n    val newConfig = ConfigFactory.parseString(\"\"\"kamon {\n        |  metric {\n        |    tick-interval = 50 ms\n        |    optimistic-tick-alignment = no\n        |  }\n        |}\"\"\".stripMargin).withFallback(ConfigFactory.load())\n    Kamon.registerModule(\"test\", TestReporter)\n    Kamon.reconfigure(newConfig)\n    reporter = TestReporter\n  }\n\n  override protected def afterEach(): Unit = {\n    reporter.stop()\n    Kamon.reconfigure(ConfigFactory.load())\n    super.afterEach()\n  }\n\n  behavior of \"KamonConsumer\"\n\n  val namespaceDemo = \"demo\"\n  val namespaceGuest = \"guest\"\n  val actionWithCustomPackage = \"apimgmt/createApi\"\n  val actionWithDefaultPackage = \"createApi\"\n  val kind = \"nodejs:20\"\n  val memory = 256\n\n  it should \"push user events to kamon\" in {\n    createCustomTopic(EventConsumer.userEventTopic)\n\n    val consumer = createConsumer(embeddedKafkaConfig.kafkaPort, system.settings.config, KamonRecorder)\n\n    publishStringMessageToKafka(\n      EventConsumer.userEventTopic,\n      newActivationEvent(s\"$namespaceDemo/$actionWithCustomPackage\").serialize)\n    publishStringMessageToKafka(\n      EventConsumer.userEventTopic,\n      newActivationEvent(s\"$namespaceDemo/$actionWithDefaultPackage\").serialize)\n\n    publishStringMessageToKafka(\n      EventConsumer.userEventTopic,\n      newActivationEvent(s\"$namespaceGuest/$actionWithDefaultPackage\").serialize)\n\n    sleep(sleepAfterProduce, \"sleeping post produce\")\n    consumer.shutdown().futureValue\n    sleep(4.second, \"sleeping for Kamon reporters to get invoked\")\n\n    // Custom package\n    TestReporter.counter(activationMetric, namespaceDemo, actionWithCustomPackage)(0).value shouldBe 1\n    TestReporter\n      .counter(activationMetric, namespaceDemo, actionWithCustomPackage)\n      .filter((t) => t.tags.get(Lookups.plain(actionMemory)) == memory.toString)(0)\n      .value shouldBe 1\n    TestReporter\n      .counter(activationMetric, namespaceDemo, actionWithCustomPackage)\n      .filter((t) => t.tags.get(Lookups.plain(actionKind)) == kind)(0)\n      .value shouldBe 1\n    TestReporter\n      .counter(statusMetric, namespaceDemo, actionWithCustomPackage)\n      .filter((t) => t.tags.get(Lookups.plain(actionStatus)) == ActivationResponse.statusDeveloperError)(0)\n      .value shouldBe 1\n    TestReporter.counter(coldStartMetric, namespaceDemo, actionWithCustomPackage)(0).value shouldBe 1\n    TestReporter.histogram(waitTimeMetric, namespaceDemo, actionWithCustomPackage).size shouldBe 1\n    TestReporter.histogram(initTimeMetric, namespaceDemo, actionWithCustomPackage).size shouldBe 1\n    TestReporter.histogram(durationMetric, namespaceDemo, actionWithCustomPackage).size shouldBe 1\n\n    // Default package\n    TestReporter.histogram(durationMetric, namespaceDemo, actionWithDefaultPackage).size shouldBe 1\n\n    // Blacklisted namespace should not be tracked\n    TestReporter.counter(activationMetric, namespaceGuest, actionWithDefaultPackage) shouldBe empty\n\n  }\n\n  private def newActivationEvent(actionPath: String) =\n    EventMessage(\n      \"test\",\n      Activation(\n        actionPath,\n        ActivationId.generate().asString,\n        2,\n        3.millis,\n        5.millis,\n        11.millis,\n        kind,\n        false,\n        memory,\n        None),\n      Subject(\"testuser\"),\n      actionPath.split(\"/\")(0),\n      UUID(\"test\"),\n      Activation.typeName)\n\n  private object TestReporter extends MetricReporter {\n    var snapshotAccumulator = PeriodSnapshot.accumulator(Duration.ofDays(1), Duration.ZERO)\n    override def reportPeriodSnapshot(snapshot: PeriodSnapshot): Unit = {\n      snapshotAccumulator.add(snapshot)\n    }\n\n    override def stop(): Unit = {}\n    override def reconfigure(config: Config): Unit = {}\n\n    def reset(): Unit = {\n      snapshotAccumulator = PeriodSnapshot.accumulator(Duration.ofDays(1), Duration.ZERO)\n    }\n\n    def counter(metricName: String, namespace: String, action: String) = {\n      snapshotAccumulator\n        .peek()\n        .counters\n        .filter(_.name == metricName)\n        .flatMap(_.instruments)\n        .filter(_.tags.get(Lookups.plain(actionNamespace)) == namespace)\n        .filter(_.tags.get(Lookups.plain(initiatorNamespace)) == namespace)\n        .filter(_.tags.get(Lookups.plain(actionName)) == action)\n    }\n\n    def namespaceCounter(metricName: String, namespace: String) = {\n      snapshotAccumulator\n        .peek()\n        .counters\n        .filter(_.name == metricName)\n        .flatMap(_.instruments)\n        .filter(_.tags.get(Lookups.plain(actionNamespace)) == namespace)\n        .filter(_.tags.get(Lookups.plain(initiatorNamespace)) == namespace)\n    }\n\n    def histogram(metricName: String, namespace: String, action: String) = {\n      snapshotAccumulator\n        .peek()\n        .histograms\n        .filter(_.name == metricName)\n        .flatMap(_.instruments)\n        .filter(_.tags.get(Lookups.plain(actionNamespace)) == namespace)\n        .filter(_.tags.get(Lookups.plain(initiatorNamespace)) == namespace)\n        .filter(_.tags.get(Lookups.plain(actionName)) == action)\n    }\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/test/scala/org/apache/openwhisk/core/monitoring/metrics/OpenWhiskEventsTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.model.{HttpRequest, StatusCodes}\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport com.typesafe.config.ConfigFactory\nimport io.prometheus.client.CollectorRegistry\nimport kamon.Kamon\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration._\nimport scala.util.Try\n\n@RunWith(classOf[JUnitRunner])\nclass OpenWhiskEventsTests extends KafkaSpecBase {\n  behavior of \"Server\"\n\n  it should \"start working http server\" in {\n    val httpPort = freePort()\n    val globalConfig = system.settings.config\n    val config = ConfigFactory.parseString(s\"\"\"\n           | pekko.kafka.consumer.kafka-clients {\n           |  bootstrap.servers = \"localhost:${embeddedKafkaConfig.kafkaPort}\"\n           | }\n           | kamon {\n           |  metric {\n           |    tick-interval = 50 ms\n           |    optimistic-tick-alignment = no\n           |  }\n           | }\n           | whisk {\n           |  user-events {\n           |    port = $httpPort\n           |    rename-tags {\n           |      namespace = \"ow_namespace\"\n           |    }\n           |  }\n           | }\n         \"\"\".stripMargin).withFallback(globalConfig)\n    CollectorRegistry.defaultRegistry.clear()\n    val binding = OpenWhiskEvents.start(config).futureValue\n    val res = get(\"localhost\", httpPort, \"/ping\")\n    res shouldBe Some(StatusCodes.OK, \"pong\")\n\n    //Check if metrics using Kamon API gets included in consolidated Prometheus\n    Kamon.counter(\"fooTest\").withoutTags().increment(42)\n    sleep(1.second)\n    val metricRes = get(\"localhost\", httpPort, \"/metrics\")\n    metricRes.get._2 should include(\"fooTest\")\n\n    binding.unbind().futureValue\n  }\n\n  def get(host: String, port: Int, path: String = \"/\") = {\n    val response = Try {\n      Http()\n        .singleRequest(HttpRequest(uri = s\"http://$host:$port$path\"))\n        .futureValue\n    }.toOption\n\n    response.map { res =>\n      (res.status, Unmarshal(res).to[String].futureValue)\n    }\n  }\n}\n"
  },
  {
    "path": "core/monitoring/user-events/src/test/scala/org/apache/openwhisk/core/monitoring/metrics/PrometheusRecorderTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.monitoring.metrics\n\nimport com.typesafe.config.ConfigFactory\nimport io.prometheus.client.CollectorRegistry\nimport kamon.prometheus.PrometheusReporter\nimport org.apache.openwhisk.core.connector.{Activation, EventMessage}\nimport org.apache.openwhisk.core.entity.{ActivationId, ActivationResponse, Subject, UUID}\nimport org.apache.openwhisk.core.monitoring.metrics.OpenWhiskEvents.MetricConfig\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatestplus.junit.JUnitRunner\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass PrometheusRecorderTests extends KafkaSpecBase with BeforeAndAfterEach with PrometheusMetricNames {\n  behavior of \"PrometheusConsumer\"\n  val namespaceDemo = \"demo\"\n  val namespaceGuest = \"guest\"\n  val actionWithCustomPackage = \"apimgmt/createApiOne\"\n  val actionWithDefaultPackage = \"createApi\"\n  val kind = \"nodejs:20\"\n  val memory = \"256\"\n  createCustomTopic(EventConsumer.userEventTopic)\n\n  it should \"push user events to kamon\" in {\n    CollectorRegistry.defaultRegistry.clear()\n    val metricConfig = loadConfigOrThrow[MetricConfig](system.settings.config, \"user-events\")\n    val metricRecorder = PrometheusRecorder(new PrometheusReporter, metricConfig)\n    val consumer = createConsumer(embeddedKafkaConfig.kafkaPort, system.settings.config, metricRecorder)\n    publishStringMessageToKafka(\n      EventConsumer.userEventTopic,\n      newActivationEvent(s\"$namespaceDemo/$actionWithCustomPackage\", kind, memory).serialize)\n    publishStringMessageToKafka(\n      EventConsumer.userEventTopic,\n      newActivationEvent(s\"$namespaceDemo/$actionWithDefaultPackage\", kind, memory).serialize)\n\n    publishStringMessageToKafka(\n      EventConsumer.userEventTopic,\n      newActivationEvent(s\"$namespaceGuest/$actionWithDefaultPackage\", kind, memory).serialize)\n\n    // Custom package\n    sleep(sleepAfterProduce, \"sleeping post produce\")\n    consumer.shutdown().futureValue\n    counterTotal(activationMetric, namespaceDemo, actionWithCustomPackage) shouldBe 1\n    counter(coldStartMetric, namespaceDemo, actionWithCustomPackage) shouldBe 1\n    counterStatus(statusMetric, namespaceDemo, actionWithCustomPackage, ActivationResponse.statusDeveloperError) shouldBe 1\n\n    histogramCount(waitTimeMetric, namespaceDemo, actionWithCustomPackage) shouldBe 1\n    histogramSum(waitTimeMetric, namespaceDemo, actionWithCustomPackage) shouldBe (0.03 +- 0.001)\n\n    histogramCount(initTimeMetric, namespaceDemo, actionWithCustomPackage) shouldBe 1\n    histogramSum(initTimeMetric, namespaceDemo, actionWithCustomPackage) shouldBe (433.433 +- 0.01)\n\n    histogramCount(durationMetric, namespaceDemo, actionWithCustomPackage) shouldBe 1\n    histogramSum(durationMetric, namespaceDemo, actionWithCustomPackage) shouldBe (1.254 +- 0.01)\n\n    gauge(memoryMetric, namespaceDemo, actionWithCustomPackage).intValue shouldBe 256\n\n    // Default package\n    counterTotal(activationMetric, namespaceDemo, actionWithDefaultPackage) shouldBe 1\n\n    // Blacklisted namespace should not be tracked\n    counterTotal(activationMetric, namespaceGuest, actionWithDefaultPackage) shouldBe (null)\n  }\n\n  it should \"push user event to kamon with prometheus metrics tags relabel\" in {\n    val httpPort = freePort()\n    val globalConfig = system.settings.config\n    val config = ConfigFactory.parseString(s\"\"\"\n            | whisk {\n            |  user-events {\n            |    port = $httpPort\n            |    enable-kamon = false\n            |    ignored-namespaces = [\"guest\"]\n            |    rename-tags {\n            |      namespace = \"ow_namespace\"\n            |    }\n            |    retry {\n            |      min-backoff = 3 secs\n            |      max-backoff = 30 secs\n            |      random-factor = 0.2\n            |      max-restarts = 10\n            |    }\n            |  }\n            | }\n         \"\"\".stripMargin)\n    CollectorRegistry.defaultRegistry.clear()\n    val metricConfig = loadConfigOrThrow[MetricConfig](config, \"whisk.user-events\")\n    val metricRecorder = PrometheusRecorder(new PrometheusReporter, metricConfig)\n    val consumer = createConsumer(embeddedKafkaConfig.kafkaPort, system.settings.config, metricRecorder)\n\n    publishStringMessageToKafka(\n      EventConsumer.userEventTopic,\n      newActivationEvent(s\"$namespaceDemo/$actionWithCustomPackage\", kind, memory).serialize)\n\n    sleep(sleepAfterProduce, \"sleeping post produce\")\n    consumer.shutdown().futureValue\n    CollectorRegistry.defaultRegistry.getSampleValue(\n      activationMetric,\n      Array(\"ow_namespace\", \"initiator\", \"action\", \"kind\", \"memory\"),\n      Array(namespaceDemo, namespaceDemo, actionWithCustomPackage, kind, memory)) shouldBe 1\n  }\n  private def newActivationEvent(actionPath: String, kind: String, memory: String) =\n    EventMessage(\n      \"test\",\n      Activation(\n        actionPath,\n        ActivationId.generate().asString,\n        2,\n        1254.millis,\n        30.millis,\n        433433.millis,\n        kind,\n        false,\n        memory.toInt,\n        None),\n      Subject(\"testuser\"),\n      actionPath.split(\"/\")(0),\n      UUID(\"test\"),\n      Activation.typeName)\n\n  private def gauge(metricName: String, namespace: String, action: String) =\n    CollectorRegistry.defaultRegistry.getSampleValue(\n      metricName,\n      Array(\"namespace\", \"initiator\", \"action\"),\n      Array(namespace, namespace, action))\n\n  private def counter(metricName: String, namespace: String, action: String) =\n    CollectorRegistry.defaultRegistry.getSampleValue(\n      metricName,\n      Array(\"namespace\", \"initiator\", \"action\"),\n      Array(namespace, namespace, action))\n\n  private def counterTotal(metricName: String, namespace: String, action: String) =\n    CollectorRegistry.defaultRegistry.getSampleValue(\n      metricName,\n      Array(\"namespace\", \"initiator\", \"action\", \"kind\", \"memory\"),\n      Array(namespace, namespace, action, kind, memory))\n\n  private def counterStatus(metricName: String, namespace: String, action: String, status: String) =\n    CollectorRegistry.defaultRegistry.getSampleValue(\n      metricName,\n      Array(\"namespace\", \"initiator\", \"action\", \"status\"),\n      Array(namespace, namespace, action, status))\n\n  private def histogramCount(metricName: String, namespace: String, action: String) =\n    CollectorRegistry.defaultRegistry.getSampleValue(\n      s\"${metricName}_count\",\n      Array(\"namespace\", \"initiator\", \"action\"),\n      Array(namespace, namespace, action))\n\n  private def histogramSum(metricName: String, namespace: String, action: String) =\n    CollectorRegistry.defaultRegistry\n      .getSampleValue(\n        s\"${metricName}_sum\",\n        Array(\"namespace\", \"initiator\", \"action\"),\n        Array(namespace, namespace, action))\n      .doubleValue()\n}\n"
  },
  {
    "path": "core/routemgmt/common/apigw-utils.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Route management action common API GW utilities\n */\nvar request = require('request');\nvar _ = require('lodash');\n\nconst ApimgmtUserAgent = \"OpenWhisk-apimgmt/1.0.0\";\nvar UserAgent = ApimgmtUserAgent;\n\n/**\n * Helper method for the validateFinalSwagger function. Generates a map of operationId to target-url strings so we\n * can validate that each operationId we find that has a parameter in the path also has its target-url appended with\n * $(request.path)\n *\n * @param ibmConfig   Required. The 'x-ibm-configuration' portion of the swaggerApi.\n * @return A map of operationId->target-url pairs for checking.\n */\nfunction generateTargetUrlMap(ibmConfig) {\n  var targetUrls = {};\n  ibmConfig['assembly']['execute'].forEach(function(exec) {\n    if (exec['operation-switch'] && exec['operation-switch']['case']) {\n      exec['operation-switch']['case'].forEach(function(element) {\n        var operations = element['operations'];\n        var execs = element['execute'];\n        //each nth element of execs and operations go together, so lets add those to the map.\n        for (var i = 0; i < operations.length ; ++i) {\n          if(i < execs.length && execs[i] && execs[i]['invoke'] && execs[i]['invoke']['target-url']) {\n            targetUrls[operations[i]] = execs[i]['invoke']['target-url'];\n          }\n        }\n      });\n    }\n  });\n  return targetUrls;\n}\n\n/**\n * Helper function that just validates whether a relative path meets the following conditions:\n * 1. It has not path parameters\n * 2. If it has path parameters, that the parameters are well formed (i.e. each param is surrounded by {}).\n *\n * @param relativePath   Required. The relative path we are checking.\n * @return True if the path is valid, false otherwise.\n */\nfunction isValidRelativePath(relativePath) {\n  var validParamRegex = /\\/\\{([^\\/]+)\\}\\/|\\/\\{([^\\/]+)\\}$/g;\n  if (relativePath.match(validParamRegex)) {\n    return true;\n  }\n  return false;\n}\n\n/**\n * Simple function to get the name of each path parameter defined in the path.\n *\n * @param path   Required. The path we are checking.\n * @return An array that contains each named path parameter, or an empty list if none are found.\n */\nfunction getPathParameters(relativePath) {\n  var params = [];\n  var validNameRegex = /\\{([^\\/]+)\\}/g\n  //Match returns all the matches found, including the {} chars. so we have to remove them.\n  var namesFound = relativePath.match(validNameRegex);\n  if (namesFound) {\n    params = namesFound.map(function (pathName){\n      return pathName.substring(1,pathName.length-1);\n    });\n  }\n  return params;\n}\n\n/**\n * Currently this only checks the final swagger that will be passed into API GW whether the path parameter definition\n * is correct.\n *\n * @param swaggerApi   Required. The API swagger object to send to the API gateway\n * @return A promise with the fully validated swaggerApi, or an error response if rejected.\n */\nfunction validateFinalSwagger(swaggerApi) {\n  return new Promise(function(resolve, reject) {\n    // This returns a map of urls to check for path parameters.\n    console.log(\"validateFinalSwagger: Validating swapper before posting to API GW.\")\n    var errorMsg;\n    var paths = swaggerApi['paths'];\n\n    if (swaggerApi.basePath && isValidRelativePath(swaggerApi.basePath)) {\n      errorMsg = \"The base path (\" + swaggerApi.basePath + \") cannot have parameters. Only the relative path supports path parameters.\";\n    }\n    /*\n     * This code will look at each path defined, and look at all the parameters in each path, and validate that each\n     * verb (GET,POST, etc) for each path defines parameter objects for each parameter defined in the path. For each of\n     * these that contain parameters, it will also check that its target-url ends in $(request.path).\n     * #beginPathValidation\n     */\n    var targetUrlMap = generateTargetUrlMap(swaggerApi['x-ibm-configuration']);\n    for (var key in paths) {\n      if (errorMsg) { break; }\n      var idx = 0;\n      if (isValidRelativePath(key)) {\n        //Path is valid, lets check that we have parameters defined for each path parameter and that the target-url\n        //has $(request.path) at the end.\n        var namedParamsInPath = getPathParameters(key);\n        //Loop over each verb (GET,POST,etc), each should contain path parameters.\n        var parameters = paths[key]['parameters'] ? paths[key]['parameters'] : [];\n        for (var httpType in paths[key]) {\n          if (httpType == \"parameters\") {\n            continue;\n          }\n          var xOpenWhisk = paths[key][httpType]['x-openwhisk']\n          if (xOpenWhisk && xOpenWhisk['url'] && !xOpenWhisk['url'].endsWith('.http')) {\n            errorMsg = \"The action must use a response type of '.http' in order to receive the path parameters.\";\n            break;\n          }\n          var opId = paths[key][httpType].operationId;\n          if (targetUrlMap[opId] && !targetUrlMap[opId].endsWith('.http$(request.path)')) {\n            errorMsg = \"The target-url for operationId '\" + opId;\n            errorMsg += \"' must end in '$(request.path)' in order for actions to receive the path parameters.\";\n            break;\n          }\n          var allParams = parameters.concat(paths[key][httpType].parameters);\n          for (var i = 0 ; i < namedParamsInPath.length ; ++i) {\n            var found = false;\n            for (var j in allParams) {\n              if (allParams[j].name == namedParamsInPath[i]) {\n                found = true;\n                break;\n              }\n            }\n            if (!found) {\n              errorMsg = \"The parameter '\" + namedParamsInPath[i] + \"' defined in path '\" + key + \"' does not match any\";\n              errorMsg += \" of the parameters defined for the path in the swagger file.\";\n              break;\n            }\n          }\n          if(errorMsg) { break; }\n        }\n        if(errorMsg) { break; }\n      }\n    }\n    //#endPathValidation\n    if (errorMsg) {\n      console.error(\"validateFinalSwagger:\" + errorMsg)\n      reject(errorMsg);\n    } else {\n      console.log(\"validateFinalSwagger: Validation of swagger before posting to API GW was successful.\")\n      resolve(swaggerApi);\n    }\n  });\n}\n\n\n/**\n * Configures an API route on the API Gateway.  This API will map to an OpenWhisk action that\n * will be invoked by the API Gateway when the API route is accessed.\n *\n * @param gwInfo Required.\n * @param    gwUrl     Required. The base URL gateway path (i.e.  'PROTOCOL://gw.host.domain:PORT/CONTEXT')\n * @param    gwAuth    Required. The user bearer token used to access the API Gateway REST endpoints\n * @param spaceGuid    Required. User's space guid.  APIs are stored under this context\n * @param swaggerApi   Required. The API swagger object to send to the API gateway\n * @param apiId        Required. API id. When specified, the API exists and will be updated; otherwise the API is created anew\n * @return A promise for an object describing the result with fields error and response\n */\nfunction addApiToGateway(gwInfo, spaceGuid, swaggerApi, apiId) {\n  var requestFcn = request.post;\n\n  console.log('addApiToGateway: ');\n  try {\n    var options = {\n      followAllRedirects: true,\n      url: gwInfo.gwUrl+'/'+encodeURIComponent(spaceGuid) + '/apis',\n      json: swaggerApi,  // Use of json automatically sets header: 'Content-Type': 'application/json'\n      headers: {\n        'User-Agent': UserAgent\n      }\n    };\n    if (gwInfo.gwAuth) {\n      _.set(options, \"headers.Authorization\", 'Bearer ' + gwInfo.gwAuth);\n    }\n\n    if (apiId) {\n      console.log(\"addApiToGateway: Updating existing API\");\n      options.url = gwInfo.gwUrl + '/' + encodeURIComponent(spaceGuid) + '/apis/' + encodeURIComponent(apiId);\n      requestFcn = request.put;\n    }\n\n    console.log('addApiToGateway: request: '+JSON.stringify(options, \" \", 2));\n  }\n  catch (e) {\n    console.error('addApiToGateway exception: '+e);\n  }\n  return new Promise(function(resolve, reject) {\n    requestFcn(options, function(error, response, body) {\n      var statusCode = response ? response.statusCode : undefined;\n      console.log('addApiToGateway: response status:'+ statusCode);\n      if (error) console.error('Warning: addRouteToGateway request failed: '+ makeJsonString(error));\n      if (response && response.headers) console.log('addApiToGateway: response headers: '+makeJsonString(response.headers));\n      if (body) console.log('addApiToGateway: response body: '+makeJsonString(body));\n      if (error) {\n        console.error('addApiToGateway: Unable to configure the API Gateway');\n        reject('Unable to configure the API Gateway: '+makeJsonString(error));\n      } else if (statusCode != 200) {\n        if (body) {\n          var errMsg = makeJsonString(body);\n          if (body.error && body.error.message) errMsg = body.error.message;\n          reject('Unable to configure the API Gateway (status code '+statusCode+'): '+ errMsg);\n        } else {\n          reject('Unable to configure the API Gateway: Response failure code: '+statusCode);\n        }\n      } else if (!body) {\n        console.error('addApiToGateway: Unable to configure the API Gateway: No response body');\n        reject('Unable to configure the API Gateway: No response received from the API Gateway');\n      } else {\n        resolve(body);\n      }\n    });\n  });\n}\n\n/**\n * Removes an API route from the API Gateway.\n *\n * @param gwInfo     Required.\n * @param    gwUrl   Required. The base URL gateway path (i.e.  'PROTOCOL://gw.host.domain:PORT/CONTEXT')\n * @param    gwAuth  Optional. The credentials used to access the API Gateway REST endpoints\n * @param spaceGuid  Required. User's space guid.  APIs are stored under this context\n * @param apiId      Required.  API basepath.  Unique per spaceGuid\n * @return A promise for an object describing the result with fields error and response\n */\nfunction deleteApiFromGateway(gwInfo, spaceGuid, apiId) {\n  var options = {\n    followAllRedirects: true,\n    url: gwInfo.gwUrl+'/'+encodeURIComponent(spaceGuid)+'/apis/'+encodeURIComponent(apiId),\n    agentOptions: {rejectUnauthorized: false},\n    headers: {\n      'Accept': 'application/json',\n      'User-Agent': UserAgent\n    }\n  };\n  if (gwInfo.gwAuth) {\n    options.headers.Authorization = 'Bearer ' + gwInfo.gwAuth;\n  }\n  console.log('deleteApiFromGateway: request: '+JSON.stringify(options));\n\n  return new Promise(function(resolve, reject) {\n    request.delete(options, function(error, response, body) {\n      var statusCode = response ? response.statusCode : undefined;\n      console.log('deleteApiFromGateway: response status:'+ statusCode);\n      if (error) console.error('Warning: deleteGatewayApi request failed: '+ makeJsonString(error));\n      if (body) console.log('deleteApiFromGateway: response body: '+makeJsonString(body));\n      if (response && response.headers) console.log('deleteApiFromGateway: response headers: '+makeJsonString(response.headers));\n      if (error) {\n        console.error('deleteApiFromGateway: Unable to delete the API Gateway');\n        reject('Unable to delete the API Gateway: '+makeJsonString(error));\n      } else if (statusCode != 200  && statusCode != 204) {\n        if (body) {\n          var errMsg = makeJsonString(body);\n          if (body.error && body.error.message) errMsg = body.error.message;\n          reject('Unable to delete the API Gateway (status code '+statusCode+'): '+ errMsg);\n        } else {\n          reject('Unable to delete the API Gateway: Response failure code: '+statusCode);\n        }\n      } else {\n        resolve();\n      }\n    });\n  });\n}\n\n/**\n * Return an array of APIs\n */\nfunction getApis(gwInfo, spaceGuid, bpOrApiName, limit, skip) {\n  var qsBasepath = { 'basePath' : bpOrApiName };\n  var qsApiName = { 'title' : bpOrApiName };\n  var qs;\n  if (bpOrApiName) {\n    if (bpOrApiName.indexOf('/') !== 0) {\n      console.log('getApis: querying APIs based on api name');\n      qs = qsApiName;\n    } else {\n      console.log('getApis: querying APIs based on basepath');\n      qs = qsBasepath;\n    }\n  }\n  var options = {\n    followAllRedirects: true,\n    url: gwInfo.gwUrl+'/'+encodeURIComponent(spaceGuid)+'/apis?limit='+limit+'&skip='+skip,\n    headers: {\n      'Accept': 'application/json',\n      'User-Agent': UserAgent\n    },\n    json: true\n  };\n  if (qs) {\n    options.qs = qs;\n  }\n  if (gwInfo.gwAuth) {\n    options.headers.Authorization = 'Bearer ' + gwInfo.gwAuth;\n  }\n  console.log('getApis: request: '+JSON.stringify(options));\n\n  return new Promise(function(resolve, reject) {\n    request.get(options, function(error, response, body) {\n      var statusCode = response ? response.statusCode : undefined;\n      console.log('getApis: response status: '+ statusCode);\n      if (error) console.error('Warning: getApis request failed: '+makeJsonString(error));\n      if (response && response.headers) console.log('getApis: response headers: '+makeJsonString(response.headers));\n      console.log('getApis: body type = '+typeof body);\n      if (body) console.log('getApis: response JSON.stringify(body): '+makeJsonString(body));\n      if (error) {\n        console.error('getApis: Unable to obtain API(s) from the API Gateway');\n        reject('Unable to obtain API(s) from the API Gateway: '+makeJsonString(error));\n      } else if (statusCode != 200) {\n        console.error('getApis: failure: response code: '+statusCode);\n        if (body) {\n          var errMsg = makeJsonString(body);\n          if (body.error && body.error.message) errMsg = body.error.message;\n          reject('Unable to obtain API(s) from the API Gateway (status code '+statusCode+'): '+ errMsg);\n        } else {\n          reject('Unable to obtain API(s) from the API Gateway: Response failure code: '+statusCode);\n        }\n      } else {\n        if (body) {\n          if (Array.isArray(body)) {\n            resolve(body);\n          } else {\n            console.error('getApis: Invalid API GW response body; a JSON array was not returned');\n            resolve( [] );\n          }\n        } else {\n          console.log('getApis: No APIs found');\n          resolve( [] );\n        }\n      }\n    });\n  });\n}\n\n/*\n * Convert API object array into specified format\n * Parameters:\n *  apis    : array of 0 or more APIs\n *  format  : 'apigw' or 'swagger'\n * Returns:\n *  array   : New array of API object - each in the specified format\n */\nfunction transformApis(apis, format) {\n  var apisOutput;\n  try {\n    if (format.toLowerCase() === 'apigw') {\n      apisOutput = apis;\n    } else if (format.toLowerCase() === 'swagger') {\n      apisOutput = JSON.parse(JSON.stringify(apis));\n      for (var i = 0; i < apisOutput.length; i++) {\n        apisOutput[i] = generateSwaggerApiFromGwApi(apisOutput[i]);\n      }\n    } else {\n      console.error('transformApis: Invalid format specification: '+format);\n      throw 'Internal error. Invalid format specification: '+format;\n    }\n  } catch(e) {\n    console.error('transformApis: exception caught: '+e);\n    throw 'API format transformation error: '+e;\n  }\n\n  return apisOutput;\n}\n\n/*\n * Convert API object into swagger JSON format\n * Parameters:\n *  gwApi  : API object as returned from the API Gateway\n * Returns:\n *  object : New API object in swagger JSON format\n */\nfunction generateSwaggerApiFromGwApi(gwApi) {\n  // Start with a copy of the gwApi object.  It's close to the desired swagger format\n  var swaggerApi = JSON.parse(JSON.stringify(gwApi));\n  swaggerApi.swagger = '2.0';\n  swaggerApi.info = {\n    title: gwApi.name,\n    version: '1.0.0'\n  };\n\n  // Copy the gwAPI's 'resources' object as the starting point for the swagger 'paths' object\n  swaggerApi.paths = JSON.parse(JSON.stringify(gwApi.resources));\n  for (var path in swaggerApi.paths) {\n    if (!swaggerApi.paths[path]) {\n      console.error('generateSwaggerApiFromGwApi: no operations defined for ignored relpath \\''+path+'\\'');\n      delete swaggerApi.paths[path];\n      continue;\n    }\n    for (var op in swaggerApi.paths[path].operations) {\n      console.log('generateSwaggerApiFromGwApi: processing path '+path+'; operation '+op);\n      if (!op) {\n        console.error('generateSwaggerApiFromGwApi: path \\''+path+'\\' has no operations!');\n        continue;\n      }\n      // swagger wants lower case operations\n      var oplower = op.toLowerCase();\n\n      // Valid swagger requires a 'responses' object for each operation\n      swaggerApi.paths[path][oplower] = {\n        responses: {\n          default: {\n            description: 'Default response'\n          }\n        }\n      };\n      // Custom swagger extension to hold the action mapping configuration\n      swaggerApi.paths[path][oplower]['x-ibm-op-ext'] = {\n        backendMethod : swaggerApi.paths[path].operations[op].backendMethod,\n        backendUrl : swaggerApi.paths[path].operations[op].backendUrl,\n        policies : JSON.parse(JSON.stringify(swaggerApi.paths[path].operations[op].policies)),\n        actionName: getActionNameFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl),\n        actionNamespace: getActionNamespaceFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl)\n      };\n    }\n    delete swaggerApi.paths[path].operations;\n  }\n  delete swaggerApi.resources;\n  delete swaggerApi.name;\n  delete swaggerApi.id;\n  delete swaggerApi.managedUrl;\n  delete swaggerApi.tenantId;\n  return swaggerApi;\n}\n\n/*\n * Take an API in JSON swagger format and create an API GW compatible\n * API configuration JSON object\n * Parameters:\n *   swaggerApi - JSON object defining API in swagger format\n * Returns:\n *   gwApi      - JSON object defining API in API GW format\n */\nfunction generateGwApiFromSwaggerApi(swaggerApi) {\n  var gwApi = {};\n  gwApi.basePath = swaggerApi.basePath;\n  gwApi.name = swaggerApi.info.title;\n  gwApi.resources = {};\n  for (var path in swaggerApi.paths) {\n  console.log('generateGwApiFromSwaggerApi: processing swaggerApi path: ', path);\n    gwApi.resources[path] = {};\n    var gwpathop = gwApi.resources[path].operations = {};\n    for (var operation in swaggerApi.paths[path]) {\n      console.log('generateGwApiFromSwaggerApi: processing swaggerApi operation: ', operation);\n      console.log('generateGwApiFromSwaggerApi: processing operation backendMethod: ', swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod);\n      var gwop = gwpathop[operation] = {};\n      gwop.backendMethod = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod;\n      gwop.backendUrl = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendUrl;\n      gwop.policies = swaggerApi.paths[path][operation]['x-ibm-op-ext'].policies;\n    }\n  }\n  return gwApi;\n}\n\n/*\n * Create a base swagger API object containing the API basepath, but no endpoints\n * Parameters:\n *   basepath   - Required. API basepath\n *   apiname    - Optional. API friendly name. Defaults to basepath\n * Returns:\n *   swaggerApi - API swagger JSON object\n */\nfunction generateBaseSwaggerApi(basepath, apiname) {\n  var swaggerApi = {\n    'swagger': '2.0',\n    'info': {\n      'title': apiname || basepath,\n      'version': '1.0.0'\n    },\n    'basePath': basepath,\n    'paths': {},\n    'x-ibm-configuration': {\n      'assembly': {\n      },\n      'cors': {\n        'enabled': true\n      }\n    }\n  };\n  return swaggerApi;\n}\n\n/*\n * Take an existing API in JSON swagger format, and update it with a single path/operation.\n * The addition can be an entirely new path or a new operation under an existing path.\n * Parameters:\n *   swaggerApi - API to augment in swagger JSON format.  This will be updated.\n *   endpoint   - JSON object describing new path/operation.  Required fields\n *                {\n *                  gatewayMethod:\n *                  gatewayPath:\n *                  action: {\n *                    authkey:\n *                    backendMethod:\n *                    backendUrl:\n *                    name:\n *                    namespace:\n *                    secureKey\n *                  }\n *                }\n *   responsetype Optional. The web action invocation .extension.  Defaults to json\n * Returns:\n *   swaggerApi - Input JSON object in swagger format containing the union of swaggerApi + new path/operation\n */\nfunction addEndpointToSwaggerApi(swaggerApi, endpoint, responsetype) {\n  var operation = endpoint.gatewayMethod.toLowerCase();\n  var operationId = makeOperationId(operation, endpoint.gatewayPath);\n  responsetype = responsetype || 'json';\n  console.log('addEndpointToSwaggerApi: operationid = '+operationId);\n  try {\n    // If the relative path already exists, append to it; otherwise create it\n    if (!swaggerApi.paths[endpoint.gatewayPath]) {\n      swaggerApi.paths[endpoint.gatewayPath] = {};\n    }\n    swaggerApi.paths[endpoint.gatewayPath][operation] = {\n      'operationId': operationId,\n      'parameters': endpoint.pathParameters,\n      'x-openwhisk': {\n        'url': makeWebActionBackendUrl(endpoint.action, responsetype),\n        'namespace': endpoint.action.namespace,\n        'package': getPackageNameFromFqActionName(endpoint.action.name),\n        'action': getActionNameFromFqActionName(endpoint.action.name),\n      },\n      'responses': {\n        'default': {\n          'description': 'Default response'\n        }\n      }\n    };\n\n    // API GW extensions\n    console.log('addEndpointToSwaggerApi: setting api gw extension values');\n    setActionOperationInvocationDetails(swaggerApi, endpoint, operationId, responsetype);\n  }\n  catch(e) {\n    console.log(\"addEndpointToSwaggerApi: exception \"+e);\n    throw 'API swagger generation error: '+e;\n  }\n\n  return swaggerApi;\n}\n\nfunction setActionOperationInvocationDetails(swagger, endpoint, operationId, responsetype) {\n  var caseArr = _.get(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case') || [];\n  var caseIdx = getCaseOperationIdx(caseArr, operationId);\n  var operations = [operationId];\n  _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].operations', operations);\n  _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[0].invoke.target-url',  makeWebActionBackendUrl(endpoint.action, responsetype, true) );\n  _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[0].invoke.verb', 'keep');\n  if (endpoint.action.secureKey) {\n    _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[1].set-variable.actions[0].set', 'message.headers.X-Require-Whisk-Auth' );\n    _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[1].set-variable.actions[0].value', endpoint.action.secureKey );\n  }\n}\n\n// Return the numeric index into case[] into which the associated operation will be configured\n// If the array is empty, the returned index is 0\n// If the operation exists, the existing index will be returned\n// Otherwise the index will be the last existing index + 1\nfunction getCaseOperationIdx(caseArr, operationId) {\n  var i;\n  for (i=0; i<caseArr.length; i++) {\n    if (caseArr[i].operations[0] == operationId) {\n      console.log('getCaseOperationIdx: found existing operation for '+operationId+' at case index '+i);\n      break;\n    }\n  }\n  return i;\n}\n\n// Create the external URL used to invoke a web-action.  Examples:\n// - https://localhost/api/v1/web/whisk.system/default/echo-web.json\n// - https://localhost/api/v1/web/whisk.system/mypkg/echo-web.json\n// NOTE: Use \"default\" as the package name when a package is not explicitly defined.\n// Parameters\n//   endpointAction       - fully qualified action name (i.e. /ns/pkg/action or /ns/action)\n//   endpointResponseType - determines the action invocation extension without the '.' (i.e. http, json, etc)\n//   parameters           - the parameters defined in the path, if any.\n// Returns:\n//   string               - web-action URL\nfunction makeWebActionBackendUrl(endpointAction, endpointResponseType, isTargetUrl = false) {\n  protocol = getProtocolFromActionUrl(endpointAction.backendUrl);\n  host = getHostFromActionUrl(endpointAction.backendUrl);\n  ns = endpointAction.namespace;\n  pkg = getPackageNameFromFqActionName(endpointAction.name) || 'default';\n  name = getActionNameFromFqActionName(endpointAction.name);\n  reqPath = isTargetUrl && endpointResponseType === 'http' ? \"$(request.path)\" : \"\";\n  return protocol + '://' + host + '/api/v1/web/' + ns + '/' + pkg + '/' + name + '.' + endpointResponseType + reqPath;\n}\n\n/*\n * Update an existing Swagger API document by removing the specified relpath/operation section.\n *   swaggerApi - API from which to remove the specified endpoint.  This object will be updated.\n *   endpoint   - JSON object describing new path/operation.  Required fields\n *                {\n *                  gatewayPath:    Optional.  The relative path.  If not provided, the original swaggerApi is returned\n *                  gatewayMethod:  Optional.  The operation under gatewayPath.  If not provided, the entire gatewayPath is deleted.\n *                                             If updated gatewayPath has no more operations, then the entire gatewayPath is deleted.\n *                }\n * @returns Updated JSON swagger API\n */\nfunction removeEndpointFromSwaggerApi(swaggerApi, endpoint) {\n  var relpath = endpoint.gatewayPath;\n  var operation = endpoint.gatewayMethod ? endpoint.gatewayMethod.toLowerCase() : endpoint.gatewayMethod;\n  console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' operation '+operation);\n  if (!relpath) {\n      console.log('removeEndpointFromSwaggerApi: No relpath specified; nothing to remove');\n      return 'No path provided; nothing to remove';\n  }\n\n  // If an operation is not specified, delete the entire relpath\n  if (!operation) {\n      console.log('removeEndpointFromSwaggerApi: No operation; removing entire relpath '+relpath);\n      if (swaggerApi.paths[relpath]) {\n          for (var op in swaggerApi.paths[relpath]) {\n            deleteActionOperationInvocationDetails(swaggerApi, makeOperationId(op, relpath));\n          }\n          delete swaggerApi.paths[relpath];\n      } else {\n          console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' does not exist in the API');\n          return 'path \\''+relpath+'\\' does not exist in the API';\n      }\n  } else { // relpath and operation are specified, just delete the specific operation\n      if (swaggerApi.paths[relpath] && swaggerApi.paths[relpath][operation]) {\n          delete swaggerApi.paths[relpath][operation];\n          if (Object.keys(swaggerApi.paths[relpath]).length === 0) {\n            console.log('removeEndpointFromSwaggerApi: after deleting operation '+operation+', relpath '+relpath+' has no more operations; so deleting entire relpath '+relpath);\n            delete swaggerApi.paths[relpath];\n          }\n          deleteActionOperationInvocationDetails(swaggerApi, makeOperationId(operation, relpath));\n      } else {\n          console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' with operation '+operation+' does not exist in the API');\n          return 'path \\''+relpath+'\\' with operation \\''+operation+'\\' does not exist in the API';\n      }\n  }\n\n  return swaggerApi;\n}\n\nfunction deleteActionOperationInvocationDetails(swagger, operationId) {\n  console.log('deleteActionOperationInvocationDetails: deleting case entry for ' + operationId);\n  var caseArr = _.get(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case') || [];\n  if (caseArr.length > 0) {\n    var caseIdx = getCaseOperationIdx(caseArr, operationId);\n    _.pullAt(caseArr, caseIdx);\n    _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case', caseArr);\n  } else {\n    console.log('deleteActionOperationInvocationDetails: empty case[] array; case operation '+operationId+' does not exist');\n  }\n}\n\nfunction confidentialPrint(str) {\n    var printStr;\n    if (str) {\n        printStr = 'XXXXXXXXXX';\n    }\n    return printStr;\n}\n\n/* Create the CLI response payload from an array of GW API objects\n * Parameters:\n *  gwApis    - Array of JSON GW API objects\n * Returns:\n *  respApis  - A new array of JSON CLI API objects\n */\nfunction generateCliResponse(gwApis) {\n  var respApis = [];\n  try {\n    for (var i=0; i<gwApis.length; i++) {\n      respApis.push(generateCliApiFromGwApi(gwApis[i]));\n    }\n  } catch(e) {\n    console.error('generateCliResponse: exception caught: '+e);\n    throw 'API format transformation error: '+e;\n  }\n  return respApis;\n}\n\n/* Use the specified GW API object to create an API JSON object in for format the CLI expects.\n * Parameters:\n *  gwApi      - JSON GW API object\n * Returns:\n *  cliApi     - JSON CLI API object\n */\nfunction generateCliApiFromGwApi(gwApi) {\n  console.log('generateCliApiFromGwApi: ' + JSON.stringify(gwApi, \" \", 2));\n  var cliApi = {};\n  cliApi.id = 'Not Used';\n  cliApi.key = 'Not Used';\n  cliApi.value = {};\n  cliApi.value.namespace = 'Not Used';\n  cliApi.value.gwApiActivated = true;\n  cliApi.value.tenantId = 'Not Used';\n  cliApi.value.gwApiUrl = gwApi.managed_url;\n  cliApi.value.apidoc = gwApi.open_api_doc;\n  return cliApi;\n}\n\n/*\n * Parses the openwhisk action URL and returns the various components\n * Parameters\n *  url    - in format PROTOCOL://HOST/api/v1/web/NAMESPACE/PACKAGE/ACTION.http\n * Returns\n *  result - an array of strings.\n *           result[0] : Entire URL\n *           result[1] : protocol (i.e. https)\n *           result[2] : host (i.e. myco.com, 1.2.3.4, myco.com/mywhisk)\n *           result[3] : namespace\n *           result[4] : package name\n *           result[5] : action name\n *           result[6] : action response type (i.e http, json, text, html, or svg)\n */\nfunction parseActionUrl(actionUrl) {\n  console.log('parseActionUrl: parsing action url: '+actionUrl);\n  var actionUrlPattern = /(\\w+):\\/\\/([:\\/\\w.\\-]+)\\/api\\/v\\d\\/web\\/([@\\w .\\-]+)\\/([@\\w .\\-]+)\\/([@\\w .\\-\\/]+)\\.(\\w+)/;\n  try {\n    return actionUrl.match(actionUrlPattern);\n  } catch(e) {\n    console.error('parseActionUrl: exception: '+e);\n    throw 'parseActionUrl: exception: '+e;\n  }\n}\n\n/*\n * https://172.17.0.1/api/v1/web/NAMESPACE/PACKAGE/ACTION.json\n * would return ACTION\n */\nfunction getActionNameFromActionUrl(actionUrl) {\n  return parseActionUrl(actionUrl)[5];\n}\n\n/*\n * https://172.17.0.1/api/v1/web/NAMESPACE/PACKAGE/ACTION.json\n * would return NAMESPACE\n */\nfunction getPackageNameFromActionUrl(actionUrl) {\n  return parseActionUrl(actionUrl)[4];\n}\n\n/*\n * https://172.17.0.1/api/v1/web/NAMESPACE/PACKAGE/ACTION.json\n * would return NAMESPACE\n */\nfunction getActionNamespaceFromActionUrl(actionUrl) {\n  return parseActionUrl(actionUrl)[3];\n}\n\n/*\n * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction\n * would return 172.17.0.1\n * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/mypkg/getaction\n * would return my-host.mycompany.com\n */\nfunction getHostFromActionUrl(actionUrl) {\n  return parseActionUrl(actionUrl)[2];\n}\n\n/*\n * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction\n * would return https\n */\nfunction getProtocolFromActionUrl(actionUrl) {\n  return parseActionUrl(actionUrl)[1];\n}\n\n/*\n * Parses an openwhisk action name into its various components\n * Parameters\n *  fqname - in one of the following formats:\n *           (1)   /[namespace]/[package]/[action]\n *           (2)   [package]/[action]\n *           (3)   [action]\n * Returns\n *  result - an array of strings; depending on input\n *           Input (1):\n *             result[0] : fqname (i.e. /ns/pkg/action)\n *             result[1] : namespace\n *             result[2] : package\n *             result[3] : action name\n *           Input (2):\n *             result[0] : fqname (i.e.  pkg/action)\n *             result[1] : package\n *             result[2] : action name\n *             result[3] : ''\n *           Input (3):\n *             result[0] : fqname   (i.e. action)\n *             result[1] : action name\n *             result[2] : ''\n *             result[3] : ''\n */\nfunction parseActionName(fqname) {\n  console.log('parseActionName: parsing action: '+fqname);\n  var actionNamePattern = /[\\/]?([@ .\\-\\w]*)[\\/]?([@ .\\-\\w]*)[\\/]?([@ .\\-\\w]*)/;\n  try {\n    return fqname.match(actionNamePattern);\n  } catch(e) {\n    console.error('parseActionName: exception: '+e);\n    throw 'parseActionName: exception: '+e;\n  }\n}\n\nfunction getNamespaceFromFqActionName(fqAction) {\n  var ns = '';\n  var parsedAction = parseActionName(fqAction);\n  if (parsedAction[3].length > 0) {\n    ns = parsedAction[1];\n  }\n  return ns;\n}\n\nfunction getPackageNameFromFqActionName(fqAction) {\n  var pkg = '';\n  var parsedAction = parseActionName(fqAction);\n  if (parsedAction[3].length > 0) {\n    pkg = parsedAction[2];\n  } else if (parsedAction[2].length > 0) {\n    pkg = parsedAction[1];\n  }\n  return pkg;\n}\n\nfunction getActionNameFromFqActionName(fqAction) {\n  var action = '';\n  var parsedAction = parseActionName(fqAction);\n  if (parsedAction[3].length > 0) {\n    action = parsedAction[3];\n  } else if (parsedAction[2].length > 0) {\n    action = parsedAction[2];\n  } else {\n    action = parsedAction[1];\n  }\n  return action;\n}\n\n/*\n * Replace the namespace values that are used in the apidoc with the\n * specified namespace\n */\nfunction updateNamespace(apidoc, namespace) {\n  if (apidoc && namespace) {\n    if (apidoc.action) {\n      // The action namespace does not have to match the CLI user's namespace\n      // If it is different, leave it alone; otherwise use the replacement namespace\n      // And only replace when the namespace is the default '_' which needs replacement\n      if (apidoc.action.namespace === '_') {\n        apidoc.action.namespace = namespace;\n        apidoc.action.backendUrl = replaceNamespaceInUrl(apidoc.action.backendUrl, namespace);      }\n    }\n    apidoc.namespace = namespace;\n  }\n}\n\n/*\n * Take an OpenWhisk URL (i.e. action invocation URL) and replace the namespace\n * path parameter value with the provided namespace value\n */\nfunction replaceNamespaceInUrl(url, namespace) {\n  var namespacesPattern = /\\/api\\/v1\\/web\\/([\\w@.-]+)\\//;\n  console.log('replaceNamespaceInUrl: namspace='+namespace+' url before - '+url);\n  matchResult = url.match(namespacesPattern);\n  if (matchResult !== null) {\n    console.log('replaceNamespaceInUrl: replacing namespace \\''+matchResult[1]+'\\' with \\''+namespace+'\\'');\n    url = url.replace(namespacesPattern, '/api/v1/web/'+namespace+'/');\n  }\n  console.log('replaceNamespaceInUrl: url after - '+url);\n  return url;\n}\n\n/*\n * Take an error string and create a response object suitable for inclusion in\n * a Promise.reject() call.\n *\n * The response object can take two formats. If the api management action was\n * invoked as a web-action (i.e. via https://OW-HOST/api/v1/web/NS/PKG/ACTION.http),\n * then the response is an error object that mimics a non-webaction openwhisk\n * action's application error response - like so:\n *     {\n *        statusCode: 502,    <- signifies an application error\n *        headers: {'Content-Type': 'application/json'},\n *        body: JSON object or JSON string\n *     }\n * Otherwise, the action was invoked as a regular OpenWhisk action\n * (i.e. https://OW-HOST/api/v1/namespaces/NS/actions/ACTION) and the\n * error response is just a string.  OpenWhisk backend logic will ultimately\n * convert this string into the above error object format.\n *\n * Parameters\n *  err             - Error string\n *  isWebAction     - Boolean. True -> generate a web-action response\n *                             False -> Generate an action response\n */\nfunction makeErrorResponseObject(err, isWebAction) {\n  console.log('makeErrorResponseObject: isWebAction: '+isWebAction);\n  if (!isWebAction) {\n    console.log('makeErrorResponseObject: not called as a web action');\n    return err;\n  }\n\n  var bodystr = err;\n  if (typeof err === 'string') {\n    bodystr = {\n      \"error\": JSON.parse(makeJsonString(err)),  // Make sure err is plain old string to avoid duplicate JSON escaping\n    };\n  }\n  return {\n    statusCode: 502,\n    headers: { 'Content-Type': 'application/json' },\n    body: bodystr\n  };\n}\n\n/*\n * Take an response string and create a response object suitable for inclusion in\n * a Promise.resolve() call.\n *\n * The response object can take two formats. If the api management action was\n * invoked as a web-action (i.e. via https://OW-HOST/api/v1/web/NS/PKG/ACTION.http),\n * then the response is an object that mimics a non-webaction openwhisk\n * action's application successful response - like so:\n *     {\n *        statusCode: 200,    <- signifies a successful action\n *        headers: {'Content-Type': 'application/json'},\n *        body: JSON object or JSON string\n *     }\n * Otherwise, the action was invoked as a regular OpenWhisk action\n * (i.e. https://OW-HOST/api/v1/namespaces/NS/actions/ACTION) and the\n * response is just a string.  OpenWhisk backend logic will ultimately\n * convert this string into the above object format.\n *\n * Parameters\n *  err             - Error string\n *  isWebAction     - Boolean. True -> generate a web-action response\n *                             False -> generate an action response\n */\nfunction makeResponseObject(resp, isWebAction) {\n  console.log('makeResponseObject: isWebAction: '+isWebAction);\n  if (!isWebAction) {\n    console.log('makeResponseObject: not called as a web action');\n    return resp;\n  }\n\n  var bodystr = resp;\n  if (typeof resp === 'string') {\n    bodystr = JSON.parse(makeJsonString(resp));\n  }\n  retobj = {\n    statusCode: 200,\n    headers: { 'Content-Type': 'application/json' },\n    body: bodystr\n  };\n  return retobj;\n}\n\n/*\n * Take an object and serialize it into a JSON string.\n *\n * Special consideration is give to strings that are already JSON formatted since\n * serializing these strings can result in redundant escaping.\n *\n * If the value is simply not JSON compliant, a JSON error string is returned.\n */\nfunction makeJsonString(x) {\n  // If the value is not already a string, rely on JSON.stringify to convert it correctly\n  if (x instanceof Error) {\n    //Print the whole error here as we are only returning the error message and nothing else.\n    console.error(x);\n    return JSON.stringify(x.message);\n  } else if (typeof x != 'string') {\n    try {\n      return JSON.stringify(x);\n    } catch (e) {\n      console.error('makeJsonString: value cannot be JSON serialized: '+e);\n      return e;\n    }\n  } else {\n    // It's a string. If it's already a JSON formatted string, leave it alone\n    // Otherwise, convert it into a JSON formatted string\n    try {\n      var temp = JSON.parse(x);\n      return x;\n    } catch (e) {\n      // The string is not a JSON string, so convert it to a JSON string.\n      console.log('makeJsonString: String is not JSON, so need to convert it: '+e);\n      return JSON.stringify(x);\n    }\n  }\n  return 'Unexpected JSON parsing failure';\n}\n\n/*\n * Generate and return a swagger OperationId value\n *\n * Parameters\n *   operation  - String. HTTP method (i.e. get, post, etc)\n *   repath     - String. Swagger path value. The path relative to the base path\n */\nfunction makeOperationId(operation, relpath) {\n   // Concatenate operation + relpath, stripping '/' and camelCasing after each '/' delimiter\n   // relpath special character handling in each path segment:\n   //   . ~ ! $ & ' ( ) * + , ; = : @ are removed and the following characters in the same path segment are camel cased\n   //   - _  are retained and the following characters in the same path segment are lower cased\n  return operation.toLowerCase() +\n         relpath.replace(/[^0-9a-z_-]/gi, ' ').replace(/\\w\\S*/g, function(word) {return makeCamelCase(word);}).replace(/\\s/g, '');\n}\n\nfunction makeCamelCase(str) {\n  return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();\n}\n\nfunction setSubUserAgent(subAgent) {\n  if (subAgent && subAgent.length > 0) {\n    UserAgent = UserAgent + \" \" + subAgent;\n  }\n}\n\nmodule.exports.getApis = getApis;\nmodule.exports.addApiToGateway = addApiToGateway;\nmodule.exports.deleteApiFromGateway = deleteApiFromGateway;\nmodule.exports.generateBaseSwaggerApi = generateBaseSwaggerApi;\nmodule.exports.generateGwApiFromSwaggerApi = generateGwApiFromSwaggerApi;\nmodule.exports.transformApis = transformApis;\nmodule.exports.generateSwaggerApiFromGwApi = generateSwaggerApiFromGwApi;\nmodule.exports.addEndpointToSwaggerApi = addEndpointToSwaggerApi;\nmodule.exports.removeEndpointFromSwaggerApi = removeEndpointFromSwaggerApi;\nmodule.exports.confidentialPrint = confidentialPrint;\nmodule.exports.generateCliResponse = generateCliResponse;\nmodule.exports.generateCliApiFromGwApi = generateCliApiFromGwApi;\nmodule.exports.updateNamespace = updateNamespace;\nmodule.exports.makeErrorResponseObject = makeErrorResponseObject;\nmodule.exports.makeResponseObject = makeResponseObject;\nmodule.exports.makeJsonString = makeJsonString;\nmodule.exports.setSubUserAgent = setSubUserAgent;\nmodule.exports.validateFinalSwagger = validateFinalSwagger;\n"
  },
  {
    "path": "core/routemgmt/common/utils.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Route management action common utilities\n */\nvar request = require('request');\nvar utils2 = require('./apigw-utils.js');\n\n/**\n * Register a tenant with the API GW.\n * A new tenant is created for each unique namespace/tenantinstance.  If the\n * tenant already exists, the tenant is left as-is\n * Parameters:\n *  gwInfo         - Required. API GW connection information (gwUrl, gwAuth)\n *  namespace      - Required. Namespace of tenant\n *  tenantInstance - Optional. Tenanant instance used to create >1 tenant per namespace\n *                   Defaults to 'openwhisk'\n * Returns:\n *  tenant object  - JSON object representing the tenant in the following format:\n *                   { id: GUID, namespace: NAMESPACE, instance: 'openwhisk' }\n */\nfunction createTenant(gwInfo, namespace, tenantInstance) {\n  var instance = tenantInstance || 'openwhisk';  // Default to a fixed instance so all openwhisk tenants have a common instance\n  var options = {\n    followAllRedirects: true,\n    url: gwInfo.gwUrl+'/tenants',\n    headers: {\n      'Accept': 'application/json'\n    },\n    json: {                     // Auto set header: 'Content-Type': 'application/json'\n      instance: instance,\n      namespace: namespace\n    }\n  };\n  if (gwInfo.gwAuth) {\n    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;\n  }\n  console.log('addTenantToGateway: request: '+JSON.stringify(options));\n\n  return new Promise(function(resolve, reject) {\n    request.put(options, function(error, response, body) {\n      var statusCode = response ? response.statusCode : undefined;\n      console.log('addTenantToGateway: response status: '+ statusCode);\n      if (error) console.error('Warning: addTenantToGateway request failed: '+utils2.makeJsonString(error));\n      if (body) console.log('addTenantToGateway: response body: '+utils2.makeJsonString(body));\n\n      if (error) {\n        console.error('addTenantToGateway: Unable to configure a tenant on the API Gateway');\n        reject('Unable to configure the API Gateway: '+utils2.makeJsonString(error));\n      } else if (statusCode != 200) {\n        if (body) {\n          var errMsg = JSON.stringify(body);\n          if (body.error && body.error.message) errMsg = body.error.message;\n          reject('API Gateway failure (status code '+statusCode+'): '+ errMsg);\n        } else {\n          reject('Unable to configure the API Gateway: Response failure code: '+statusCode);\n        }\n\n      } else {\n        if (body && body.id) {  // body has format like:  { id: GUID, namespace: NAMESPACE, instance: 'openwhisk' }\n          console.log('addTenantToGateway: got a single tenant response');\n          resolve(body);\n        } else {\n          console.error('addTenantToGateway: failure: No tenant guid provided');\n          reject('Unable to configure the API Gateway: Invalid response from API Gateway');\n        }\n      }\n    });\n  });\n}\n\n/*\n * Return an array of tenants\n */\nfunction getTenants(gwInfo, ns, tenantInstance) {\n  var qsNsOnly = { 'filter[where][namespace]' : ns };\n  var qsNsAndInstance = { 'filter[where][namespace]' : ns,\n                          'filter[where][instance]'  : tenantInstance };\n  var qs = qsNsOnly;\n  if (tenantInstance) qs = qsNsAndInstance;\n  var options = {\n    followAllRedirects: true,\n    url: gwInfo.gwUrl+'/tenants',\n    qs: qs,\n    headers: {\n      'Accept': 'application/json'\n    },\n  };\n  if (gwInfo.gwAuth) {\n    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;\n  }\n  console.log('getTenants: request: '+JSON.stringify(options));\n\n  return new Promise(function(resolve, reject) {\n    request.get(options, function(error, response, body) {\n      var statusCode = response ? response.statusCode : undefined;\n      console.log('getTenants: response status: '+ statusCode);\n      if (error) console.error('Warning: getTenant request failed: '+utils2.makeJsonString(error));\n      if (body) console.log('getTenants: response body: '+utils2.makeJsonString(body));\n      if (error) {\n        console.error('getTenants: Unable to obtain tenant from the API Gateway');\n        reject('Unable to obtain Tenant from the API Gateway: '+utils2.makeJsonString(error));\n      } else if (statusCode != 200) {\n        if (body) {\n          var errMsg = JSON.stringify(body);\n          if (body.error && body.error.message) errMsg = body.error.message;\n          reject('API Gateway failure (status code '+statusCode+'): '+ errMsg);\n        } else {\n          reject('Unable to configure the API Gateway: Response failure code: '+statusCode);\n        }\n      } else {\n        if (body) {\n          try {\n            var bodyJson = JSON.parse(body);\n            if (Array.isArray(bodyJson)) {\n              resolve(bodyJson);\n            } else {\n              console.error('getTenants: Invalid API GW response body; a JSON array was not returned');\n              resolve( [] );\n            }\n          } catch(e) {\n            console.error('getTenants: Invalid API GW response body; JSON.parse() failure: '+e);\n            reject('Internal error. Invalid API Gateway response: '+e);\n          }\n        } else {\n          console.log('getTenants: No tenants found');\n          resolve( [] );\n        }\n      }\n    });\n  });\n}\n\n/**\n * Configures an API route on the API Gateway.  This API will map to an OpenWhisk action that\n * will be invoked by the API Gateway when the API route is accessed.\n *\n * @param gwInfo Required.\n * @param    gwUrl   Required.  The base URL gateway path (i.e.  'PROTOCOL://gw.host.domain:PORT/CONTEXT')\n * @param    gwAuth  Required.  The credentials used to access the API Gateway REST endpoints\n * @param tenantId   Required.\n * @param swaggerApi   Required. The gateway API object to send to the API gateway\n * @param   payload.namespace  Required. The OpenWhisk namespace of the user defining this API route\n * @param   payload.gatewayPath  Required.  The relative path for this route\n * @param   payload.gatewayMethod  Required.  The gateway route REST verb\n * @param   payload.backendUrl  Required.  The full REST URL used to invoke the associated action\n * @param   payload.backendMethod  Required.  The REST verb used to invoke the associated action\n * @return A promise for an object describing the result with fields error and response\n */\nfunction addApiToGateway(gwInfo, tenantId, swaggerApi, gwApiId) {\n  var requestFcn = request.post;\n\n  // Init the GW API configuration object; base it off the swagger API\n  var gwApi;\n  try {\n    gwApi = generateGwApiFromSwaggerApi(swaggerApi);\n  } catch(e) {\n    console.error('generateGwApiFromSwaggerApi exception: '+e);\n    return Promise.reject('Invalid API configuration: '+e);\n  }\n  gwApi.tenantId = tenantId;\n\n  var options = {\n    followAllRedirects: true,\n    url: gwInfo.gwUrl+'/apis',\n    headers: {\n      'Accept': 'application/json'\n    },\n    json: gwApi,  // Use of json automaticatlly sets header: 'Content-Type': 'application/json'\n  };\n  if (gwInfo.gwAuth) {\n    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;\n  }\n\n  if (gwApiId) {\n    console.log(\"addApiToGateway: Updating existing API\");\n    gwApi.id = gwApiId;\n    options.url = gwInfo.gwUrl+'/apis/'+gwApiId;\n    requestFcn = request.put;\n  }\n\n  console.log('addApiToGateway: request: '+JSON.stringify(options, \" \", 2));\n\n  return new Promise(function(resolve, reject) {\n    requestFcn(options, function(error, response, body) {\n      var statusCode = response ? response.statusCode : undefined;\n      console.log('addApiToGateway: response status:'+ statusCode);\n      if (error) console.error('Warning: addRouteToGateway request failed: '+ utils2.makeJsonString(error));\n      if (body) console.log('addApiToGateway: response body: '+utils2.makeJsonString(body));\n\n      if (error) {\n        console.error('addApiToGateway: Unable to configure the API Gateway');\n        reject('Unable to configure the API Gateway: '+utils2.makeJsonString(error));\n      } else if (statusCode != 200) {\n        if (body) {\n          var errMsg = JSON.stringify(body);\n          if (body.error && body.error.message) errMsg = body.error.message;\n          reject('Unable to configure the API Gateway (status code '+statusCode+'): '+ errMsg);\n        } else {\n          reject('Unable to configure the API Gateway: Response failure code: '+statusCode);\n        }\n      } else if (!body) {\n        console.error('addApiToGateway: Unable to configure the API Gateway: No response body');\n        reject('Unable to configure the API Gateway: No response received from the API Gateway');\n      } else {\n        resolve(body);\n      }\n    });\n  });\n}\n\n/**\n * Removes an API route from the API Gateway.\n *\n * @param gwInfo Required.\n * @param    gwUrl   Required. The base URL gateway path (i.e.  'PROTOCOL://gw.host.domain:PORT/CONTEXT')\n * @param    gwAuth  Optional. The credentials used to access the API Gateway REST endpoints\n * @param apiId  Required.  Unique Gateway API Id\n * @return A promise for an object describing the result with fields error and response\n */\nfunction deleteApiFromGateway(gwInfo, gwApiId) {\n  var options = {\n    followAllRedirects: true,\n    url: gwInfo.gwUrl+'/apis/'+gwApiId,\n    agentOptions: {rejectUnauthorized: false},\n    headers: {\n      'Accept': 'application/json'\n    }\n  };\n  if (gwInfo.gwAuth) {\n    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;\n  }\n  console.log('deleteApiFromGateway: request: '+JSON.stringify(options, \" \", 2));\n\n  return new Promise(function(resolve, reject) {\n    request.delete(options, function(error, response, body) {\n      var statusCode = response ? response.statusCode : undefined;\n      console.log('deleteApiFromGateway: response status:'+ statusCode);\n      if (error) console.error('Warning: deleteGatewayApi request failed: '+ utils2.makeJsonString(error));\n      if (body) console.log('deleteApiFromGateway: response body: '+utils2.makeJsonString(body));\n\n      if (error) {\n        console.error('deleteApiFromGateway: Unable to delete the API Gateway');\n        reject('Unable to delete the API Gateway: '+utils2.makeJsonString(error));\n      } else if (statusCode != 200) {\n        if (body) {\n          var errMsg = JSON.stringify(body);\n          if (body.error && body.error.message) errMsg = body.error.message;\n          reject('Unable to delete the API Gateway (status code '+statusCode+'): '+ errMsg);\n        } else {\n          reject('Unable to delete the API Gateway: Response failure code: '+statusCode);\n        }\n      } else {\n        resolve();\n      }\n    });\n  });\n}\n\n/**\n * Return an array of APIs\n */\nfunction getApis(gwInfo, tenantId, bpOrApiName) {\n  var qsBasepath = { 'filter[where][basePath]' : bpOrApiName };\n  var qsApiName = { 'filter[where][name]' : bpOrApiName };\n  var qs;\n  if (bpOrApiName) {\n    if (bpOrApiName.indexOf('/') !== 0) {\n      console.log('getApis: querying APIs based on api name');\n      qs = qsApiName;\n    } else {\n      console.log('getApis: querying APIs based on basepath');\n      qs = qsBasepath;\n    }\n  }\n  var options = {\n    followAllRedirects: true,\n    url: gwInfo.gwUrl+'/tenants/'+tenantId+'/apis',\n    headers: {\n      'Accept': 'application/json'\n    },\n  };\n  if (qs) {\n    options.qs = qs;\n  }\n  if (gwInfo.gwAuth) {\n    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;\n  }\n  console.log('getApis: request: '+JSON.stringify(options));\n\n  return new Promise(function(resolve, reject) {\n    request.get(options, function(error, response, body) {\n      var statusCode = response ? response.statusCode : undefined;\n      console.log('getApis: response status: '+ statusCode);\n      if (error) console.error('Warning: getApis request failed: '+utils2.makeJsonString(error));\n      if (body) console.log('getApis: response body: '+utils2.makeJsonString(body));\n      if (error) {\n        console.error('getApis: Unable to obtain API(s) from the API Gateway');\n        reject('Unable to obtain API(s) from the API Gateway: '+utils2.makeJsonString(error));\n      } else if (statusCode != 200) {\n        if (body) {\n          var errMsg = JSON.stringify(body);\n          if (body.error && body.error.message) errMsg = body.error.message;\n          reject('Unable to obtain API(s) from the API Gateway (status code '+statusCode+'): '+ errMsg);\n        } else {\n          reject('Unable to obtain API(s) from the API Gateway: Response failure code: '+statusCode);\n        }\n      } else {\n        if (body) {\n          try {\n            var bodyJson = JSON.parse(body);\n            if (Array.isArray(bodyJson)) {\n              resolve(bodyJson);\n            } else {\n              console.error('getApis: Invalid API GW response body; a JSON array was not returned');\n              resolve( [] );\n            }\n          } catch(e) {\n            console.error('getApis: Invalid API GW response body; JSON.parse() failure: '+e);\n            reject('Invalid API Gateway response: '+e);\n          }\n        } else {\n          console.log('getApis: No APIs found');\n          resolve( [] );\n        }\n      }\n    });\n  });\n}\n\n/**\n * Convert API object array into specified format\n * Parameters:\n *  apis    : array of 0 or more APIs\n *  format  : 'apigw' or 'swagger'\n * Returns:\n *  array   : New array of API object - each in the specified format\n */\nfunction transformApis(apis, format) {\n  var apisOutput;\n  try {\n    if (format.toLowerCase() === 'apigw') {\n      apisOutput = apis;\n    } else if (format.toLowerCase() === 'swagger') {\n      apisOutput = JSON.parse(JSON.stringify(apis));\n      for (var i = 0; i < apisOutput.length; i++) {\n        apisOutput[i] = generateSwaggerApiFromGwApi(apisOutput[i]);\n      }\n    } else {\n      console.error('transformApis: Invalid format specification: '+format);\n      throw 'Internal error. Invalid format specification: '+format;\n    }\n  } catch(e) {\n    console.error('transformApis: exception caught: '+e);\n    throw 'API format transformation error: '+e;\n  }\n\n  return apisOutput;\n}\n\n/**\n * Convert API object into swagger JSON format\n * Parameters:\n *  gwApi  : API object as returned from the API Gateway\n * Returns:\n *  object : New API object in swagger JSON format\n */\nfunction generateSwaggerApiFromGwApi(gwApi) {\n  // Start with a copy of the gwApi object.  It's close to the desired swagger format\n  var swaggerApi = JSON.parse(JSON.stringify(gwApi));\n  swaggerApi.swagger = '2.0';\n  swaggerApi.info = {\n    title: gwApi.name,\n    version: '1.0.0'\n  };\n\n  // Copy the gwAPI's 'resources' object as the starting point for the swagger 'paths' object\n  swaggerApi.paths = JSON.parse(JSON.stringify(gwApi.resources));\n  for (var path in swaggerApi.paths) {\n    if (!swaggerApi.paths[path]) {\n      console.error('generateSwaggerApiFromGwApi: no operations defined for ignored relpath \\''+path+'\\'');\n      delete swaggerApi.paths[path];\n      continue;\n    }\n    for (var op in swaggerApi.paths[path].operations) {\n      console.log('generateSwaggerApiFromGwApi: processing path '+path+'; operation '+op);\n      if (!op) {\n        console.error('generateSwaggerApiFromGwApi: path \\''+path+'\\' has no operations!');\n        continue;\n      }\n      // swagger wants lower case operations\n      var oplower = op.toLowerCase();\n\n      // Valid swagger requires a 'responses' object for each operation\n      swaggerApi.paths[path][oplower] = {\n        responses: {\n          default: {\n            description: 'Default response'\n          }\n        }\n      };\n      // Custom swagger extension to hold the action mapping configuration\n      swaggerApi.paths[path][oplower]['x-ibm-op-ext'] = {\n        backendMethod : swaggerApi.paths[path].operations[op].backendMethod,\n        backendUrl : swaggerApi.paths[path].operations[op].backendUrl,\n        policies : JSON.parse(JSON.stringify(swaggerApi.paths[path].operations[op].policies)),\n        actionName: getActionNameFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl),\n        actionNamespace: getActionNamespaceFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl)\n      };\n    }\n    delete swaggerApi.paths[path].operations;\n  }\n  delete swaggerApi.resources;\n  delete swaggerApi.name;\n  delete swaggerApi.id;\n  delete swaggerApi.managedUrl;\n  delete swaggerApi.tenantId;\n  return swaggerApi;\n}\n\n/**\n * Create a base swagger API object containing the API basepath, but no endpoints\n * Parameters:\n *   basepath   - Required. API basepath\n *   apiname    - Optional. API friendly name. Defaults to basepath\n * Returns:\n *   swaggerApi - API swagger JSON object\n */\nfunction generateBaseSwaggerApi(basepath, apiname) {\n  var swaggerApi = {\n    swagger: \"2.0\",\n    info: {\n      title: apiname || basepath,\n      version: \"1.0.0\"\n    },\n    basePath: basepath,\n    paths: {}\n  };\n  return swaggerApi;\n}\n\n/**\n * Take an API in JSON swagger format and create an API GW compatible\n * API configuration JSON object\n * Parameters:\n *   swaggerApi - JSON object defining API in swagger format\n * Returns:\n *   gwApi      - JSON object defining API in API GW format\n */\nfunction generateGwApiFromSwaggerApi(swaggerApi) {\n  var gwApi = {};\n  gwApi.basePath = swaggerApi.basePath;\n  gwApi.name = swaggerApi.info.title;\n  gwApi.resources = {};\n  for (var path in swaggerApi.paths) {\n  console.log('generateGwApiFromSwaggerApi: processing swaggerApi path: ', path);\n    gwApi.resources[path] = {};\n    var gwpathop = gwApi.resources[path].operations = {};\n    for (var operation in swaggerApi.paths[path]) {\n      console.log('generateGwApiFromSwaggerApi: processing swaggerApi operation: ', operation);\n      console.log('generateGwApiFromSwaggerApi: processing operation backendMethod: ', swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod);\n      var gwop = gwpathop[operation] = {};\n      gwop.backendMethod = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod;\n      gwop.backendUrl = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendUrl;\n      gwop.policies = swaggerApi.paths[path][operation]['x-ibm-op-ext'].policies;\n    }\n  }\n  return gwApi;\n}\n\n/**\n * Take an existing API in JSON swagger format, and update it with a single path/operation.\n * The addition can be an entirely new path or a new operation under an existing path.\n * Parameters:\n *   swaggerApi - API to augment in swagger JSON format.  This will be updated.\n *   endpoint   - JSON object describing new path/operation.  Required fields\n *                {\n *                  gatewayMethod:\n *                  gatewayPath:\n *                  action: {\n *                    authkey:\n *                    backendMethod:\n *                    backendUrl:\n *                    name:\n *                    namespace:\n *                  }\n *                }\n * Returns:\n *   swaggerApi - Input JSON object in swagger format containing the union of swaggerApi + new path/operation\n */\nfunction addEndpointToSwaggerApi(swaggerApi, endpoint) {\n  var operation = endpoint.gatewayMethod.toLowerCase();\n  var auth_base64 = Buffer.from(endpoint.action.authkey,'ascii').toString('base64');\n\n  // If the relative path already exists, append to it; otherwise create it\n  if (!swaggerApi.paths[endpoint.gatewayPath]) {\n    swaggerApi.paths[endpoint.gatewayPath] = {};\n  }\n  swaggerApi.paths[endpoint.gatewayPath][operation] = {\n    'x-ibm-op-ext': {\n      backendMethod: endpoint.action.backendMethod,\n      backendUrl: endpoint.action.backendUrl,\n      actionName: endpoint.action.name,\n      actionNamespace: endpoint.action.namespace,\n      policies: [\n        {\n          type: 'reqMapping',\n          value: [\n            {\n              action: 'transform',\n              from: {\n                name: '*',\n                location: 'query'\n              },\n              to: {\n                name: '*',\n                location: 'body'\n              }\n            },\n            {\n              action: 'insert',\n              from: {\n                value: 'Basic '+auth_base64\n              },\n              to: {\n                name: 'Authorization',\n                location: 'header'\n              }\n            },\n            {\n              action: 'insert',\n              from: {\n                value: 'application/json'\n              },\n              to: {\n                name: 'Content-Type',\n                location: 'header'\n              }\n            },\n            {\n              action: 'insert',\n              from: {\n                value: 'true'\n              },\n              to: {\n                name: 'blocking',\n                location: 'query'\n              }\n            },\n            {\n              action: 'insert',\n              from: {\n                value: 'true'\n              },\n              to: {\n                name: 'result',\n                location: 'query'\n              }\n            }\n          ]\n        }\n      ]\n    },\n    responses: {\n      default: {\n        description: \"Default response\"\n      }\n    }\n  };\n\n  return swaggerApi;\n}\n\n/**\n * Update an existing DB API document by removing the specified relpath/operation section.\n *   swaggerApi - API from which to remove the specified endpoint.  This object will be updated.\n *   endpoint   - JSON object describing new path/operation.  Required fields\n *                {\n *                  gatewayPath:    Optional.  The relative path.  If not provided, the original swaggerApi is returned\n *                  gatewayMethod:  Optional.  The operation under gatewayPath.  If not provided, the entire gatewayPath is deleted.\n *                                             If updated gatewayPath has no more operations, then the entire gatewayPath is deleted.\n *                }\n * @returns Updated JSON swagger API\n */\nfunction removeEndpointFromSwaggerApi(swaggerApi, endpoint) {\n  var relpath = endpoint.gatewayPath;\n  var operation = endpoint.gatewayMethod ? endpoint.gatewayMethod.toLowerCase() : endpoint.gatewayMethod;\n  console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' operation '+operation);\n  if (!relpath) {\n      console.log('removeEndpointFromSwaggerApi: No relpath specified; nothing to remove');\n      return 'No relpath provided; nothing to remove';\n  }\n\n  // If an operation is not specified, delete the entire relpath\n  if (!operation) {\n      console.log('removeEndpointFromSwaggerApi: No operation; removing entire relpath '+relpath);\n      if (swaggerApi.paths[relpath]) {\n          delete swaggerApi.paths[relpath];\n      } else {\n          console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' does not exist in the API; already deleted');\n          return 'relpath '+relpath+' does not exist in the API';\n      }\n  } else {\n      if (swaggerApi.paths[relpath] && swaggerApi.paths[relpath][operation]) {\n          delete swaggerApi.paths[relpath][operation];\n          if (Object.keys(swaggerApi.paths[relpath]).length === 0) {\n            console.log('removeEndpointFromSwaggerApi: after deleting operation '+operation+', relpath '+relpath+' has no more operations; so deleting entire relpath '+relpath);\n            delete swaggerApi.paths[relpath];\n          }\n      } else {\n          console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' with operation '+operation+' does not exist in the API');\n          return 'relpath '+relpath+' with operation '+operation+' does not exist in the API';\n      }\n  }\n\n  return swaggerApi;\n}\n\nfunction confidentialPrint(str) {\n    var printStr;\n    if (str) {\n        printStr = 'XXXXXXXXXX';\n    }\n    return printStr;\n}\n\n/**\n * Create the CLI response payload from an array of GW API objects\n * Parameters:\n *  gwApis    - Array of JSON GW API objects\n * Returns:\n *  respApis  - A new array of JSON CLI API objects\n */\nfunction generateCliResponse(gwApis) {\n  var respApis = [];\n  try {\n    for (var i=0; i<gwApis.length; i++) {\n      respApis.push(generateCliApiFromGwApi(gwApis[i]));\n    }\n  } catch(e) {\n    console.error('generateCliResponse: exception caught: '+e);\n    throw 'API format transformation error: '+e;\n  }\n  return respApis;\n}\n\n/**\n * Use the specified GW API object to create an API JSON object in for format the CLI expects.\n * Parameters:\n *  gwApi      - JSON GW API object\n * Returns:\n *  cliApi     - JSON CLI API object\n */\nfunction generateCliApiFromGwApi(gwApi) {\n  var cliApi = {};\n  cliApi.id = 'Not Used';\n  cliApi.key = 'Not Used';\n  cliApi.value = {};\n  cliApi.value.namespace = 'Not Used';\n  cliApi.value.gwApiActivated = true;\n  cliApi.value.tenantId = 'Not Used';\n  cliApi.value.gwApiUrl = gwApi.managedUrl;\n  cliApi.value.apidoc = generateSwaggerApiFromGwApi(gwApi);\n  return cliApi;\n}\n\n/*\n * Parses the openwhisk action URL and returns the various components\n * Parameters\n *  url    - in format PROTOCOL://HOST/api/v1/namespaces/NAMESPACE/actions/ACTIONNAME\n * Returns\n *  result - an array of strings.\n *           result[0] : Entire URL\n *           result[1] : protocol (i.e. https)\n *           result[2] : host (i.e. myco.com, 1.2.3.4, myco.com/whisk)\n *           result[3] : namespace\n *           result[4] : action name, including the package if used (i.e. myaction, mypkg/myaction)\n */\nfunction parseActionUrl(actionUrl) {\n  var actionUrlPattern = /(\\w+):\\/\\/([:\\/\\w.\\-]+)\\/api\\/v\\d\\/namespaces\\/([@\\w .\\-]+)\\/actions\\/([@\\w .\\-\\/]+)/;\n  try {\n    return actionUrl.match(actionUrlPattern);\n  } catch(e) {\n    console.error('parseActionUrl: exception: '+e);\n    throw 'parseActionUrl: exception: '+e;\n  }\n}\n\n/*\n * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction\n * would return getaction\n * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/getaction\n * would return getaction\n *\n * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/mypkg/getaction\n * would return mypkg/getaction\n * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/mypkg/getaction\n * would return mypkg/getaction\n */\nfunction getActionNameFromActionUrl(actionUrl) {\n  return parseActionUrl(actionUrl)[4];\n}\n\n/*\n * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction\n * would return whisk.system\n * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/mypkg/getaction\n * would return myid@gmail.com_dev\n */\nfunction getActionNamespaceFromActionUrl(actionUrl) {\n  return parseActionUrl(actionUrl)[3];\n}\n\n/*\n * Replace the namespace values that are used in the apidoc with the\n * specified namespace\n */\nfunction updateNamespace(apidoc, namespace) {\n  if (apidoc && namespace) {\n    if (apidoc.action) {\n      // The action namespace does not have to match the CLI user's namespace\n      // If it is different, leave it alone; otherwise use the replacement namespace\n      if (apidoc.namespace === apidoc.action.namespace) {\n        apidoc.action.namespace = namespace;\n        apidoc.action.backendUrl = replaceNamespaceInUrl(apidoc.action.backendUrl, namespace);      }\n    }\n    apidoc.namespace = namespace;\n  }\n}\n\n/*\n * Take an OpenWhisk URL (i.e. action invocation URL) and replace the namespace\n * path parameter value with the provided namespace value\n */\nfunction replaceNamespaceInUrl(url, namespace) {\n  var namespacesPattern = /\\/namespaces\\/([\\w@.-]+)\\//;\n  console.log('replaceNamespaceInUrl: url before - '+url);\n  matchResult = url.match(namespacesPattern);\n  if (matchResult !== null) {\n    console.log('replaceNamespaceInUrl: replacing namespace \\''+matchResult[1]+'\\' with \\''+namespace+'\\'');\n    url = url.replace(namespacesPattern, '/namespaces/'+namespace+'/');\n  }\n  console.log('replaceNamespaceInUrl: url after - '+url);\n  return url;\n}\n\nmodule.exports.createTenant = createTenant;\nmodule.exports.getTenants = getTenants;\nmodule.exports.getApis = getApis;\nmodule.exports.addApiToGateway = addApiToGateway;\nmodule.exports.deleteApiFromGateway = deleteApiFromGateway;\nmodule.exports.generateBaseSwaggerApi = generateBaseSwaggerApi;\nmodule.exports.generateGwApiFromSwaggerApi = generateGwApiFromSwaggerApi;\nmodule.exports.transformApis = transformApis;\nmodule.exports.generateSwaggerApiFromGwApi = generateSwaggerApiFromGwApi;\nmodule.exports.addEndpointToSwaggerApi = addEndpointToSwaggerApi;\nmodule.exports.removeEndpointFromSwaggerApi = removeEndpointFromSwaggerApi;\nmodule.exports.confidentialPrint = confidentialPrint;\nmodule.exports.generateCliResponse = generateCliResponse;\nmodule.exports.generateCliApiFromGwApi = generateCliApiFromGwApi;\nmodule.exports.updateNamespace = updateNamespace;\n"
  },
  {
    "path": "core/routemgmt/createApi/createApi.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n /*\n  * Add a new or update an existing API configuration in the API Gateway\n  * https://docs.cloudant.com/document.html#documentCreate\n  *\n  * Parameters (all as fields in the message JSON object)\n  *   gwUrlV2              Required when accesstoken is provided. The V2 API Gateway base path (i.e. http://gw.com)\n  *   gwUrl                Required. The API Gateway base path (i.e. http://gw.com)\n  *   gwUser               Optional. The API Gateway authentication\n  *   gwPwd                Optional. The API Gateway authentication\n  *   __ow_user            Required. Namespace of API author.  Set by controller\n  *                          The value overrides namespace values in the apidoc\n  *                          Don't override namespace values in the swagger though\n  *   tenantInstance       Optional. Instance identifier used when creating the specific API GW Tenant\n  *   accesstoken          Optional. Dynamic API GW auth.  Overrides gwUser/gwPwd\n  *   spaceguid            Optional. Namespace unique id.\n  *   responsetype         Optional. web action response .extension to use.  default to json\n  *   apidoc               Required. The API Gateway mapping document\n  *      namespace           Required.  Namespace of user/caller\n  *      apiName             Optional if swagger not specified.  API descriptive name\n  *      gatewayBasePath     Required if swagger not specified.  API base path\n  *      gatewayPath         Required if swagger not specified.  Specific API path (relative to base path)\n  *      gatewayMethod       Required if swagger not specified.  API path operation\n  *      id                  Optional if swagger not specified.  Unique id of API\n  *      action              Required. if swagger not specified\n  *           name             Required.  Action name (includes package)\n  *           namespace        Required.  Action namespace\n  *           backendMethod    Required.  Action invocation REST verb.  \"POST\"\n  *           backendUrl       Required.  Action invocation REST url\n  *           authkey          Required.  Action invocation auth key\n  *           secureKey        Optional.  Action's require-whisk-auth value\n  *      swagger             Required if gatewayBasePath not provided.  API swagger JSON\n  *\n  * NOTE: The package containing this action will be bound to the following values:\n  *         gwUrl, gwAuth\n  *       As such, the caller to this action should normally avoid explicitly setting\n  *       these values\n  */\nvar utils = require('./utils.js');\nvar utils2 = require('./apigw-utils.js');\n\nfunction main(message) {\n  //console.log('message: '+JSON.stringify(message));  // ONLY FOR TEMPORARY/LOCAL DEBUG; DON'T ENABLE PERMANENTLY\n  var badArgMsg = validateArgs(message);\n  if (badArgMsg) {\n    return Promise.reject(utils2.makeErrorResponseObject(badArgMsg, (message.__ow_method !== undefined)));\n  }\n\n  var gwInfo = {\n    gwUrl: message.gwUrl,\n  };\n\n  // Replace the CLI provided namespace values with the controller provided namespace value\n  // If __ow_user is not set, the namespace values are left alone\n  if (message.accesstoken) {\n    utils2.updateNamespace(message.apidoc, message.__ow_user);\n  } else {\n    utils.updateNamespace(message.apidoc, message.__ow_user);\n  }\n\n  // Set the User-Agent header value\n  if (message.__ow_headers && message.__ow_headers['user-agent']) {\n    utils2.setSubUserAgent(message.__ow_headers['user-agent']);\n  }\n\n  // message.apidoc already validated; creating shortcut to it\n  var doc;\n  if (typeof message.apidoc === 'object') {\n    doc = message.apidoc;\n  } else if (typeof message.apidoc === 'string') {\n    doc = JSON.parse(message.apidoc);\n  }\n\n  // message.swagger already validated; creating object\n  var swaggerObj;\n  if (typeof doc.swagger === 'object') {\n    swaggerObj = doc.swagger;\n  } else if (typeof doc.swagger === 'string') {\n    swaggerObj = JSON.parse(doc.swagger);\n  }\n  doc.swagger = swaggerObj;\n\n  var basepath = getBasePath(doc);\n\n  var tenantInstance = message.tenantInstance || 'openwhisk';\n\n  // This can be invoked as either a standard web action or as a normal action\n  var calledAsWebAction = message.__ow_method !== undefined;\n\n  // Log parameter values\n  console.log('GW URL        : '+message.gwUrl);\n  console.log('GW URL V2     : '+message.gwUrlV2);\n  console.log('GW Auth       : '+utils.confidentialPrint(message.gwPwd));\n  console.log('__ow_user     : '+message.__ow_user);\n  console.log('namespace     : '+doc.namespace);\n  console.log('tenantInstance: '+message.tenantInstance+' / '+tenantInstance);\n  console.log('accesstoken   : '+message.accesstoken);\n  console.log('spaceguid     : '+message.spaceguid);\n  console.log('responsetype  : '+message.responsetype);\n  console.log('API name      : '+doc.apiName);\n  console.log('basepath      : '+basepath);\n  console.log('relpath       : '+doc.gatewayPath);\n  console.log('GW method     : '+doc.gatewayMethod);\n  if (doc.action) {\n    console.log('action name: '+doc.action.name);\n    console.log('action namespace: '+doc.action.namespace);\n    console.log('action backendUrl: '+doc.action.backendUrl);\n    console.log('action backendMethod: '+doc.action.backendMethod);\n    console.log('action authkey: '+utils.confidentialPrint(doc.action.authkey));\n    console.log('action secureKey: '+utils.confidentialPrint(doc.action.secureKey));\n  }\n  console.log('calledAsWebAction: '+calledAsWebAction);\n  console.log('apidoc        :\\n'+JSON.stringify(doc));\n\n  // If an API GW access token is provided, use the API GW V2 URL and use this token to auth with the API GW\n  // Otherwise, use the API GW \"V1\" URL and use the supplied GW auth credentials to auth with the API GW\n  if (message.accesstoken) {\n    var apiDocId;\n    gwInfo.gwUrl = message.gwUrlV2;\n    gwInfo.gwAuth = message.accesstoken;\n    // 1. If an existing API exists for this namespace/basepath combination, retrieve it and update it\n    // 2. If not, create a new API\n    return utils2.getApis(gwInfo, message.spaceguid, basepath)\n    .then(function(endpointDocs) {\n      console.log('Got '+endpointDocs.length+' APIs');\n      if (endpointDocs.length === 0) {\n        console.log('No API found for namespace '+doc.namespace + ' with basePath '+ basepath);\n        return Promise.resolve(utils2.generateBaseSwaggerApi(basepath, doc.apiName));\n      } else {\n        apiDocId = endpointDocs[0].artifact_id;\n        return Promise.resolve(endpointDocs[0].open_api_doc);\n      }\n    })\n    .then(function(endpointDoc) {\n      if (doc.swagger) {\n        console.log('Use provided swagger as the entire API; override any existing API');\n        return Promise.resolve(doc.swagger);\n      } else {\n        console.log('Add the provided API endpoint');\n        return Promise.resolve(utils2.addEndpointToSwaggerApi(endpointDoc, doc, message.responsetype));\n      }\n    })\n    .then(function(apiSwagger){\n      console.log(\"Validating Swagger doc before sending it to API GW.\")\n      return utils2.validateFinalSwagger(apiSwagger);\n    })\n    .then(function(apiSwagger) {\n      console.log('Final swagger API config: '+ JSON.stringify(apiSwagger));\n      return utils2.addApiToGateway(gwInfo, message.spaceguid, apiSwagger, apiDocId);\n    })\n    .then(function(gwApi) {\n      console.log('API GW configured with API');\n      var cliApi = utils2.generateCliApiFromGwApi(gwApi).value;\n      console.log('createApi success');\n      return Promise.resolve(utils2.makeResponseObject(cliApi, calledAsWebAction));\n    })\n    .catch(function(reason) {\n      var rejmsg = 'API creation failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes\n      console.error(rejmsg);\n      return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));\n    });\n  } else {\n    // Create and activate a new API path\n    // 1. Create tenant id this namespace.  If id exists, create is a noop\n    // 2. Obtain any existing configuration for the target API.  If none, this is a new API\n    // 3. Create the API document to send to the API GW.  If API exists, update it\n    // 4. Configure API GW with the new/updated API\n    var tenantId;\n    var gwApiId;\n    if (message.gwUser && message.gwPwd) {\n      gwInfo.gwAuth = Buffer.from(message.gwUser+':'+message.gwPwd,'ascii').toString('base64');\n    }\n    return utils.createTenant(gwInfo, doc.namespace, tenantInstance)\n    .then(function(tenant) {\n      console.log('Got the API GW tenant: '+JSON.stringify(tenant));\n      tenantId = tenant.id;\n      return Promise.resolve(utils.getApis(gwInfo, tenant.id, basepath));\n    })\n    .then(function(apis) {\n      console.log('Got '+apis.length+' APIs');\n      if (apis.length === 0) {\n        console.log('No APIs found for namespace '+doc.namespace+' with basepath '+basepath);\n        return Promise.resolve(utils.generateBaseSwaggerApi(basepath, doc.apiName));\n      } else if (apis.length > 1) {\n        console.error('Multiple APIs found for namespace '+doc.namespace+' with basepath '+basepath);\n        return Promise.reject('Internal error. Multiple APIs found for namespace '+doc.namespace+' with basepath '+basepath);\n      }\n      gwApiId = apis[0].id;\n      return Promise.resolve(utils.generateSwaggerApiFromGwApi(apis[0]));\n    })\n    .then(function(swaggerApi) {\n      if (doc.swagger) {\n        console.log('Use provided swagger as the entire API; override any existing API');\n        return Promise.resolve(doc.swagger);\n      } else {\n        console.log('Add the provided API endpoint');\n        return Promise.resolve(utils.addEndpointToSwaggerApi(swaggerApi, doc));\n      }\n    })\n    .then(function(apiSwagger){\n       console.log(\"Validating Swagger doc before sending it to API GW.\")\n       return utils2.validateFinalSwagger(apiSwagger);\n    })\n    .then(function(swaggerApi) {\n      console.log('Final swagger API config: '+ JSON.stringify(swaggerApi));\n      return utils.addApiToGateway(gwInfo, tenantId, swaggerApi, gwApiId);\n    })\n    .then(function(gwApi) {\n      console.log('API GW configured with API');\n      var cliApi = utils.generateCliApiFromGwApi(gwApi).value;\n      console.log('createApi success');\n      return Promise.resolve(utils2.makeResponseObject(cliApi, calledAsWebAction));\n    })\n    .catch(function(reason) {\n      var rejmsg = 'API creation failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes\n      console.error(rejmsg);\n      return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));\n    });\n  }\n}\n\nfunction getBasePath(apidoc) {\n  if (apidoc.swagger) {\n    return apidoc.swagger.basePath;\n  }\n  return apidoc.gatewayBasePath;\n}\n\n\nfunction validateArgs(message) {\n  var tmpdoc;\n  if(!message) {\n    console.error('No message argument!');\n    return 'Internal error.  A message parameter was not supplied.';\n  }\n\n  if (!message.gwUrl && !message.gwUrlV2) {\n    return 'gwUrl is required.';\n  }\n\n  if (!message.__ow_user) {\n    return 'A valid auth key is required.';\n  }\n\n  if(!message.apidoc) {\n    return 'apidoc is required.';\n  }\n  if (typeof message.apidoc == 'object') {\n    tmpdoc = message.apidoc;\n  } else if (typeof message.apidoc === 'string') {\n    try {\n      tmpdoc = JSON.parse(message.apidoc);\n    } catch (e) {\n      return 'apidoc field cannot be parsed. Ensure it is valid JSON.';\n    }\n  } else {\n    return 'apidoc field is of type ' + (typeof message.apidoc) + ' and should be a JSON object or a JSON string.';\n  }\n\n  if (!tmpdoc.namespace) {\n    return 'apidoc is missing the namespace field';\n  }\n\n var tmpSwaggerDoc;\n  if(tmpdoc.swagger) {\n    if (tmpdoc.gatewayBasePath) {\n      return 'swagger and gatewayBasePath are mutually exclusive and cannot be specified together.';\n    }\n    if (typeof tmpdoc.swagger == 'object') {\n      tmpSwaggerDoc = tmpdoc.swagger;\n    } else if (typeof tmpdoc.swagger === 'string') {\n      try {\n        tmpSwaggerDoc = JSON.parse(tmpdoc.swagger);\n      } catch (e) {\n        return 'swagger field cannot be parsed. Ensure it is valid JSON.';\n      }\n    } else {\n      return 'swagger field is ' + (typeof tmpdoc.swagger) + ' and should be an object or a JSON string.';\n    }\n    console.log('Swagger JSON object: ', tmpSwaggerDoc);\n    if (!tmpSwaggerDoc.basePath) {\n      return 'swagger is missing the basePath field.';\n    }\n    if (!tmpSwaggerDoc.paths) {\n      return 'swagger is missing the paths field.';\n    }\n    if (!tmpSwaggerDoc.info) {\n      return 'swagger is missing the info field.';\n    }\n  } else {\n    if (!tmpdoc.gatewayBasePath) {\n      return 'apidoc is missing the gatewayBasePath field';\n    }\n\n    if (!tmpdoc.gatewayPath) {\n      return 'apidoc is missing the gatewayPath field';\n    }\n\n    if (!tmpdoc.gatewayMethod) {\n      return 'apidoc is missing the gatewayMethod field';\n    }\n\n    if (!tmpdoc.action) {\n      return 'apidoc is missing the action field.';\n    }\n\n    if (!tmpdoc.action.backendMethod) {\n      return 'action is missing the backendMethod field.';\n    }\n\n    if (!tmpdoc.action.backendUrl) {\n      return 'action is missing the backendUrl field.';\n    }\n\n    if (!tmpdoc.action.namespace) {\n      return 'action is missing the namespace field.';\n    }\n\n    if(!tmpdoc.action.name) {\n      return 'action is missing the name field.';\n    }\n\n    if (!tmpdoc.action.authkey) {\n      return 'action is missing the authkey field.';\n    }\n  }\n\n  return '';\n}\n\nmodule.exports.main = main;\n"
  },
  {
    "path": "core/routemgmt/createApi/package.json",
    "content": "{\n  \"main\": \"createApi.js\",\n  \"dependencies\": {\n    \"lodash\": \"4.17.15\",\n    \"request\": \"2.88.0\"\n  }\n}\n"
  },
  {
    "path": "core/routemgmt/deleteApi/deleteApi.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n *\n * Delete an API Gateway to action mapping document from the database:\n * https://docs.cloudant.com/document.html#delete\n *\n * Parameters (all as fields in the message JSON object)\n *   gwUrlV2              Required when accesstoken is provided. The V2 API Gateway base path (i.e. http://gw.com)\n *   gwUrl                Required. The API Gateway base path (i.e. http://gw.com)\n *   gwUser               Optional. The API Gateway authentication\n *   gwPwd                Optional. The API Gateway authentication\n *   __ow_user            Optional. Set to the authenticated API authors's namespace when valid authentication is supplied.\n *   namespace            Required if __ow_user not specified.  Namespace of API author\n *   accesstoken          Optional. Dynamic API GW auth.  Overrides gwUser/gwPwd\n *   spaceguid            Optional. Namespace unique id.\n *   tenantInstance       Optional. Instance identifier used when creating the specific API GW Tenant\n *   basepath             Required. Base path or API name of the API\n *   relpath              Optional. Delete just this relative path from the API.  Required if operation is specified\n *   operation            Optional. Delete just this relpath's operation from the API.\n *\n * NOTE: The package containing this action will be bound to the following values:\n *         gwUrl, gwAuth\n *       As such, the caller to this action should normally avoid explicitly setting\n *       these values\n **/\nvar utils = require('./utils.js');\nvar utils2 = require('./apigw-utils.js');\nvar _ = require('lodash');\n\nfunction main(message) {\n  //console.log('message: '+JSON.stringify(message));  // ONLY FOR TEMPORARY/LOCAL DEBUG; DON'T ENABLE PERMANENTLY\n  var badArgMsg = validateArgs(message);\n  if (badArgMsg) {\n    return Promise.reject(utils2.makeErrorResponseObject(badArgMsg, (message.__ow_method != undefined)));\n  }\n\n  var gwInfo = {\n    gwUrl: message.gwUrl,\n  };\n  if (message.gwUser && message.gwPwd) {\n    gwInfo.gwAuth = Buffer.from(message.gwUser+':'+message.gwPwd,'ascii').toString('base64');\n  }\n\n  // Set the User-Agent header value\n  if (message.__ow_headers && message.__ow_headers['user-agent']) {\n    utils2.setSubUserAgent(message.__ow_headers['user-agent']);\n  }\n\n  // Set namespace override if provided\n  message.namespace = message.__ow_user || message.namespace;\n\n  var tenantInstance = message.tenantInstance || 'openwhisk';\n\n  // This can be invoked as either a web action or as a normal action\n  var calledAsWebAction = message.__ow_method !== undefined;\n\n  // Log parameter values\n  console.log('GW URL        : '+message.gwUrl);\n  console.log('GW URL V2     : '+message.gwUrlV2);\n  console.log('GW User       : '+utils.confidentialPrint(message.gwUser));\n  console.log('GW Pwd        : '+utils.confidentialPrint(message.gwPwd));\n  console.log('__ow_user     : '+message.__ow_user);\n  console.log('namespace     : '+message.namespace);\n  console.log('tenantInstance: '+message.tenantInstance+' / '+tenantInstance);\n  console.log('accesstoken   : '+message.accesstoken);\n  console.log('spaceguid     : '+message.spaceguid);\n  console.log('basepath/name : '+message.basepath);\n  console.log('relpath       : '+message.relpath);\n  console.log('operation     : '+message.operation);\n  console.log('calledAsWebAction: '+calledAsWebAction);\n\n  // If no relpath (or relpath/operation) is specified, delete the entire API\n  var deleteEntireApi = !message.relpath;\n\n  if (message.accesstoken) {\n    // Delete an API route\n    // 1. Use the spaceguid and basepath to obtain the API from the API GW\n    // 2. If a relpath or relpath/operation is specified (i.e. delete subset of API)\n    //    a. Remove that section from the API config\n    //    b. Update API GW with updated API config\n    // 3. If relpath or replath/operation is NOT specified (i.e. delete entire API)\n    //    a. Delete entire API from API GW\n    gwInfo.gwUrl = message.gwUrlV2;\n    gwInfo.gwAuth = message.accesstoken;\n\n    return utils2.getApis(gwInfo, message.spaceguid, message.basepath)\n    .then(function(endpointDocs) {\n      console.log('Got '+endpointDocs.length+' APIs');\n      if (endpointDocs.length === 0) {\n        console.log('No API found for namespace '+message.namespace + ' with basePath '+ message.basepath);\n        return Promise.reject('API \\''+message.basepath+'\\' does not exist.');\n      } else if (endpointDocs.length > 1) {\n        console.error('Multiple APIs found for namespace '+message.namespace+' with basepath/apiname '+message.basepath);\n      }\n      return Promise.resolve(endpointDocs[0]);\n    })\n    .then(function(endpointDoc) {\n      console.log('Got API');\n      if (deleteEntireApi) {\n        console.log('Removing entire API '+message.basepath+' from API GW');\n        return utils2.deleteApiFromGateway(gwInfo, message.spaceguid, endpointDoc.artifact_id);\n      } else {\n        console.log('Removing path '+message.relpath+' with operation '+message.operation+' from API '+message.basepath);\n        var endpointToRemove = {\n          gatewayMethod: message.operation,\n          gatewayPath: message.relpath\n        };\n        var swaggerOrErrMsg = utils2.removeEndpointFromSwaggerApi(endpointDoc.open_api_doc, endpointToRemove);\n        if (typeof swaggerOrErrMsg === 'string' ) {\n          return Promise.reject(swaggerOrErrMsg);\n        }\n        if (_.isEmpty(swaggerOrErrMsg.paths)) {\n          console.log('After path/operation removal, no paths exist in API; so removing entire API '+message.basepath+' from API GW');\n          return utils2.deleteApiFromGateway(gwInfo, message.spaceguid, endpointDoc.artifact_id);\n        }\n        return utils2.addApiToGateway(gwInfo, message.spaceguid, swaggerOrErrMsg, endpointDoc.artifact_id);\n      }\n    })\n    .then(function() {\n      console.log('deleteApi success');\n      return Promise.resolve(utils2.makeResponseObject({}, calledAsWebAction));\n    })\n    .catch(function(reason) {\n        var rejmsg = 'API deletion failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes\n        console.error(rejmsg);\n        return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));\n    });\n  } else {\n    // Delete an API route\n    // 1. Get the tenant ID associated with the specified namespace and optional tenant instance\n    // 2. Obtain the tenantId/basepath/apiName associated API configuration from the API GW\n    // 3. If a relpath or relpath/operation is specified (i.e. delete subset of API)\n    //    a. Remove that section from the API config\n    //    b. Update API GW with updated API config\n    // 4. If relpath or replath/operation is NOT specified (i.e. delete entire API)\n    //    a. Delete entire API from API GW\n    var tenantId;\n    return utils.getTenants(gwInfo, message.namespace, tenantInstance)\n    .then(function(tenants) {\n      // If a non-empty tenant array was returned, pick the first one from the list\n      if (tenants.length === 0) {\n        console.error('No Tenant found for namespace '+message.namespace);\n        return Promise.reject('No Tenant found for namespace '+message.namespace);\n      } else if (tenants.length > 1 ) {\n        console.error('Multiple tenants found for namespace '+message.namespace+' and tenant instance '+tenantInstance);\n        return Promise.reject('Internal error. Multiple API Gateway tenants found for namespace '+message.namespace+' and tenant instance '+tenantInstance);\n      }\n      console.log('Got a tenant: '+JSON.stringify(tenants[0]));\n      tenantId = tenants[0].id;\n      return Promise.resolve(tenants[0].id);\n    })\n    .then(function(tenantId) {\n      console.log('Got Tenant ID: '+tenantId);\n      return utils.getApis(gwInfo, tenantId, message.basepath);\n    })\n    .then(function(apis) {\n      console.log('Got '+apis.length+' APIs');\n      if (apis.length === 0) {\n        console.log('No APIs found for namespace '+message.namespace+' with basepath/apiname '+message.basepath);\n        return Promise.reject('API \\''+message.basepath+'\\' does not exist.');\n      } else if (apis.length > 1) {\n        console.error('Multiple APIs found for namespace '+message.namespace+' with basepath/apiname '+message.basepath);\n        Promise.reject('Internal error. Multiple APIs found for namespace '+message.namespace+' with basepath '+message.basepath);\n      }\n      return Promise.resolve(apis[0]);\n    })\n    .then(function(gwApi) {\n      if (deleteEntireApi) {\n        console.log('Removing entire API '+gwApi.basePath+' from API GW');\n        return utils.deleteApiFromGateway(gwInfo, gwApi.id);\n      } else {\n        console.log('Removing path '+message.relpath+'; operation '+message.operation+' from API '+gwApi.basePath);\n        var swaggerApi = utils.generateSwaggerApiFromGwApi(gwApi);\n        var endpoint = {\n          gatewayMethod: message.operation,\n          gatewayPath: message.relpath\n        };\n        var swaggerOrErrMsg = utils.removeEndpointFromSwaggerApi(swaggerApi, endpoint);\n        if (typeof swaggerOrErrMsg === 'string' ) {\n          return Promise.reject(swaggerOrErrMsg);\n        }\n        return utils.addApiToGateway(gwInfo, gwApi.tenantId, swaggerOrErrMsg, gwApi.id);\n      }\n    })\n    .then(function() {\n      console.log('deleteApi success');\n      return Promise.resolve(utils2.makeResponseObject({}, calledAsWebAction));\n    })\n    .catch(function(reason) {\n      var rejmsg = 'API deletion failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes\n      console.error(rejmsg);\n      return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));\n    });\n  }\n}\n\n\nfunction validateArgs(message) {\n  var tmpdoc;\n  if(!message) {\n    console.error('No message argument!');\n    return 'Internal error.  A message parameter was not supplied.';\n  }\n\n  if (!message.gwUrl && !message.gwUrlV2) {\n    return 'gwUrl is required.';\n  }\n\n  if (!message.__ow_user && !message.namespace) {\n    return 'Invalid authentication.';\n  }\n\n  if (!message.basepath) {\n    return 'basepath is required.';\n  }\n\n  if (!message.relpath && message.operation) {\n    return 'When specifying an operation, the path is required.';\n  }\n\n  if (message.operation) {\n    message.operation = message.operation.toLowerCase();\n  }\n\n  return '';\n}\n\nmodule.exports.main = main;\n"
  },
  {
    "path": "core/routemgmt/deleteApi/package.json",
    "content": "{\n  \"main\": \"deleteApi.js\",\n  \"dependencies\": {\n    \"lodash\": \"4.17.15\",\n    \"request\": \"2.88.0\"\n  }\n}\n"
  },
  {
    "path": "core/routemgmt/getApi/getApi.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n *\n * Retrieve API configuration from the API Gateway:\n *\n * Parameters (all as fields in the message JSON object)\n *   gwUrlV2              Required when accesstoken is provided. The V2 API Gateway base path (i.e. http://gw.com)\n *   gwUrl                Required. The API Gateway base path (i.e. http://gw.com)\n *   gwUser               Optional. The API Gateway authentication\n *   gwPwd                Optional. The API Gateway authentication\n *   __ow_user            Optional. Set to the authenticated API authors's namespace when valid authentication is supplied.\n *   namespace            Required if __ow_user not specified.  Namespace of API author\n *   tenantInstance       Optional. Instance identifier used when creating the specific API GW Tenant\n *   accesstoken          Optional. Dynamic API GW auth.  Overrides gwUser/gwPwd\n *   spaceguid            Optional. Namespace unique id.\n *   basepath             Optional. Base path or API name of the API.\n *                                  If not provided, all APIs for the namespace are returned\n *   relpath              Optional. Must be defined with 'operation'.  Filters API result to path/operation\n *   operation            Optional. Must be defined with 'relpath'.  Filters API result to path/operation\n *   outputFormat         Optional. Defaults to 'swagger'.  Possible values:\n *                                  'apigw' = return API as obtained from the API Gateway\n *                                  'swagger' = return API as swagger compliant JSON\n *\n * NOTE: The package containing this action will be bound to the following values:\n *         gwUrl, gwAuth\n *       As such, the caller to this action should normally avoid explicitly setting\n *       these values\n **/\nvar utils = require('./utils.js');\nvar utils2 = require('./apigw-utils.js');\n\nfunction main(message) {\n  console.log('message: '+JSON.stringify(message));  // ONLY FOR TEMPORARY/LOCAL DEBUG; DON'T ENABLE PERMANENTLY\n  var badArgMsg = validateArgs(message);\n  if (badArgMsg) {\n    return Promise.reject(utils2.makeErrorResponseObject(badArgMsg, (message.__ow_method !== undefined)));\n  }\n\n  message.outputFormat = message.outputFormat || 'swagger';\n  var tenantInstance = message.tenantInstance || 'openwhisk';\n\n  var gwInfo = {\n    gwUrl: message.gwUrl,\n  };\n  if (message.gwUser && message.gwPwd) {\n    gwInfo.gwAuth = Buffer.from(message.gwUser+':'+message.gwPwd,'ascii').toString('base64');\n  }\n\n  // Set the User-Agent header value\n  if (message.__ow_headers && message.__ow_headers['user-agent']) {\n    utils2.setSubUserAgent(message.__ow_headers['user-agent']);\n  }\n\n  // Set namespace override if provided\n  message.namespace = message.__ow_user || message.namespace;\n\n  // This can be invoked as either web action or as a normal action\n  var calledAsWebAction = message.__ow_method !== undefined;\n\n  // Log parameter values\n  console.log('gwUrl         : '+message.gwUrl);\n  console.log('GW URL V2     : '+message.gwUrlV2);\n  console.log('__ow_user     : '+message.__ow_user);\n  console.log('namespace     : '+message.namespace);\n  console.log('tenantInstance: '+message.tenantInstance+' / '+tenantInstance);\n  console.log('accesstoken   : '+message.accesstoken);\n  console.log('spaceguid     : '+message.spaceguid);\n  console.log('limit         : '+message.limit);\n  console.log('skip          : '+message.skip);\n  console.log('basepath/name : '+message.basepath);\n  console.log('relpath       : '+message.relpath);\n  console.log('operation     : '+message.operation);\n  console.log('outputFormat  : '+message.outputFormat);\n  console.log('calledAsWebAction: '+calledAsWebAction);\n\n  if (message.accesstoken) {\n    gwInfo.gwUrl = message.gwUrlV2;\n    gwInfo.gwAuth = message.accesstoken;\n    // Obtain the API from the API GW\n    return utils2.getApis(gwInfo, message.spaceguid, message.basepath, message.limit, message.skip)\n    .then(function(endpointDocs) {\n      console.log('Got '+endpointDocs.length+' APIs');\n      if (endpointDocs.length === 0) {\n        console.log('No API found for namespace '+message.namespace + ' with basePath '+ message.basepath);\n      }\n      var cliApis = utils2.generateCliResponse(endpointDocs);\n      console.log('getApi success');\n      return Promise.resolve(utils2.makeResponseObject({ apis: cliApis }, calledAsWebAction));\n    })\n    .catch(function(reason) {\n      var reasonstr = JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes\n      console.error('API GW failure: '+reasonstr);\n      return Promise.reject(utils2.makeErrorResponseObject(reasonstr, calledAsWebAction));\n    });\n  } else {\n    // Issue a request to read API(s) from the API GW\n    // 1. Get the tenant ID associated with the specified namespace and optional tenant instance\n    // 2. Get the API(s) associated with the tenant ID and optional basepath/apiname\n    // 3. Format the API(s) per the outputFormat specification\n    return utils.getTenants(gwInfo, message.namespace, tenantInstance)\n    .then(function(tenants) {\n      // If a non-empty tenant array was returned, pick the first one from the list\n      if (tenants.length === 0) {\n        console.error('No Tenant found for namespace '+message.namespace);\n        return Promise.reject('No Tenant found for namespace '+message.namespace);\n      } else if (tenants.length > 1 ) {\n        console.error('Multiple tenants found for namespace '+message.namespace+' and tenant instance '+tenantInstance);\n        return Promise.reject('Internal error. Multiple API Gateway tenants found for namespace '+message.namespace+' and tenant instance '+tenantInstance);\n      }\n      console.log('Got a tenant: '+JSON.stringify(tenants[0]));\n      return Promise.resolve(tenants[0].id);\n    })\n    .then(function(tenantId) {\n      console.log('Got Tenant ID: '+tenantId);\n      return utils.getApis(gwInfo, tenantId, message.basepath);\n    })\n    .then(function(apis) {\n      console.log('Got API(s)');\n      if (apis.length === 0) {\n        console.error('No APIs found for namespace '+message.namespace);\n      }\n      var cliApis = utils.generateCliResponse(apis);\n      console.log('getApi success');\n      return Promise.resolve(utils2.makeResponseObject({ apis: cliApis }, calledAsWebAction));\n    })\n    .catch(function(reason) {\n      var reasonstr = JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes\n      var rejmsg = 'API GW failure: ' + reasonstr;\n      console.error(rejmsg);\n      // Special case handling\n      // If no tenant id found, then just return an empty list of APIs\n      if ( (typeof reason === 'string') && (reason.indexOf('No Tenant found') !== -1) ) {\n        console.log('Namespace has no tenant id yet; returning empty list of APIs');\n        return Promise.resolve(utils2.makeResponseObject({ apis: utils.generateCliResponse([]) }, calledAsWebAction));\n      }\n      return Promise.reject(utils2.makeErrorResponseObject(reasonstr, calledAsWebAction));\n    });\n  }\n\n}\n\n\nfunction validateArgs(message) {\n  if(!message) {\n    console.error('No message argument!');\n    return 'Internal error. A message parameter was not supplied.';\n  }\n\n  if (!message.gwUrl && !message.gwUrlV2) {\n    return 'gwUrl is required.';\n  }\n\n  if (!message.__ow_user && !message.namespace) {\n    return 'Invalid authentication.';\n  }\n\n  if (message.outputFormat && !(message.outputFormat.toLowerCase() === 'apigw' || message.outputFormat.toLowerCase() === 'swagger')) {\n    return 'Invalid outputFormat value. Valid values are: apigw, swagger';\n  }\n\n  return '';\n}\n\nmodule.exports.main = main;\n"
  },
  {
    "path": "core/routemgmt/getApi/package.json",
    "content": "{\n  \"main\": \"getApi.js\",\n  \"dependencies\": {\n    \"lodash\": \"4.17.21\",\n    \"request\": \"2.88.0\"\n  }\n}\n"
  },
  {
    "path": "core/scheduler/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nARG BASE=scala\nFROM ${BASE}\n\nENV UID=1001 \\\n    NOT_ROOT_USER=owuser\n\n# Copy app jars\nADD build/distributions/scheduler.tar /\n\nCOPY init.sh /\nRUN chmod +x init.sh\n\nRUN useradd -m -u 1001 -d /home/${NOT_ROOT_USER} -s /bin/bash ${NOT_ROOT_USER}\nUSER ${NOT_ROOT_USER}\n\nEXPOSE 8080\nCMD [\"./init.sh\", \"0\"]\n"
  },
  {
    "path": "core/scheduler/Dockerfile.cov",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM scheduler\n\nARG OW_ROOT_DIR\n\nUSER root\nRUN mkdir -p /coverage/common && \\\n    mkdir -p /coverage/scheduler && \\\n    mkdir -p \"${OW_ROOT_DIR}/common/scala/build\" && \\\n    mkdir -p \"${OW_ROOT_DIR}/core/scheduler/build\" && \\\n    ln -s /coverage/common \"${OW_ROOT_DIR}/common/scala/build/scoverage\" && \\\n    ln -s /coverage/scheduler \"${OW_ROOT_DIR}/core/scheduler/build/scoverage\"\n\nCOPY build/tmp/docker-coverage /scheduler/\n"
  },
  {
    "path": "core/scheduler/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\napply plugin: 'scala'\napply plugin: 'application'\napply plugin: 'eclipse'\napply plugin: 'maven-publish'\napply plugin: 'org.scoverage'\napply plugin: 'org.apache.pekko.grpc.gradle'\n\next.dockerImageName = 'scheduler'\napply from: '../../gradle/docker.gradle'\ndistDocker.dependsOn ':common:scala:distDocker', 'distTar'\n\nproject.archivesBaseName = \"openwhisk-scheduler\"\n\nscoverage {\n    scoverageVersion.set(\"${gradle.scala.scoverageVersion}\")\n    scoverageScalaVersion.set(\"${gradle.scala.scoverageScalaVersion}\")\n}\n\next.coverageDirs = [\n        \"${buildDir}/classes/scala/scoverage\",\n        \"${project(':common:scala').buildDir.absolutePath}/classes/scala/scoverage\"\n]\n\nbuildscript {\n    repositories {\n        mavenLocal()\n        mavenCentral()\n        maven {\n            url \"https://plugins.gradle.org/m2/\"\n        }\n    }\n    dependencies {\n        // Pekko gRPC plugin is deployed in Maven Central\n        // See: https://pekko.apache.org/docs/pekko-grpc/current/release-notes/\n        classpath \"org.apache.pekko:pekko-grpc-gradle-plugin:${gradle.pekko_grpc.version}\"\n    }\n}\n\nprotobuf {\n    protoc {\n        if (osdetector.os == \"osx\") {\n            artifact = 'com.google.protobuf:protoc:3.25.5:osx-x86_64'\n        } else {\n            artifact = 'com.google.protobuf:protoc:3.25.5'\n        }\n    }\n}\n\n// Define a separate configuration for managing the dependency on Jetty ALPN agent.\nconfigurations {\n    alpnagent\n}\n\ndependencies {\n    configurations.all {\n        resolutionStrategy.force \"com.lihaoyi:fastparse_${gradle.scala.depVersion}:2.3.0\"\n        resolutionStrategy.force \"org.apache.pekko:pekko-http-core_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n        resolutionStrategy.force \"org.apache.pekko:pekko-http_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n        resolutionStrategy.force \"org.apache.pekko:pekko-http2-support_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n        resolutionStrategy.force \"org.apache.pekko:pekko-http-spray-json_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n        resolutionStrategy.force \"org.apache.pekko:pekko-parsing_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n        resolutionStrategy.force \"org.apache.pekko:pekko-http_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n    }\n    implementation \"org.scala-lang:scala-library:${gradle.scala.version}\"\n    implementation project(':common:scala')\n    implementation \"io.altoo:pekko-kryo-serialization_${gradle.scala.depVersion}:1.3.0\"\n    implementation \"org.apache.pekko:pekko-management-cluster-bootstrap_${gradle.scala.depVersion}:${gradle.pekko_management.version}\"\n    implementation \"org.apache.pekko:pekko-discovery-kubernetes-api_${gradle.scala.depVersion}:${gradle.pekko_management.version}\"\n}\n\nprintProtocLogs.doFirst {\n    mkdir \"$buildDir\"\n    file(\"$buildDir/pekko-grpc-gradle-plugin.log\").text = \"x\"\n    mkdir \"$project.rootDir/build\"\n    file(\"$project.rootDir/build/pekko-grpc-gradle-plugin.log\").text = \"x\"\n}\nprintProtocLogs.configure {\n    mkdir \"$buildDir\"\n    file(\"$buildDir/pekko-grpc-gradle-plugin.log\").text = \"x\"\n    mkdir \"$project.rootDir/build\"\n    file(\"$project.rootDir/build/pekko-grpc-gradle-plugin.log\").text = \"x\"\n}\n\nmainClassName = \"org.apache.openwhisk.core.scheduler.Scheduler\"\napplicationDefaultJvmArgs = [\"-Djava.security.egd=file:/dev/./urandom\"]\n\n// Explictly declare task dependencies\ntasks.named(\"processResources\") {\n    dependsOn tasks.named(\"extractProto\")\n}\n\ntasks.named(\"processScoverageResources\") {\n    dependsOn tasks.named(\"extractScoverageProto\")\n}\n\n"
  },
  {
    "path": "core/scheduler/init.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n./copyJMXFiles.sh\n\nexport SCHEDULER_OPTS\nSCHEDULER_OPTS=\"$SCHEDULER_OPTS -Dpekko.remote.artery.bind.hostname=$(hostname -i) $(./transformEnvironment.sh)\"\n\nexec scheduler/bin/scheduler \"$@\"\n"
  },
  {
    "path": "core/scheduler/src/main/protobuf/activation.proto",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nsyntax = \"proto3\";\nimport \"google/protobuf/wrappers.proto\";\n\n//#options\noption java_multiple_files = true;\noption java_package = \"org.apache.openwhisk.grpc\";\noption java_outer_classname = \"ActivationProto\";\n\npackage activation;\n//#options\n\n//#services\nservice ActivationService {\n    rpc FetchActivation (FetchRequest) returns (FetchResponse) {}\n    rpc RescheduleActivation (RescheduleRequest) returns (RescheduleResponse) {}\n}\n//#services\n\n//#messages\n// The request message\nmessage FetchRequest {\n    string transactionId = 1;\n    string invocationNamespace = 2;\n    string fqn = 3;\n    string rev = 4;\n    string containerId = 5;\n    bool warmed = 6;\n    // This allows optional value\n    google.protobuf.Int64Value lastDuration = 7;\n    // to record alive containers\n    bool alive = 8;\n}\n\n// The response message\nmessage FetchResponse {\n    string activationMessage = 1;\n}\n\nmessage RescheduleRequest {\n    string invocationNamespace = 1;\n    string fqn = 2;\n    string rev = 3;\n    string activationMessage = 4;\n}\n\nmessage RescheduleResponse {\n    // if reschedule request is failed, then it will be `false`\n    bool isRescheduled = 1;\n}\n\n"
  },
  {
    "path": "core/scheduler/src/main/resources/application.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\npekko {\n  actor {\n    provider = cluster\n    allow-java-serialization = off\n    serializers {\n      kryo = \"io.altoo.serialization.kryo.pekko.PekkoKryoSerializer\"\n    }\n    serialization-bindings {\n      \"org.apache.openwhisk.core.scheduler.queue.CreateQueue\" = kryo\n      \"org.apache.openwhisk.core.scheduler.queue.CreateQueueResponse\" = kryo\n      \"org.apache.openwhisk.core.connector.ActivationMessage\" = kryo\n    }\n    kryo {\n      idstrategy = \"automatic\"\n      classes = [\n        \"org.apache.openwhisk.core.scheduler.queue.CreateQueue\",\n        \"org.apache.openwhisk.core.scheduler.queue.CreateQueueResponse\",\n        \"org.apache.openwhisk.core.connector.ActivationMessage\"\n      ]\n    }\n  }\n\n  remote {\n    artery {\n      enabled = on\n      transport = tcp\n    }\n  }\n\n  cluster {\n    shutdown-after-unsuccessful-join-seed-nodes = 60s\n\n    # Disable legacy metrics in pekko-cluster.\n    metrics.enabled = off\n  }\n}\n\npekko-kryo-serialization.kryo-initializer = \"org.apache.openwhisk.core.scheduler.CompatibleKryoInitializer\"\n\nwhisk {\n  cluster {\n    use-cluster-bootstrap: false\n  }\n\n  # tracing configuration\n  tracing {\n    component = \"Scheduler\"\n  }\n\n  fraction {\n      managed-fraction: 90%\n      blackbox-fraction: 10%\n  }\n\n  scheduler {\n    protocol = \"http\"\n    username: \"scheduler.user\"\n    password: \"scheduler.pass\"\n    grpc {\n      tls = \"false\"\n    }\n    scheduling {\n      stale-threshold = \"100 milliseconds\"\n      check-interval = \"100 milliseconds\"\n      drop-interval = \"10 seconds\"\n      allow-over-provision-before-throttle = false\n      namespace-over-provision-before-throttle-ratio = 1.5\n    }\n    queue {\n      idle-grace = \"20 seconds\"\n      stop-grace = \"20 seconds\"\n      flush-grace = \"60 seconds\"\n      graceful-shutdown-timeout = \"5 seconds\"\n      max-retention-size = \"10000\"\n      max-retention-ms = \"60000\"\n      max-blackbox-retention-ms = \"300000\"\n      throttling-fraction = \"0.9\"\n      duration-buffer-size = \"10\"\n      fail-throttle-as-whisk-error = \"false\"\n    }\n    queue-manager {\n      max-scheduling-time = \"20 seconds\"\n      max-retries-to-get-queue = \"13\"\n    }\n    max-peek = \"128\"\n    in-progress-job-retention = \"20 seconds\"\n    blackbox-multiple = \"15\"\n    data-management-service {\n        retry-interval = \"1 second\"\n    }\n  }\n}\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/FPCSchedulerServer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.http.BasicRasService\nimport org.apache.openwhisk.http.CorsSettings.RespondWithServerCorsHeaders\nimport org.apache.openwhisk.http.ErrorResponse.terminate\nimport pureconfig.loadConfigOrThrow\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.concurrent.ExecutionContext\n\n/**\n * Implements web server to handle certain REST API calls.\n * Currently provides a health ping route, only.\n */\nclass FPCSchedulerServer(scheduler: SchedulerCore, systemUsername: String, systemPassword: String)(\n  implicit val ec: ExecutionContext,\n  implicit val actorSystem: ActorSystem,\n  implicit val logger: Logging)\n    extends BasicRasService\n    with RespondWithServerCorsHeaders {\n\n  override def routes(implicit transid: TransactionId): Route = {\n    super.routes ~ sendCorsHeaders {\n      options {\n        complete(OK)\n      } ~ extractCredentials {\n        case Some(BasicHttpCredentials(username, password))\n            if username == systemUsername && password == systemPassword =>\n          (path(\"state\") & get) {\n            complete {\n              scheduler.getState.map {\n                case (list, creationCount) =>\n                  val sum = list.map(tuple => tuple._2).sum\n                  (Map(\"queue\" -> sum.toString) ++ Map(\"creationCount\" -> creationCount.toString)).toJson\n              }\n            }\n          } ~ (path(\"disable\") & post) {\n            logger.warn(this, \"Scheduler is disabled\")\n            scheduler.disable()\n            complete(\"scheduler disabled\")\n          } ~ (pathPrefix(FPCSchedulerServer.queuePathPrefix) & get) {\n            pathEndOrSingleSlash {\n              complete(scheduler.getQueueStatusData.map(s => s.toJson))\n            } ~ (path(\"count\") & get) {\n              complete(scheduler.getQueueSize.map(s => s.toJson))\n            }\n          } ~ (path(\"activation\" / \"count\") & get) {\n            pathEndOrSingleSlash {\n              complete(\n                scheduler.getQueueStatusData\n                  .map { s =>\n                    s.map(_.waitingActivation.size)\n                  }\n                  .map(a => a.sum)\n                  .map(_.toJson))\n            }\n          }\n        case _ =>\n          implicit val jsonPrettyResponsePrinter = PrettyPrinter\n          terminate(StatusCodes.Unauthorized)\n      }\n    }\n  }\n}\n\nobject FPCSchedulerServer {\n\n  private val schedulerUsername = loadConfigOrThrow[String](ConfigKeys.whiskSchedulerUsername)\n  private val schedulerPassword = loadConfigOrThrow[String](ConfigKeys.whiskSchedulerPassword)\n  private val queuePathPrefix = \"queues\"\n\n  def instance(scheduler: SchedulerCore)(implicit ec: ExecutionContext,\n                                         actorSystem: ActorSystem,\n                                         logger: Logging): BasicRasService =\n    new FPCSchedulerServer(scheduler, schedulerUsername, schedulerPassword)\n}\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/Scheduler.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.{ActorRef, ActorRefFactory, ActorSelection, ActorSystem, CoordinatedShutdown, Props}\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.model.{HttpRequest, HttpResponse}\nimport org.apache.pekko.management.scaladsl.PekkoManagement\nimport org.apache.pekko.management.cluster.bootstrap.ClusterBootstrap\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.util.Timeout\nimport com.typesafe.config.ConfigValueFactory\nimport io.altoo.serialization.kryo.pekko.DefaultKryoInitializer\nimport io.altoo.serialization.kryo.scala.serializer.ScalaKryo\nimport kamon.Kamon\nimport org.apache.openwhisk.common.Https.HttpsConfig\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.WhiskConfig.{servicePort, _}\nimport org.apache.openwhisk.core.ack.{MessagingActiveAck, UserEventSender}\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.database.{ActivationStoreProvider, NoDocumentException, UserContext}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.etcd.EtcdKV.{QueueKeys, SchedulerKeys}\nimport org.apache.openwhisk.core.etcd.EtcdType.ByteStringToString\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdConfig, EtcdWorker}\nimport org.apache.openwhisk.core.scheduler.container.{ContainerManager, CreationJobManager}\nimport org.apache.openwhisk.core.scheduler.grpc.ActivationServiceImpl\nimport org.apache.openwhisk.core.scheduler.queue._\nimport org.apache.openwhisk.core.service.{DataManagementService, LeaseKeepAliveService, WatcherService}\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.grpc.ActivationServiceHandler\nimport org.apache.openwhisk.http.BasicHttpService\nimport org.apache.openwhisk.spi.SpiLoader\nimport org.apache.openwhisk.utils.ExecutionContextFactory\nimport pureconfig.generic.auto._\nimport pureconfig.loadConfigOrThrow\nimport spray.json.{DefaultJsonProtocol, _}\n\nimport scala.collection.JavaConverters\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, Future}\nimport scala.language.postfixOps\nimport scala.util.{Failure, Success, Try}\n\nclass Scheduler(schedulerId: SchedulerInstanceId, schedulerEndpoints: SchedulerEndpoints)(implicit config: WhiskConfig,\n                                                                                          actorSystem: ActorSystem,\n                                                                                          logging: Logging)\n    extends SchedulerCore {\n  implicit val ec = actorSystem.dispatcher\n  private val authStore = WhiskAuthStore.datastore()\n\n  val msgProvider = SpiLoader.get[MessagingProvider]\n  val producer = msgProvider.getProducer(config, Some(ActivationEntityLimit.MAX_ACTIVATION_LIMIT))\n\n  val maxPeek = loadConfigOrThrow[Int](ConfigKeys.schedulerMaxPeek)\n  val etcdClient = EtcdClient(loadConfigOrThrow[EtcdConfig](ConfigKeys.etcd))\n  val watcherService: ActorRef = actorSystem.actorOf(WatcherService.props(etcdClient))\n  val leaseService =\n    actorSystem.actorOf(LeaseKeepAliveService.props(etcdClient, schedulerId, watcherService))\n\n  val schedulingConfig = loadConfigOrThrow[SchedulingConfig](ConfigKeys.schedulerScheduling)\n\n  implicit val entityStore = WhiskEntityStore.datastore()\n  private val activationStore =\n    SpiLoader.get[ActivationStoreProvider].instance(actorSystem, logging)\n\n  private val ack = {\n    val sender = if (UserEvents.enabled) Some(new UserEventSender(producer)) else None\n    new MessagingActiveAck(producer, schedulerId, sender)\n  }\n\n  /** Stores an activation in the database. */\n  private val store = (tid: TransactionId, activation: WhiskActivation, context: UserContext) => {\n    implicit val transid: TransactionId = tid\n    activationStore.store(activation, context)(tid, notifier = None).andThen {\n      case Success(doc) => logging.info(this, s\"save ${doc} successfully\")\n      case Failure(t)   => logging.error(this, s\"failed to save activation $activation, error: ${t.getMessage}\")\n    }\n  }\n  val durationCheckerProvider = SpiLoader.get[DurationCheckerProvider]\n  val durationChecker = durationCheckerProvider.instance(actorSystem, logging)\n\n  override def getState: Future[(List[(SchedulerInstanceId, Int)], Int)] = {\n    logging.info(this, s\"getting the queue states\")\n    etcdClient\n      .getPrefix(s\"${QueueKeys.inProgressPrefix}/${QueueKeys.queuePrefix}\")\n      .map(res => {\n        JavaConverters\n          .asScalaIteratorConverter(res.getKvsList.iterator())\n          .asScala\n          .map(kv => ByteStringToString(kv.getValue))\n          .count(_ == schedulerId.asString)\n      })\n      .flatMap { creationCount =>\n        etcdClient\n          .get(SchedulerKeys.scheduler(schedulerId))\n          .map(res => {\n            JavaConverters\n              .asScalaIteratorConverter(res.getKvsList.iterator())\n              .asScala\n              .map { kv =>\n                SchedulerStates.parse(kv.getValue).getOrElse(SchedulerStates(schedulerId, -1, schedulerEndpoints))\n              }\n              .map { schedulerState =>\n                (schedulerState.sid, schedulerState.queueSize)\n              }\n              .toList\n          })\n          .map { list =>\n            (list, creationCount)\n          }\n      }\n  }\n\n  override def getQueueSize: Future[Int] = {\n    queueManager.ask(QueueSize)(Timeout(1.minute)).mapTo[Int]\n  }\n\n  override def getQueueStatusData: Future[List[StatusData]] = {\n    queueManager.ask(GetState)(Timeout(1.minute)).mapTo[Future[List[StatusData]]].flatten\n  }\n\n  override def disable(): Unit = {\n    logging.info(this, s\"Gracefully shutting down the scheduler\")\n    containerManager ! GracefulShutdown\n    queueManager ! GracefulShutdown\n  }\n\n  private def getUserLimit(invocationNamespace: String): Future[Int] = {\n    Identity\n      .get(authStore, EntityName(invocationNamespace))(trasnid)\n      .map { identity =>\n        val limit = identity.limits.concurrentInvocations.getOrElse(config.actionInvokeConcurrentLimit.toInt)\n        logging.debug(this, s\"limit for ${invocationNamespace}: ${limit}\")(trasnid)\n        limit\n      }\n      .andThen {\n        case Failure(_: NoDocumentException) =>\n          logging.warn(this, s\"namespace does not exist: $invocationNamespace\")(trasnid)\n        case Failure(_: IllegalStateException) =>\n          logging.warn(this, s\"namespace is not unique: $invocationNamespace\")(trasnid)\n      }\n  }\n\n  private val etcdWorkerFactory = (f: ActorRefFactory) => f.actorOf(EtcdWorker.props(etcdClient, leaseService))\n\n  /**\n   * This component is in charge of storing data to ETCD.\n   * Even if any error happens we can assume the data will be eventually available in the ETCD by this component.\n   */\n  val dataManagementService: ActorRef =\n    actorSystem.actorOf(DataManagementService.props(watcherService, etcdWorkerFactory))\n\n  val feedFactory = (f: ActorRefFactory,\n                     description: String,\n                     topic: String,\n                     maxActiveAcksPerPoll: Int,\n                     processAck: Array[Byte] => Future[Unit]) => {\n    val consumer = msgProvider.getConsumer(config, topic, topic, maxActiveAcksPerPoll)\n    f.actorOf(Props(new MessageFeed(description, logging, consumer, maxActiveAcksPerPoll, 1.second, processAck)))\n  }\n\n  val creationJobManagerFactory: ActorRefFactory => ActorRef =\n    factory => {\n      factory.actorOf(CreationJobManager.props(feedFactory, schedulerId, dataManagementService))\n    }\n\n  /**\n   * This component is responsible for creating containers for a given action.\n   * It relies on the creationJobManager to manage the container creation job.\n   */\n  val containerManager: ActorRef =\n    actorSystem.actorOf(\n      ContainerManager.props(creationJobManagerFactory, msgProvider, schedulerId, etcdClient, config, watcherService))\n\n  /**\n   * This is a factory to create memory queues.\n   * In the new architecture, each action is given its own dedicated queue.\n   */\n  val memoryQueueFactory\n    : (ActorRefFactory, String, FullyQualifiedEntityName, DocRevision, WhiskActionMetaData) => ActorRef =\n    (factory, invocationNamespace, fqn, revision, actionMetaData) => {\n      // Todo: Change this to SPI\n      val decisionMaker = factory.actorOf(SchedulingDecisionMaker.props(invocationNamespace, fqn, schedulingConfig))\n\n      factory.actorOf(\n        MemoryQueue.props(\n          etcdClient,\n          durationChecker,\n          fqn,\n          producer,\n          schedulingConfig,\n          invocationNamespace,\n          revision,\n          schedulerEndpoints,\n          actionMetaData,\n          dataManagementService,\n          watcherService,\n          containerManager,\n          decisionMaker,\n          schedulerId: SchedulerInstanceId,\n          ack,\n          store: (TransactionId, WhiskActivation, UserContext) => Future[Any],\n          getUserLimit: String => Future[Int]))\n    }\n\n  val topic = s\"${Scheduler.topicPrefix}scheduler${schedulerId.asString}\"\n  val schedulerConsumer =\n    msgProvider.getConsumer(config, topic, topic, maxPeek, maxPollInterval = TimeLimit.MAX_DURATION + 1.minute)\n\n  implicit val trasnid = TransactionId.containerCreation\n\n  /**\n   * This is one of the major components which take charge of managing queues and coordinating requests among the scheduler, controllers, and invokers.\n   */\n  val queueManager = actorSystem.actorOf(\n    QueueManager.props(\n      entityStore,\n      WhiskActionMetaData.get,\n      etcdClient,\n      schedulerEndpoints,\n      schedulerId,\n      dataManagementService,\n      watcherService,\n      ack,\n      store: (TransactionId, WhiskActivation, UserContext) => Future[Any],\n      memoryQueueFactory,\n      schedulerConsumer),\n    QueueManager.actorName)\n\n  val serviceHandlers: HttpRequest => Future[HttpResponse] = ActivationServiceHandler.apply(ActivationServiceImpl())\n}\n\ncase class CmdLineArgs(uniqueName: Option[String] = None, id: Option[Int] = None, displayedName: Option[String] = None)\n\ntrait SchedulerCore {\n  def getState: Future[(List[(SchedulerInstanceId, Int)], Int)]\n\n  def getQueueSize: Future[Int]\n\n  def getQueueStatusData: Future[List[StatusData]]\n\n  def disable(): Unit\n}\n\nobject Scheduler {\n\n  protected val protocol = loadConfigOrThrow[String](\"whisk.scheduler.protocol\")\n  protected val useClusterBootstrap = loadConfigOrThrow[Boolean](\"whisk.cluster.use-cluster-bootstrap\")\n\n  val topicPrefix = loadConfigOrThrow[String](ConfigKeys.kafkaTopicsPrefix)\n\n  /**\n   * The scheduler has two ports, one for pekko-remote and the other for pekko-grpc.\n   */\n  def requiredProperties =\n    Map(\n      servicePort -> 8080.toString,\n      schedulerHost -> null,\n      schedulerPekkoPort -> null,\n      schedulerRpcPort -> null,\n      WhiskConfig.actionInvokeConcurrentLimit -> null) ++\n      kafkaHosts ++\n      ExecManifest.requiredProperties\n\n  def initKamon(instance: SchedulerInstanceId): Unit = {\n    // Replace the hostname of the scheduler to the assigned id of the scheduler.\n    val newKamonConfig = Kamon.config\n      .withValue(\"kamon.environment.host\", ConfigValueFactory.fromAnyRef(s\"scheduler${instance.asString}\"))\n    Kamon.init(newKamonConfig)\n  }\n\n  def main(args: Array[String]): Unit = {\n    implicit val ec = ExecutionContextFactory.makeCachedThreadPoolExecutionContext()\n    implicit val actorSystem: ActorSystem =\n      ActorSystem(name = \"scheduler-actor-system\", defaultExecutionContext = Some(ec))\n\n    implicit val logger = new PekkoLogging(org.apache.pekko.event.Logging.getLogger(actorSystem, this))\n\n    if (useClusterBootstrap) {\n      PekkoManagement(actorSystem).start()\n      ClusterBootstrap(actorSystem).start()\n    }\n\n    // Prepare Kamon shutdown\n    CoordinatedShutdown(actorSystem).addTask(CoordinatedShutdown.PhaseActorSystemTerminate, \"shutdownKamon\") { () =>\n      logger.info(this, s\"Shutting down Kamon with coordinated shutdown\")\n      Kamon.stopModules().map(_ => Done)\n    }\n\n    def abort(message: String) = {\n      logger.error(this, message)\n      actorSystem.terminate()\n      Await.result(actorSystem.whenTerminated, 30.seconds)\n      sys.exit(1)\n    }\n\n    // extract configuration data from the environment\n    implicit val config = new WhiskConfig(requiredProperties)\n    if (!config.isValid) {\n      abort(\"Bad configuration, cannot start.\")\n    }\n\n    val port = config.servicePort.toInt\n    val host = config.schedulerHost\n    val rpcPort = config.schedulerRpcPort.toInt\n    val pekkoPort = config.schedulerPekkoPort.toInt\n\n    // if deploying multiple instances (scale out), must pass the instance number as they need to be uniquely identified.\n    require(args.length >= 1, \"scheduler instance required\")\n    val instanceId = SchedulerInstanceId(args(0))\n\n    initKamon(instanceId)\n\n    val msgProvider = SpiLoader.get[MessagingProvider]\n\n    Seq(\n      (topicPrefix + \"scheduler\" + instanceId.asString, \"scheduler\", Some(ActivationEntityLimit.MAX_ACTIVATION_LIMIT)),\n      (\n        topicPrefix + \"creationAck\" + instanceId.asString,\n        \"creationAck\",\n        Some(ActivationEntityLimit.MAX_ACTIVATION_LIMIT)))\n      .foreach {\n        case (topic, topicConfigurationKey, maxMessageBytes) =>\n          if (msgProvider.ensureTopic(config, topic, topicConfigurationKey, maxMessageBytes).isFailure) {\n            abort(s\"failure during msgProvider.ensureTopic for topic $topic\")\n          }\n      }\n\n    ExecManifest.initialize(config) match {\n      case Success(_) =>\n        val schedulerEndpoints = SchedulerEndpoints(host, rpcPort, pekkoPort)\n        // Create scheduler\n        val scheduler = new Scheduler(instanceId, schedulerEndpoints)\n\n        Http()\n          .newServerAt(\"0.0.0.0\", port = rpcPort)\n          .bind(scheduler.serviceHandlers)\n          .foreach { _ =>\n            val httpsConfig =\n              if (Scheduler.protocol == \"https\") Some(loadConfigOrThrow[HttpsConfig](\"whisk.controller.https\"))\n              else None\n\n            BasicHttpService.startHttpService(FPCSchedulerServer.instance(scheduler).route, port, httpsConfig)(\n              actorSystem)\n          }\n      case Failure(t) =>\n        abort(s\"Invalid runtimes manifest: $t\")\n    }\n  }\n}\ncase class SchedulerEndpoints(host: String, rpcPort: Int, pekkoPort: Int) {\n  require(rpcPort != 0 || pekkoPort != 0)\n  def asRpcEndpoint: String = s\"$host:$rpcPort\"\n  def asPekkoEndpoint: String = s\"$host:$pekkoPort\"\n\n  def getRemoteRef(name: String)(implicit context: ActorRefFactory): ActorSelection = {\n    implicit val ec = context.dispatcher\n\n    val path = s\"pekko://scheduler-actor-system@${asPekkoEndpoint}/user/${name}\"\n    context.actorSelection(path)\n  }\n\n  def serialize = SchedulerEndpoints.serdes.write(this).compactPrint\n}\n\nobject SchedulerEndpoints extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat(SchedulerEndpoints.apply, \"host\", \"rpcPort\", \"pekkoPort\")\n  def parse(endpoints: String) = Try(serdes.read(endpoints.parseJson))\n}\n\ncase class SchedulerStates(sid: SchedulerInstanceId, queueSize: Int, endpoints: SchedulerEndpoints) {\n  private implicit val askTimeout = Timeout(5 seconds)\n\n  def getRemoteRef(name: String)(implicit context: ActorRefFactory): ActorSelection = {\n    implicit val ec = context.dispatcher\n\n    val path = s\"pekko://scheduler-actor-system@${endpoints.asPekkoEndpoint}/user/${name}\"\n    context.actorSelection(path)\n  }\n\n  def getSchedulerId(): SchedulerInstanceId = sid\n\n  def serialize = SchedulerStates.serdes.write(this).compactPrint\n\n  override def equals(that: Any): Boolean =\n    that match {\n      case that: SchedulerStates => {\n        this.queueSize == that.queueSize\n      }\n      case _ => false\n    }\n\n  override def hashCode: Int = {\n    var result = 1\n    val prime = 31\n    result = prime * result + queueSize.hashCode()\n    result\n  }\n}\n\nobject SchedulerStates extends DefaultJsonProtocol {\n  private implicit val endpointsSerde = SchedulerEndpoints.serdes\n  implicit val serdes = jsonFormat(SchedulerStates.apply, \"sid\", \"queueSize\", \"endpoints\")\n\n  def parse(states: String) = Try(serdes.read(states.parseJson))\n}\n\ncase class SchedulingConfig(staleThreshold: FiniteDuration,\n                            checkInterval: FiniteDuration,\n                            dropInterval: FiniteDuration,\n                            allowOverProvisionBeforeThrottle: Boolean,\n                            namespaceOverProvisionBeforeThrottleRatio: Double)\n\nclass CompatibleKryoInitializer extends DefaultKryoInitializer {\n  override def preInit(kryo: ScalaKryo): Unit = {\n    super.preInit(kryo)\n    // Use CompatibleFieldSerializer for schema evolution support\n    kryo.setDefaultSerializer(classOf[com.esotericsoftware.kryo.kryo5.serializers.CompatibleFieldSerializer[_]])\n  }\n}\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/container/ContainerManager.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.container\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, ActorSystem, Props}\nimport org.apache.pekko.event.Logging.InfoLevel\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Offline, Unhealthy}\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.connector.ContainerCreationError.{\n  containerCreationErrorToString,\n  NoAvailableInvokersError,\n  NoAvailableResourceInvokersError\n}\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.etcd.EtcdClient\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.containerPrefix\nimport org.apache.openwhisk.core.etcd.EtcdKV.{ContainerKeys, InvokerKeys}\nimport org.apache.openwhisk.core.etcd.EtcdType._\nimport org.apache.openwhisk.core.scheduler.Scheduler\nimport org.apache.openwhisk.core.scheduler.container.ContainerManager.{sendState, updateInvokerMemory}\nimport org.apache.openwhisk.core.scheduler.message._\nimport org.apache.openwhisk.core.scheduler.queue.{MemoryQueueKey, QueuePool}\nimport org.apache.openwhisk.core.service._\nimport org.apache.openwhisk.core.{ConfigKeys, WarmUp, WhiskConfig}\nimport pureconfig.loadConfigOrThrow\nimport spray.json.DefaultJsonProtocol._\nimport pureconfig.generic.auto._\n\nimport java.nio.charset.StandardCharsets\nimport java.util.concurrent.ThreadLocalRandom\nimport scala.annotation.tailrec\nimport scala.collection.JavaConverters._\nimport scala.collection.concurrent.TrieMap\nimport scala.collection.immutable\nimport scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}\nimport scala.util.{Failure, Success}\n\ncase class ScheduledPair(msg: ContainerCreationMessage,\n                         invokerId: Option[InvokerInstanceId],\n                         err: Option[ContainerCreationError] = None)\n\ncase class BlackboxFractionConfig(managedFraction: Double, blackboxFraction: Double)\n\nclass ContainerManager(jobManagerFactory: ActorRefFactory => ActorRef,\n                       provider: MessagingProvider,\n                       schedulerInstanceId: SchedulerInstanceId,\n                       etcdClient: EtcdClient,\n                       config: WhiskConfig,\n                       watcherService: ActorRef)(implicit actorSystem: ActorSystem, logging: Logging)\n    extends Actor {\n  private implicit val ec: ExecutionContextExecutor = context.dispatcher\n\n  private val creationJobManager = jobManagerFactory(context)\n\n  private val messagingProducer = provider.getProducer(config)\n\n  private var warmedContainers = Set.empty[String]\n\n  private val warmedInvokers = TrieMap[Int, String]()\n\n  private val inProgressWarmedContainers = TrieMap.empty[String, String]\n\n  private val warmKey = ContainerKeys.warmedPrefix\n  private val invokerKey = InvokerKeys.prefix\n  private val watcherName = s\"container-manager\"\n\n  watcherService ! WatchEndpoint(warmKey, \"\", isPrefix = true, watcherName, Set(PutEvent, DeleteEvent))\n  watcherService ! WatchEndpoint(invokerKey, \"\", isPrefix = true, watcherName, Set(PutEvent, DeleteEvent))\n\n  override def receive: Receive = {\n    case ContainerCreation(msgs, memory, invocationNamespace) =>\n      createContainer(msgs, memory, invocationNamespace)\n\n    case ContainerDeletion(invocationNamespace, fqn, revision, whiskActionMetaData) =>\n      getInvokersWithOldContainer(invocationNamespace, fqn, revision)\n        .map { invokers =>\n          val msg = ContainerDeletionMessage(\n            TransactionId.containerDeletion,\n            invocationNamespace,\n            fqn,\n            revision,\n            whiskActionMetaData)\n          invokers.foreach(sendDeletionContainerToInvoker(messagingProducer, _, msg))\n        }\n\n    case rescheduling: ReschedulingCreationJob =>\n      val msg = rescheduling.toCreationMessage(schedulerInstanceId, rescheduling.retry + 1)\n      createContainer(\n        List(msg),\n        rescheduling.actionMetaData.limits.memory.megabytes.MB,\n        rescheduling.invocationNamespace)\n\n    case WatchEndpointInserted(watchKey, key, _, true) =>\n      watchKey match {\n        case `warmKey` => warmedContainers += key\n        case `invokerKey` =>\n          val invoker = InvokerKeys.getInstanceId(key)\n          warmedInvokers.getOrElseUpdate(invoker.instance, {\n            warmUpInvoker(invoker)\n            invoker.toString\n          })\n      }\n\n    case WatchEndpointRemoved(watchKey, key, _, true) =>\n      watchKey match {\n        case `warmKey` => warmedContainers -= key\n        case `invokerKey` =>\n          val invoker = InvokerKeys.getInstanceId(key)\n          warmedInvokers.remove(invoker.instance)\n      }\n\n    case FailedCreationJob(cid, _, _, _, _, _) =>\n      inProgressWarmedContainers.remove(cid.asString)\n\n    case SuccessfulCreationJob(cid, _, _, _) =>\n      inProgressWarmedContainers.remove(cid.asString)\n\n    case GracefulShutdown =>\n      watcherService ! UnwatchEndpoint(warmKey, isPrefix = true, watcherName)\n      watcherService ! UnwatchEndpoint(invokerKey, isPrefix = true, watcherName)\n      creationJobManager ! GracefulShutdown\n\n    case _ =>\n  }\n\n  private def createContainer(msgs: List[ContainerCreationMessage], memory: ByteSize, invocationNamespace: String)(\n    implicit logging: Logging): Unit = {\n    logging.info(this, s\"received ${msgs.size} creation message [${msgs.head.invocationNamespace}:${msgs.head.action}]\")\n    ContainerManager\n      .getAvailableInvokers(etcdClient, memory, invocationNamespace)\n      .recover({\n        case t: Throwable =>\n          logging.error(this, s\"Unable to get available invokers: ${t.getMessage}.\")\n          List.empty[InvokerHealth]\n      })\n      .foreach { invokers =>\n        if (invokers.isEmpty) {\n          logging.error(this, \"there is no available invoker to schedule.\")\n          msgs.foreach(ContainerManager.sendState(_, NoAvailableInvokersError, NoAvailableInvokersError))\n        } else {\n          val (coldCreations, warmedCreations) =\n            ContainerManager.filterWarmedCreations(warmedContainers, inProgressWarmedContainers, invokers, msgs)\n\n          // handle warmed creation\n          val chosenInvokers: immutable.Seq[Option[(Int, ContainerCreationMessage)]] = warmedCreations.map {\n            warmedCreation =>\n              // update the in-progress map for warmed containers.\n              // even if it is done in the filterWarmedCreations method, it is still necessary to apply the change to the original map.\n              warmedCreation._3.foreach(inProgressWarmedContainers.update(warmedCreation._1.creationId.asString, _))\n\n              // send creation message to the target invoker.\n              warmedCreation._2 map { chosenInvoker =>\n                val msg = warmedCreation._1\n                creationJobManager ! RegisterCreationJob(msg)\n                sendCreationContainerToInvoker(messagingProducer, chosenInvoker, msg)\n                (chosenInvoker, msg)\n              }\n          }\n\n          // update the resource usage of invokers to apply changes from warmed creations.\n          val updatedInvokers = chosenInvokers.foldLeft(invokers) { (invokers, chosenInvoker) =>\n            chosenInvoker match {\n              case Some((chosenInvoker, msg)) =>\n                updateInvokerMemory(chosenInvoker, msg.whiskActionMetaData.limits.memory.megabytes, invokers)\n              case err =>\n                // this is not supposed to happen.\n                logging.error(this, s\"warmed creation is scheduled but no invoker is chosen: $err\")\n                invokers\n            }\n          }\n\n          // handle cold creations\n          if (coldCreations.nonEmpty) {\n            ContainerManager\n              .schedule(updatedInvokers, coldCreations.map(_._1), memory)\n              .map { pair =>\n                pair.invokerId match {\n                  // an invoker is assigned for the msg\n                  case Some(instanceId) =>\n                    creationJobManager ! RegisterCreationJob(pair.msg)\n                    sendCreationContainerToInvoker(messagingProducer, instanceId.instance, pair.msg)\n\n                  // if a chosen invoker does not exist, it means it failed to find a matching invoker for the msg.\n                  case _ =>\n                    pair.err.foreach(error => sendState(pair.msg, error, error))\n                }\n              }\n          }\n        }\n      }\n  }\n\n  private def getInvokersWithOldContainer(invocationNamespace: String,\n                                          fqn: FullyQualifiedEntityName,\n                                          currentRevision: DocRevision): Future[List[Int]] = {\n    val namespacePrefix = containerPrefix(ContainerKeys.namespacePrefix, invocationNamespace, fqn)\n    val warmedPrefix = containerPrefix(ContainerKeys.warmedPrefix, invocationNamespace, fqn)\n\n    for {\n      existing <- etcdClient\n        .getPrefix(namespacePrefix)\n        .map { res =>\n          res.getKvsList.asScala.map { kv =>\n            parseExistingContainerKey(namespacePrefix, kv.getKey)\n          }\n        }\n      warmed <- etcdClient\n        .getPrefix(warmedPrefix)\n        .map { res =>\n          res.getKvsList.asScala.map { kv =>\n            parseWarmedContainerKey(warmedPrefix, kv.getKey)\n          }\n        }\n    } yield {\n      (existing ++ warmed)\n        .dropWhile(k => k.revision > currentRevision) // remain latest revision\n        .groupBy(k => k.invokerId) // remove duplicated value\n        .map(_._2.head.invokerId)\n        .toList\n    }\n  }\n\n  /**\n   * existingKey format: {tag}/namespace/{invocationNamespace}/{namespace}/({pkg}/)/{name}/{revision}/invoker{id}/container/{containerId}\n   */\n  private def parseExistingContainerKey(prefix: String, existingKey: String): ContainerKeyMeta = {\n    val keys = existingKey.replace(prefix, \"\").split(\"/\")\n    val revision = DocRevision(keys(0))\n    val invokerId = keys(1).replace(\"invoker\", \"\").toInt\n    val containerId = keys(3)\n    ContainerKeyMeta(revision, invokerId, containerId)\n  }\n\n  /**\n   * warmedKey format: {tag}/warmed/{invocationNamespace}/{namespace}/({pkg}/)/{name}/{revision}/invoker/{id}/container/{containerId}\n   */\n  private def parseWarmedContainerKey(prefix: String, warmedKey: String): ContainerKeyMeta = {\n    val keys = warmedKey.replace(prefix, \"\").split(\"/\")\n    val revision = DocRevision(keys(0))\n    val invokerId = keys(2).toInt\n    val containerId = keys(4)\n    ContainerKeyMeta(revision, invokerId, containerId)\n  }\n\n  private def sendCreationContainerToInvoker(producer: MessageProducer,\n                                             invoker: Int,\n                                             msg: ContainerCreationMessage): Future[ResultMetadata] = {\n    implicit val transid: TransactionId = msg.transid\n\n    val topic = s\"${Scheduler.topicPrefix}invoker$invoker\"\n    val start = transid.started(this, LoggingMarkers.SCHEDULER_KAFKA, s\"posting to $topic\")\n\n    producer.send(topic, msg).andThen {\n      case Success(status) =>\n        transid.finished(\n          this,\n          start,\n          s\"posted creationId: ${msg.creationId} for ${msg.invocationNamespace}/${msg.action} to ${status.topic}[${status.partition}][${status.offset}]\",\n          logLevel = InfoLevel)\n      case Failure(_) =>\n        logging.error(this, s\"Failed to create container for ${msg.action}, error: error on posting to topic $topic\")\n        transid.failed(this, start, s\"error on posting to topic $topic\")\n    }\n  }\n\n  private def sendDeletionContainerToInvoker(producer: MessageProducer,\n                                             invoker: Int,\n                                             msg: ContainerDeletionMessage): Future[ResultMetadata] = {\n    implicit val transid: TransactionId = msg.transid\n\n    val topic = s\"${Scheduler.topicPrefix}invoker$invoker\"\n    val start = transid.started(this, LoggingMarkers.SCHEDULER_KAFKA, s\"posting to $topic\")\n\n    producer.send(topic, msg).andThen {\n      case Success(status) =>\n        transid.finished(\n          this,\n          start,\n          s\"posted deletion for ${msg.invocationNamespace}/${msg.action} to ${status.topic}[${status.partition}][${status.offset}]\",\n          logLevel = InfoLevel)\n      case Failure(_) =>\n        logging.error(this, s\"Failed to delete container for ${msg.action}, error: error on posting to topic $topic\")\n        transid.failed(this, start, s\"error on posting to topic $topic\")\n    }\n  }\n\n  private def warmUpInvoker(invoker: InvokerInstanceId): Unit = {\n    logging.info(this, s\"Warm up invoker $invoker\")\n    WarmUp.warmUpContainerCreationMessage(schedulerInstanceId).foreach {\n      sendCreationContainerToInvoker(messagingProducer, invoker.instance, _)\n    }\n  }\n\n  // warm up all invokers\n  private def warmUp() = {\n    // warm up exist invokers\n    ContainerManager.getAvailableInvokers(etcdClient, MemoryLimit.MIN_MEMORY).map { invokers =>\n      invokers.foreach { invoker =>\n        warmedInvokers.getOrElseUpdate(invoker.id.instance, {\n          warmUpInvoker(invoker.id)\n          invoker.id.toString\n        })\n      }\n    }\n\n  }\n\n  warmUp()\n}\n\nobject ContainerManager {\n  val fractionConfig: BlackboxFractionConfig =\n    loadConfigOrThrow[BlackboxFractionConfig](ConfigKeys.fraction)\n\n  private val managedFraction: Double = Math.max(0.0, Math.min(1.0, fractionConfig.managedFraction))\n  private val blackboxFraction: Double = Math.max(1.0 - managedFraction, Math.min(1.0, fractionConfig.blackboxFraction))\n\n  def props(jobManagerFactory: ActorRefFactory => ActorRef,\n            provider: MessagingProvider,\n            schedulerInstanceId: SchedulerInstanceId,\n            etcdClient: EtcdClient,\n            config: WhiskConfig,\n            watcherService: ActorRef)(implicit actorSystem: ActorSystem, logging: Logging): Props =\n    Props(new ContainerManager(jobManagerFactory, provider, schedulerInstanceId, etcdClient, config, watcherService))\n\n  /**\n   * The rng algorithm is responsible for the invoker distribution, and the better the distribution, the smaller the number of rescheduling.\n   *\n   */\n  def rng(mod: Int): Int = ThreadLocalRandom.current().nextInt(mod)\n\n  // Partition messages that can use warmed containers.\n  // return: (list of messages that cannot use warmed containers, list of messages that can take advantage of warmed containers)\n  protected[container] def filterWarmedCreations(warmedContainers: Set[String],\n                                                 inProgressWarmedContainers: TrieMap[String, String],\n                                                 invokers: List[InvokerHealth],\n                                                 msgs: List[ContainerCreationMessage])(\n    implicit logging: Logging): (List[(ContainerCreationMessage, Option[Int], Option[String])],\n                                 List[(ContainerCreationMessage, Option[Int], Option[String])]) = {\n    val warmedApplied = msgs.map { msg =>\n      val warmedPrefix =\n        containerPrefix(ContainerKeys.warmedPrefix, msg.invocationNamespace, msg.action, Some(msg.revision))\n      val container = warmedContainers\n        .filter(!inProgressWarmedContainers.values.toSeq.contains(_))\n        .find { container =>\n          if (container.startsWith(warmedPrefix)) {\n            logging.info(this, s\"Choose a warmed container $container\")\n\n            // this is required to exclude already chosen invokers\n            inProgressWarmedContainers.update(msg.creationId.asString, container)\n            true\n          } else\n            false\n        }\n\n      // chosenInvoker is supposed to have only one item\n      val chosenInvoker = container\n        .map(_.split(\"/\").takeRight(3).apply(0))\n        // filter warmed containers in disabled invokers\n        .filter(\n          invoker =>\n            invokers\n            // filterWarmedCreations method is supposed to receive healthy invokers only but this will make sure again only healthy invokers are used.\n              .filter(invoker => invoker.status.isUsable)\n              .map(_.id.instance)\n              .contains(invoker.toInt))\n\n      if (chosenInvoker.nonEmpty && container.nonEmpty) {\n        (msg, Some(chosenInvoker.get.toInt), Some(container.get))\n      } else\n        (msg, None, None)\n    }\n\n    warmedApplied.partition { item =>\n      if (item._2.nonEmpty) false\n      else true\n    }\n  }\n\n  protected[container] def updateInvokerMemory(invokerId: Int,\n                                               requiredMemory: Long,\n                                               invokers: List[InvokerHealth]): List[InvokerHealth] = {\n    // it must be compared to the instance unique id\n    val index = invokers.indexOf(invokers.filter(p => p.id.instance == invokerId).head)\n    val invoker = invokers(index)\n\n    // if the invoker has less than minimum memory, drop it from the list.\n    if (invoker.id.userMemory.toMB - requiredMemory < MemoryLimit.MIN_MEMORY.toMB) {\n      // drop the nth element\n      val split = invokers.splitAt(index)\n      val _ :: t1 = split._2\n      split._1 ::: t1\n    } else {\n      invokers.updated(\n        index,\n        invoker.copy(id = invoker.id.copy(userMemory = invoker.id.userMemory - requiredMemory.MB)))\n    }\n  }\n\n  protected[container] def updateInvokerMemory(invokerId: Option[InvokerInstanceId],\n                                               requiredMemory: Long,\n                                               invokers: List[InvokerHealth]): List[InvokerHealth] = {\n    invokerId match {\n      case Some(instanceId) =>\n        updateInvokerMemory(instanceId.instance, requiredMemory, invokers)\n\n      case None =>\n        // do nothing\n        invokers\n    }\n  }\n\n  /**\n   * Assign an invoker to a message\n   *\n   * Assumption\n   *  - The memory of each invoker is larger than minMemory.\n   *  - Messages that are not assigned an invoker are discarded.\n   *\n   * @param invokers Available invoker pool\n   * @param msgs Messages to which the invoker will be assigned\n   * @param minMemory Minimum memory for all invokers\n   * @return A pair of messages and assigned invokers\n   */\n  def schedule(invokers: List[InvokerHealth], msgs: List[ContainerCreationMessage], minMemory: ByteSize)(\n    implicit logging: Logging): List[ScheduledPair] = {\n    logging.info(this, s\"usable total invoker size: ${invokers.size}\")\n    val noTaggedInvokers = invokers.filter(_.id.tags.isEmpty)\n    val managed = Math.max(1, Math.ceil(noTaggedInvokers.size.toDouble * managedFraction).toInt)\n    val blackboxes = Math.max(1, Math.floor(noTaggedInvokers.size.toDouble * blackboxFraction).toInt)\n    val managedInvokers = noTaggedInvokers.take(managed)\n    val blackboxInvokers = noTaggedInvokers.takeRight(blackboxes)\n    logging.info(\n      this,\n      s\"${msgs.size} creation messages for ${msgs.head.invocationNamespace}/${msgs.head.action}, managedFraction:$managedFraction, blackboxFraction:$blackboxFraction, managed invoker size:$managed, blackboxes invoker size:$blackboxes\")\n    val list = msgs\n      .foldLeft((List.empty[ScheduledPair], invokers)) { (tuple, msg: ContainerCreationMessage) =>\n        val pairs = tuple._1\n        val candidates = tuple._2\n\n        val requiredResources =\n          msg.whiskActionMetaData.annotations\n            .getAs[Seq[String]](Annotations.InvokerResourcesAnnotationName)\n            .getOrElse(Seq.empty[String])\n        val resourcesStrictPolicy = msg.whiskActionMetaData.annotations\n          .getAs[Boolean](Annotations.InvokerResourcesStrictPolicyAnnotationName)\n          .getOrElse(true)\n        val isBlackboxInvocation = msg.whiskActionMetaData.toExecutableWhiskAction.exists(_.exec.pull)\n        if (requiredResources.isEmpty) {\n          // only choose managed invokers or blackbox invokers\n          val wantedInvokers = if (isBlackboxInvocation) {\n            logging.info(this, s\"[${msg.invocationNamespace}/${msg.action}] looking for blackbox invokers to schedule.\")\n            candidates\n              .filter(\n                c =>\n                  blackboxInvokers\n                    .map(b => b.id.instance)\n                    .contains(c.id.instance) && c.id.userMemory.toMB >= msg.whiskActionMetaData.limits.memory.megabytes)\n              .toSet\n          } else {\n            logging.info(this, s\"[${msg.invocationNamespace}/${msg.action}] looking for managed invokers to schedule.\")\n            candidates\n              .filter(\n                c =>\n                  managedInvokers\n                    .map(m => m.id.instance)\n                    .contains(c.id.instance) && c.id.userMemory.toMB >= msg.whiskActionMetaData.limits.memory.megabytes)\n              .toSet\n          }\n          val taggedInvokers = candidates.filter(_.id.tags.nonEmpty)\n\n          if (wantedInvokers.nonEmpty) {\n            val scheduledPair = chooseInvokerFromCandidates(wantedInvokers.toList, msg)\n            val updatedInvokers =\n              updateInvokerMemory(scheduledPair.invokerId, msg.whiskActionMetaData.limits.memory.megabytes, invokers)\n            (scheduledPair :: pairs, updatedInvokers)\n          } else if (taggedInvokers.nonEmpty) { // if not found from the wanted invokers, choose tagged invokers then\n            logging.info(\n              this,\n              s\"[${msg.invocationNamespace}/${msg.action}] since there is no available non-tagged invoker, choose one among tagged invokers.\")\n            val scheduledPair = chooseInvokerFromCandidates(taggedInvokers, msg)\n            val updatedInvokers =\n              updateInvokerMemory(scheduledPair.invokerId, msg.whiskActionMetaData.limits.memory.megabytes, invokers)\n            (scheduledPair :: pairs, updatedInvokers)\n          } else {\n            logging.error(\n              this,\n              s\"[${msg.invocationNamespace}/${msg.action}] there is no invoker available to schedule to schedule.\")\n            val scheduledPair =\n              ScheduledPair(msg, invokerId = None, Some(NoAvailableInvokersError))\n            (scheduledPair :: pairs, invokers)\n          }\n        } else {\n          logging.info(this, s\"[${msg.invocationNamespace}/${msg.action}] looking for tagged invokers to schedule.\")\n          val wantedInvokers = candidates.filter(health => requiredResources.toSet.subsetOf(health.id.tags.toSet))\n          if (wantedInvokers.nonEmpty) {\n            val scheduledPair = chooseInvokerFromCandidates(wantedInvokers, msg)\n            val updatedInvokers =\n              updateInvokerMemory(scheduledPair.invokerId, msg.whiskActionMetaData.limits.memory.megabytes, invokers)\n            (scheduledPair :: pairs, updatedInvokers)\n          } else if (resourcesStrictPolicy) {\n            logging.error(\n              this,\n              s\"[${msg.invocationNamespace}/${msg.action}] there is no available invoker with the resource: ${requiredResources}\")\n            val scheduledPair =\n              ScheduledPair(msg, invokerId = None, Some(NoAvailableResourceInvokersError))\n            (scheduledPair :: pairs, invokers)\n          } else {\n            logging.info(\n              this,\n              s\"[${msg.invocationNamespace}/${msg.action}] since there is no available invoker with the resource, choose any invokers without the resource.\")\n            val (noTaggedInvokers, taggedInvokers) = candidates.partition(_.id.tags.isEmpty)\n            if (noTaggedInvokers.nonEmpty) { // choose no tagged invokers first\n              val scheduledPair = chooseInvokerFromCandidates(noTaggedInvokers, msg)\n              val updatedInvokers =\n                updateInvokerMemory(scheduledPair.invokerId, msg.whiskActionMetaData.limits.memory.megabytes, invokers)\n              (scheduledPair :: pairs, updatedInvokers)\n            } else {\n              val leftInvokers =\n                taggedInvokers.filterNot(health => requiredResources.toSet.subsetOf(health.id.tags.toSet))\n              if (leftInvokers.nonEmpty) {\n                val scheduledPair = chooseInvokerFromCandidates(leftInvokers, msg)\n                val updatedInvokers =\n                  updateInvokerMemory(\n                    scheduledPair.invokerId,\n                    msg.whiskActionMetaData.limits.memory.megabytes,\n                    invokers)\n                (scheduledPair :: pairs, updatedInvokers)\n              } else {\n                logging.error(this, s\"[${msg.invocationNamespace}/${msg.action}] no available invoker is found\")\n                val scheduledPair =\n                  ScheduledPair(msg, invokerId = None, Some(NoAvailableInvokersError))\n                (scheduledPair :: pairs, invokers)\n              }\n            }\n          }\n        }\n      }\n      ._1 // pairs\n    list\n  }\n\n  @tailrec\n  protected[container] def chooseInvokerFromCandidates(candidates: List[InvokerHealth], msg: ContainerCreationMessage)(\n    implicit logging: Logging): ScheduledPair = {\n    val requiredMemory = msg.whiskActionMetaData.limits.memory\n    if (candidates.isEmpty) {\n      ScheduledPair(msg, invokerId = None, Some(NoAvailableInvokersError))\n    } else if (candidates.forall(p => p.id.userMemory.toMB < requiredMemory.megabytes)) {\n      ScheduledPair(msg, invokerId = None, Some(NoAvailableResourceInvokersError))\n    } else {\n      val idx = rng(mod = candidates.size)\n      val instance = candidates(idx)\n      if (instance.id.userMemory.toMB < requiredMemory.megabytes) {\n        val split = candidates.splitAt(idx)\n        val _ :: t1 = split._2\n        chooseInvokerFromCandidates(split._1 ::: t1, msg)\n      } else {\n        ScheduledPair(msg, invokerId = Some(instance.id))\n      }\n    }\n  }\n\n  private def sendState(msg: ContainerCreationMessage, err: ContainerCreationError, reason: String)(\n    implicit logging: Logging): Unit = {\n    val state = FailedCreationJob(msg.creationId, msg.invocationNamespace, msg.action, msg.revision, err, reason)\n    QueuePool.get(MemoryQueueKey(state.invocationNamespace, state.action.toDocId.asDocInfo(state.revision))) match {\n      case Some(memoryQueueValue) if memoryQueueValue.isLeader =>\n        memoryQueueValue.queue ! state\n      case _ =>\n        logging.error(this, s\"get a $state for a nonexistent memory queue or a follower\")\n    }\n  }\n\n  protected[scheduler] def getAvailableInvokers(etcd: EtcdClient, minMemory: ByteSize, invocationNamespace: String)(\n    implicit executor: ExecutionContext): Future[List[InvokerHealth]] = {\n    etcd\n      .getPrefix(InvokerKeys.prefix)\n      .map { res =>\n        res.getKvsList.asScala\n          .map { kv =>\n            InvokerResourceMessage\n              .parse(kv.getValue.toString(StandardCharsets.UTF_8))\n              .map { resourceMessage =>\n                val status = resourceMessage.status match {\n                  case Healthy.asString   => Healthy\n                  case Unhealthy.asString => Unhealthy\n                  case _                  => Offline\n                }\n\n                val temporalId = InvokerKeys.getInstanceId(kv.getKey.toString(StandardCharsets.UTF_8))\n                val invoker = temporalId.copy(\n                  userMemory = resourceMessage.freeMemory.MB,\n                  busyMemory = Some(resourceMessage.busyMemory.MB),\n                  tags = resourceMessage.tags,\n                  dedicatedNamespaces = resourceMessage.dedicatedNamespaces)\n\n                InvokerHealth(invoker, status)\n              }\n              .getOrElse(InvokerHealth(InvokerInstanceId(kv.getKey, userMemory = 0.MB), Offline))\n          }\n          .filter(i => i.status.isUsable)\n          .filter(_.id.userMemory >= minMemory)\n          .filter { invoker =>\n            invoker.id.dedicatedNamespaces.isEmpty || invoker.id.dedicatedNamespaces.contains(invocationNamespace)\n          }\n          .toList\n      }\n  }\n\n  protected[scheduler] def getAvailableInvokers(etcd: EtcdClient, minMemory: ByteSize)(\n    implicit executor: ExecutionContext): Future[List[InvokerHealth]] = {\n    etcd\n      .getPrefix(InvokerKeys.prefix)\n      .map { res =>\n        res.getKvsList.asScala\n          .map { kv =>\n            InvokerResourceMessage\n              .parse(kv.getValue.toString(StandardCharsets.UTF_8))\n              .map { resourceMessage =>\n                val status = resourceMessage.status match {\n                  case Healthy.asString   => Healthy\n                  case Unhealthy.asString => Unhealthy\n                  case _                  => Offline\n                }\n\n                val temporalId = InvokerKeys.getInstanceId(kv.getKey.toString(StandardCharsets.UTF_8))\n                val invoker = temporalId.copy(\n                  userMemory = resourceMessage.freeMemory.MB,\n                  busyMemory = Some(resourceMessage.busyMemory.MB),\n                  tags = resourceMessage.tags,\n                  dedicatedNamespaces = resourceMessage.dedicatedNamespaces)\n                InvokerHealth(invoker, status)\n              }\n              .getOrElse(InvokerHealth(InvokerInstanceId(kv.getKey, userMemory = 0.MB), Offline))\n          }\n          .filter(i => i.status.isUsable)\n          .filter(_.id.userMemory >= minMemory)\n          .toList\n      }\n  }\n\n}\n\ncase class NoCapacityException(msg: String) extends Exception(msg)\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/container/CreationJobManager.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.container\n\nimport java.nio.charset.StandardCharsets\nimport java.util.concurrent.TimeUnit\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, ActorSystem, Cancellable, Props}\nimport org.apache.openwhisk.common.{GracefulShutdown, Logging}\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.inProgressContainer\nimport org.apache.openwhisk.core.service.{RegisterData, UnregisterData}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.scheduler.message.{\n  CreationJobState,\n  FailedCreationJob,\n  FinishCreationJob,\n  RegisterCreationJob,\n  ReschedulingCreationJob,\n  SuccessfulCreationJob\n}\nimport org.apache.openwhisk.core.scheduler.queue.{MemoryQueueKey, QueuePool}\nimport pureconfig.loadConfigOrThrow\n\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\n\ncase object GetPoolStatus\n\ncase class JobEntry(action: FullyQualifiedEntityName, timer: Cancellable)\n\nclass CreationJobManager(feedFactory: (ActorRefFactory, String, String, Int, Array[Byte] => Future[Unit]) => ActorRef,\n                         schedulerInstanceId: SchedulerInstanceId,\n                         dataManagementService: ActorRef,\n                         baseTimeout: FiniteDuration,\n                         blackboxMultiple: Int)(implicit actorSystem: ActorSystem, logging: Logging)\n    extends Actor {\n  private implicit val ec: ExecutionContext = actorSystem.dispatcher\n  private val retryLimit = 5\n\n  /**\n   * Store a JobEntry in local to get an alarm for key timeout\n   * It does not matter whether the information stored in Local is redundant or null.\n   * When a new JobEntry is created, it is overwritten if it is duplicated.\n   * If there is no corresponding JobEntry at the time of deletion, nothing is done.\n   */\n  protected val creationJobPool = TrieMap[CreationId, JobEntry]()\n\n  override def receive: Receive = {\n    case RegisterCreationJob(\n        ContainerCreationMessage(_, invocationNamespace, action, revision, actionMetaData, _, _, _, _, creationId)) =>\n      val isBlackboxInvocation = actionMetaData.toExecutableWhiskAction.exists(a => a.exec.pull)\n      registerJob(invocationNamespace, action, revision, creationId, isBlackboxInvocation)\n\n    case FinishCreationJob(\n        ContainerCreationAckMessage(\n          tid,\n          creationId,\n          invocationNamespace,\n          action,\n          revision,\n          actionMetaData,\n          _,\n          schedulerHost,\n          rpcPort,\n          retryCount,\n          error,\n          reason)) =>\n      if (error.isEmpty) {\n        logging.info(this, s\"[$action] [$creationId] create container successfully\")\n        deleteJob(\n          invocationNamespace,\n          action,\n          revision,\n          creationId,\n          SuccessfulCreationJob(creationId, invocationNamespace, action, revision))\n\n      } else {\n        val cause = reason.getOrElse(\"unknown reason\")\n        // if exceed the retry limit or meet errors which we don't need to reschedule, make it a failure\n        if (retryCount >= retryLimit || !error.exists(ContainerCreationError.whiskErrors.contains)) {\n          logging.error(\n            this,\n            s\"[$action] [$creationId] Failed to create container $retryCount/$retryLimit times for $cause. Finished creation\")\n          // Delete from pool after all retries are failed\n          deleteJob(\n            invocationNamespace,\n            action,\n            revision,\n            creationId,\n            FailedCreationJob(creationId, invocationNamespace, action, revision, error.get, cause))\n        } else {\n          // Reschedule\n          logging.error(\n            this,\n            s\"[$action] [$creationId] Failed to create container $retryCount/$retryLimit times for $cause. Started rescheduling\")\n          // Add some exponential delay time interval during retry create container, because etcd put operation needs some time if data inconsistant happens\n          val retryDelayTime = (scala.math.pow(2, retryCount) * 100).milliseconds\n          actorSystem.scheduler.scheduleOnce(retryDelayTime) {\n            context.parent ! ReschedulingCreationJob(\n              tid,\n              creationId,\n              invocationNamespace,\n              action,\n              revision,\n              actionMetaData,\n              schedulerHost,\n              rpcPort,\n              retryCount)\n          }\n        }\n      }\n\n    case GracefulShutdown =>\n      ackFeed ! GracefulShutdown\n  }\n\n  private def registerJob(invocationNamespace: String,\n                          action: FullyQualifiedEntityName,\n                          revision: DocRevision,\n                          creationId: CreationId,\n                          isBlackboxInvocation: Boolean) = {\n    creationJobPool getOrElseUpdate (creationId, {\n      val key = inProgressContainer(invocationNamespace, action, revision, schedulerInstanceId, creationId)\n      dataManagementService ! RegisterData(key, \"\", failoverEnabled = false)\n      JobEntry(action, createTimer(invocationNamespace, action, revision, creationId, isBlackboxInvocation))\n    })\n  }\n\n  private def deleteJob(invocationNamespace: String,\n                        action: FullyQualifiedEntityName,\n                        revision: DocRevision,\n                        creationId: CreationId,\n                        state: CreationJobState) = {\n    val key = inProgressContainer(invocationNamespace, action, revision, schedulerInstanceId, creationId)\n\n    // If there is a JobEntry, delete it.\n    creationJobPool\n      .remove(creationId)\n      .map(entry => entry.timer.cancel())\n\n    // even if there is no entry because of timeout, we still need to send the state to the queue if the queue exists\n    sendState(state)\n\n    dataManagementService ! UnregisterData(key)\n    Future.successful({})\n  }\n\n  private def sendState(state: CreationJobState): Unit = {\n    context.parent ! state // send state to ContainerManager\n    QueuePool.get(MemoryQueueKey(state.invocationNamespace, state.action.toDocId.asDocInfo(state.revision))) match {\n      case Some(memoryQueueValue) if memoryQueueValue.isLeader =>\n        memoryQueueValue.queue ! state\n      case _ =>\n        logging.error(this, s\"get a $state for a nonexistent memory queue or a follower\")\n    }\n  }\n\n  protected def createTimer(invocationNamespace: String,\n                            action: FullyQualifiedEntityName,\n                            revision: DocRevision,\n                            creationId: CreationId,\n                            isBlackbox: Boolean): Cancellable = {\n    val timeout =\n      if (isBlackbox) FiniteDuration(baseTimeout.toSeconds * blackboxMultiple, TimeUnit.SECONDS) else baseTimeout\n    actorSystem.scheduler.scheduleOnce(timeout) {\n      logging.warn(\n        this,\n        s\"Failed to create a container for $action(blackbox: $isBlackbox), error: $creationId timed out after $timeout\")\n      creationJobPool\n        .remove(creationId)\n        .foreach(_ =>\n          sendState(FailedCreationJob(\n            creationId,\n            invocationNamespace,\n            action,\n            revision,\n            ContainerCreationError.TimeoutError,\n            s\"[$action] timeout waiting for the ack of $creationId after $timeout\")))\n      dataManagementService ! UnregisterData(\n        inProgressContainer(invocationNamespace, action, revision, schedulerInstanceId, creationId))\n    }\n  }\n\n  private val topicPrefix = loadConfigOrThrow[String](ConfigKeys.kafkaTopicsPrefix)\n  private val topic = s\"${topicPrefix}creationAck${schedulerInstanceId.asString}\"\n  private val maxActiveAcksPerPoll = 128\n  private val ackFeed = feedFactory(actorSystem, \"creationAck\", topic, maxActiveAcksPerPoll, processAcknowledgement)\n\n  def processAcknowledgement(bytes: Array[Byte]): Future[Unit] = {\n    Future(ContainerCreationAckMessage.parse(new String(bytes, StandardCharsets.UTF_8)))\n      .flatMap(Future.fromTry)\n      .flatMap { msg =>\n        // forward msg to job manager\n        self ! FinishCreationJob(msg)\n        ackFeed ! MessageFeed.Processed\n        Future.successful(())\n      }\n      .recoverWith {\n        case t =>\n          // Iff everything above failed, we have a terminal error at hand. Either the message failed\n          // to deserialize, or something threw an error where it is not expected to throw.\n          ackFeed ! MessageFeed.Processed\n          logging.error(this, s\"terminal failure while processing container creation ack message: $t\")\n          Future.successful(())\n      }\n  }\n}\n\nobject CreationJobManager {\n  private val baseTimeout = loadConfigOrThrow[FiniteDuration](ConfigKeys.schedulerInProgressJobRetention)\n  private val blackboxMultiple = loadConfigOrThrow[Int](ConfigKeys.schedulerBlackboxMultiple)\n\n  def props(feedFactory: (ActorRefFactory, String, String, Int, Array[Byte] => Future[Unit]) => ActorRef,\n            schedulerInstanceId: SchedulerInstanceId,\n            dataManagementService: ActorRef)(implicit actorSystem: ActorSystem, logging: Logging) =\n    Props(\n      new CreationJobManager(feedFactory, schedulerInstanceId, dataManagementService, baseTimeout, blackboxMultiple))\n}\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/grpc/ActivationServiceImpl.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.grpc\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.util.Timeout\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.WarmUp\nimport org.apache.openwhisk.core.connector.{ActivationMessage, Message}\nimport org.apache.openwhisk.core.entity.{DocRevision, FullyQualifiedEntityName}\nimport org.apache.openwhisk.core.scheduler.queue._\nimport org.apache.openwhisk.grpc.{ActivationService, FetchRequest, FetchResponse, RescheduleRequest, RescheduleResponse}\nimport spray.json._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContextExecutor, Future}\nimport scala.util.Try\n\nclass ActivationServiceImpl()(implicit actorSystem: ActorSystem, logging: Logging) extends ActivationService {\n  implicit val requestTimeout: Timeout = Timeout(5.seconds)\n  implicit val ec: ExecutionContextExecutor = actorSystem.dispatcher\n\n  override def rescheduleActivation(request: RescheduleRequest): Future[RescheduleResponse] = {\n    logging.info(this, s\"Try to reschedule activation ${request.invocationNamespace} ${request.fqn} ${request.rev}\")\n    Future(for {\n      fqn <- FullyQualifiedEntityName.parse(request.fqn)\n      rev <- DocRevision.parse(request.rev)\n      msg <- ActivationMessage.parse(request.activationMessage)\n    } yield (fqn, rev, msg)).flatMap(Future.fromTry) flatMap { res =>\n      {\n        val key = res._1.toDocId.asDocInfo(res._2)\n        QueuePool.get(MemoryQueueKey(request.invocationNamespace, key)) match {\n          case Some(queueValue) =>\n            // enqueue activation message to reschedule\n            logging.info(\n              this,\n              s\"Enqueue activation message to reschedule ${request.invocationNamespace} ${request.fqn} ${request.rev}\")\n            queueValue.queue ? res._3\n            Future.successful(RescheduleResponse(true))\n          case None =>\n            logging.error(this, s\"Queue not found for ${request.invocationNamespace} ${request.fqn} ${request.rev}\")\n            Future.successful(RescheduleResponse())\n        }\n      }\n    }\n  }\n\n  override def fetchActivation(request: FetchRequest): Future[FetchResponse] = {\n    Future(for {\n      fqn <- FullyQualifiedEntityName.parse(request.fqn)\n      rev <- DocRevision.parse(request.rev)\n    } yield (fqn, rev)).flatMap(Future.fromTry) flatMap { res =>\n      val (fqn, rev) = res\n      if (!WarmUp.isWarmUpAction(fqn)) {\n        val key = fqn.toDocId.asDocInfo(rev)\n        QueuePool.get(MemoryQueueKey(request.invocationNamespace, key)) match {\n          case Some(queueValue) =>\n            implicit val transid = TransactionId.serdes.read(request.transactionId.parseJson)\n            if (!request.alive) logging.info(this, s\"the container(${request.containerId}) is not alive\")\n\n            (queueValue.queue ? GetActivation(\n              transid,\n              fqn,\n              request.containerId,\n              request.warmed,\n              request.lastDuration,\n              request.alive))\n              .mapTo[ActivationResponse]\n              .map { response =>\n                FetchResponse(response.serialize)\n              }\n              .recover {\n                case t: Throwable =>\n                  logging.error(\n                    this,\n                    s\"Failed to get message from QueueManager container: ${request.containerId}, fqn: ${request.fqn}, rev: ${request.rev}, alive: ${request.alive}, lastDuration: ${request.lastDuration}, error: ${t.getMessage}\")\n                  FetchResponse(ActivationResponse(Left(NoActivationMessage())).serialize)\n              }\n          case None =>\n            if (QueuePool.keys.exists { mkey =>\n                  mkey.invocationNamespace == request.invocationNamespace && mkey.docInfo.id == key.id\n                })\n              Future.successful(FetchResponse(ActivationResponse(Left(ActionMismatch())).serialize))\n            else\n              Future.successful(FetchResponse(ActivationResponse(Left(NoMemoryQueue())).serialize))\n        }\n      } else {\n        logging.info(\n          this,\n          s\"The ${request.fqn} action is an action used to connect a network level connection. So response no activation\")\n        Future.successful(FetchResponse(ActivationResponse(Left(NoActivationMessage())).serialize))\n      }\n    }\n  }\n}\n\nobject ActivationServiceImpl {\n\n  def apply()(implicit actorSystem: ActorSystem, logging: Logging) =\n    new ActivationServiceImpl()\n}\n\ncase class GetActivation(transactionId: TransactionId,\n                         action: FullyQualifiedEntityName,\n                         containerId: String,\n                         warmed: Boolean,\n                         lastDuration: Option[Long],\n                         alive: Boolean = true)\ncase class ActivationResponse(message: Either[MemoryQueueError, ActivationMessage]) extends Message {\n  override def serialize = ActivationResponse.serdes.write(this).compactPrint\n}\n\nobject ActivationResponse extends DefaultJsonProtocol {\n\n  private implicit val noMessageSerdes = NoActivationMessage.serdes\n  private implicit val noQueueSerdes = NoMemoryQueue.serdes\n  private implicit val mismatchSerdes = ActionMismatch.serdes\n  private implicit val messageSerdes = ActivationMessage.serdes\n  private implicit val memoryqueueuErrorSerdes = MemoryQueueErrorSerdes.memoryQueueErrorFormat\n\n  def parse(msg: String) = Try(serdes.read(msg.parseJson))\n\n  implicit def rootEitherFormat[A: RootJsonFormat, B: RootJsonFormat] =\n    new RootJsonFormat[Either[A, B]] {\n      val format = DefaultJsonProtocol.eitherFormat[A, B]\n\n      def write(either: Either[A, B]) = format.write(either)\n\n      def read(value: JsValue) = format.read(value)\n    }\n\n  type ActivationResponse = Either[MemoryQueueError, ActivationMessage]\n  implicit val serdes = jsonFormat(ActivationResponse.apply _, \"message\")\n\n}\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/message/ContainerMessage.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.message\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.connector.{\n  ContainerCreationAckMessage,\n  ContainerCreationError,\n  ContainerCreationMessage\n}\nimport org.apache.openwhisk.core.entity.{\n  ByteSize,\n  CreationId,\n  DocRevision,\n  FullyQualifiedEntityName,\n  SchedulerInstanceId,\n  WhiskActionMetaData\n}\n\ncase class ContainerKeyMeta(revision: DocRevision, invokerId: Int, containerId: String)\n\ncase class ContainerCreation(msgs: List[ContainerCreationMessage], memory: ByteSize, invocationNamespace: String)\ncase class ContainerDeletion(invocationNamespace: String,\n                             action: FullyQualifiedEntityName,\n                             revision: DocRevision,\n                             whiskActionMetaData: WhiskActionMetaData)\n\nsealed trait CreationJob\ncase class RegisterCreationJob(msg: ContainerCreationMessage) extends CreationJob\ncase class FinishCreationJob(ack: ContainerCreationAckMessage) extends CreationJob\ncase class ReschedulingCreationJob(tid: TransactionId,\n                                   creationId: CreationId,\n                                   invocationNamespace: String,\n                                   action: FullyQualifiedEntityName,\n                                   revision: DocRevision,\n                                   actionMetaData: WhiskActionMetaData,\n                                   schedulerHost: String,\n                                   rpcPort: Int,\n                                   retry: Int)\n    extends CreationJob {\n\n  def toCreationMessage(sid: SchedulerInstanceId, retryCount: Int): ContainerCreationMessage =\n    ContainerCreationMessage(\n      tid,\n      invocationNamespace,\n      action,\n      revision,\n      actionMetaData,\n      sid,\n      schedulerHost,\n      rpcPort,\n      retryCount,\n      creationId)\n}\n\nabstract class CreationJobState(val creationId: CreationId,\n                                val invocationNamespace: String,\n                                val action: FullyQualifiedEntityName,\n                                val revision: DocRevision)\ncase class FailedCreationJob(override val creationId: CreationId,\n                             override val invocationNamespace: String,\n                             override val action: FullyQualifiedEntityName,\n                             override val revision: DocRevision,\n                             error: ContainerCreationError,\n                             message: String)\n    extends CreationJobState(creationId, invocationNamespace, action, revision)\ncase class SuccessfulCreationJob(override val creationId: CreationId,\n                                 override val invocationNamespace: String,\n                                 override val action: FullyQualifiedEntityName,\n                                 override val revision: DocRevision)\n    extends CreationJobState(creationId, invocationNamespace, action, revision)\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/queue/ContainerCounter.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorSystem, Props}\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.etcd.EtcdClient\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys\nimport org.apache.openwhisk.core.service.{DeleteEvent, PutEvent, UnwatchEndpoint, WatchEndpoint, WatchEndpointOperation}\n\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass ContainerCounter(invocationNamespace: String, etcdClient: EtcdClient, watcherService: ActorRef)(\n  implicit val actorSystem: ActorSystem,\n  ec: ExecutionContext,\n  logging: Logging) {\n  private[queue] var existingContainerNumByNamespace: Int = 0\n  private[queue] var inProgressContainerNumByNamespace: Int = 0\n  private[queue] val references = new AtomicInteger(0)\n  private val watcherName = s\"container-counter-$invocationNamespace\"\n\n  private val inProgressContainerPrefixKeyByNamespace =\n    ContainerKeys.inProgressContainerPrefixByNamespace(invocationNamespace)\n  private val existingContainerPrefixKeyByNamespace =\n    ContainerKeys.existingContainersPrefixByNamespace(invocationNamespace)\n\n  private val watchedKeys = Seq(inProgressContainerPrefixKeyByNamespace, existingContainerPrefixKeyByNamespace)\n\n  private val watcher =\n    actorSystem.actorOf(Props(new Actor {\n      private var countingKeys = Set.empty[String]\n      private var waitingForCountKeys = Set.empty[String]\n\n      override def receive: Receive = {\n        case operation: WatchEndpointOperation if operation.isPrefix =>\n          if (countingKeys\n                .contains(operation.watchKey))\n            waitingForCountKeys += operation.watchKey\n          else {\n            countingKeys += operation.watchKey\n            refreshContainerCount(operation.watchKey)\n          }\n\n        case ReadyToGetCount(key) =>\n          if (waitingForCountKeys.contains(key)) {\n            waitingForCountKeys -= key\n            refreshContainerCount(key)\n          } else\n            countingKeys -= key\n      }\n    }))\n\n  private def refreshContainerCount(key: String): Future[Unit] = {\n    etcdClient\n      .getCount(key)\n      .map { count =>\n        key match {\n          case `inProgressContainerPrefixKeyByNamespace` => inProgressContainerNumByNamespace = count.toInt\n          case `existingContainerPrefixKeyByNamespace`   => existingContainerNumByNamespace = count.toInt\n        }\n        watcher ! ReadyToGetCount(key)\n      }\n      .recover {\n        case t: Throwable =>\n          logging.error(\n            this,\n            s\"failed to get the number of existing containers for ${invocationNamespace} due to ${t}.\")\n          watcher ! ReadyToGetCount(key)\n      }\n  }\n\n  def increaseReference(): ContainerCounter = {\n    if (references.incrementAndGet() == 1) {\n      watchedKeys.foreach { key =>\n        watcherService.tell(WatchEndpoint(key, \"\", true, watcherName, Set(PutEvent, DeleteEvent)), watcher)\n      }\n\n    }\n    this\n  }\n\n  def close(): Unit = {\n    if (references.decrementAndGet() == 0) {\n      watchedKeys.foreach { key =>\n        watcherService ! UnwatchEndpoint(key, true, watcherName)\n      }\n      NamespaceContainerCount.instances.remove(invocationNamespace)\n    }\n  }\n}\n\nobject NamespaceContainerCount {\n  private[queue] val instances = TrieMap[String, ContainerCounter]()\n  def apply(namespace: String, etcdClient: EtcdClient, watcherService: ActorRef)(implicit actorSystem: ActorSystem,\n                                                                                 ec: ExecutionContext,\n                                                                                 logging: Logging): ContainerCounter = {\n    instances\n      .getOrElseUpdate(namespace, new ContainerCounter(namespace, etcdClient, watcherService))\n      .increaseReference()\n  }\n}\n\ncase class ReadyToGetCount(key: String)\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/queue/ElasticSearchDurationChecker.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.openwhisk.core.scheduler.queue\n\nimport org.apache.pekko.actor.ActorSystem\nimport com.sksamuel.elastic4s.http.ElasticDsl._\nimport com.sksamuel.elastic4s.http.{ElasticClient, ElasticProperties, NoOpRequestConfigCallback}\nimport com.sksamuel.elastic4s.searches.queries.Query\nimport com.sksamuel.elastic4s.{ElasticDate, ElasticDateMath, Seconds}\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.WhiskActionMetaData\nimport org.apache.openwhisk.spi.Spi\nimport pureconfig.loadConfigOrThrow\nimport spray.json.{JsArray, JsNumber, JsValue, RootJsonFormat, deserializationError, _}\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration.FiniteDuration\nimport scala.language.implicitConversions\nimport scala.util.{Failure, Try}\nimport pureconfig.generic.auto._\n\ntrait DurationChecker {\n  def checkAverageDuration(invocationNamespace: String, actionMetaData: WhiskActionMetaData)(\n    callback: DurationCheckResult => DurationCheckResult): Future[DurationCheckResult]\n}\n\ncase class DurationCheckResult(averageDuration: Option[Double], hitCount: Long, took: Long)\n\nobject ElasticSearchDurationChecker {\n  val FilterAggregationName = \"filterAggregation\"\n  val AverageAggregationName = \"averageAggregation\"\n\n  implicit val serde = new ElasticSearchDurationCheckResultFormat()\n\n  def getFromDate(timeWindow: FiniteDuration): ElasticDateMath =\n    ElasticDate.now minus (timeWindow.toSeconds.toInt, Seconds)\n}\n\nclass ElasticSearchDurationChecker(private val client: ElasticClient, val timeWindow: FiniteDuration)(\n  implicit val actorSystem: ActorSystem,\n  implicit val logging: Logging)\n    extends DurationChecker {\n  import ElasticSearchDurationChecker._\n  import org.apache.openwhisk.core.database.elasticsearch.ElasticSearchActivationStore.generateIndex\n\n  implicit val ec = actorSystem.getDispatcher\n\n  override def checkAverageDuration(invocationNamespace: String, actionMetaData: WhiskActionMetaData)(\n    callback: DurationCheckResult => DurationCheckResult): Future[DurationCheckResult] = {\n    val index = generateIndex(invocationNamespace)\n    val fqn = actionMetaData.fullyQualifiedName(false)\n    val fromDate = getFromDate(timeWindow)\n\n    logging.info(this, s\"check average duration for $fqn in $index for last $timeWindow\")\n\n    actionMetaData.binding match {\n      case Some(binding) =>\n        val boolQueryResult = List(\n          matchQuery(\"annotations.binding\", s\"$binding\"),\n          matchQuery(\"name\", actionMetaData.name),\n          rangeQuery(\"@timestamp\").gte(fromDate))\n\n        executeQuery(boolQueryResult, callback, index)\n\n      case None =>\n        val queryResult = List(matchQuery(\"path.keyword\", fqn.toString), rangeQuery(\"@timestamp\").gte(fromDate))\n\n        executeQuery(queryResult, callback, index)\n    }\n  }\n\n  private def executeQuery(boolQueryResult: List[Query],\n                           callback: DurationCheckResult => DurationCheckResult,\n                           index: String) = {\n    client\n      .execute {\n        (search(index) query {\n          boolQuery must {\n            boolQueryResult\n          }\n        } aggregations\n          avgAgg(AverageAggregationName, \"duration\")).size(0)\n      }\n      .map { res =>\n        logging.debug(this, s\"ElasticSearch query results: $res\")\n        Try(serde.read(res.body.getOrElse(\"\").parseJson))\n      }\n      .flatMap(Future.fromTry)\n      .map(callback(_))\n      .andThen {\n        case Failure(t) =>\n          logging.error(this, s\"failed to check the average duration: ${t}\")\n      }\n  }\n}\n\nobject ElasticSearchDurationCheckerProvider extends DurationCheckerProvider {\n  import org.apache.openwhisk.core.database.elasticsearch.ElasticSearchActivationStore._\n\n  override def instance(actorSystem: ActorSystem, log: Logging): ElasticSearchDurationChecker = {\n    implicit val as: ActorSystem = actorSystem\n    implicit val logging: Logging = log\n\n    val elasticClient =\n      ElasticClient(\n        ElasticProperties(s\"${elasticSearchConfig.protocol}://${elasticSearchConfig.hosts}\"),\n        NoOpRequestConfigCallback,\n        httpClientCallback)\n\n    new ElasticSearchDurationChecker(elasticClient, durationCheckerConfig.timeWindow)\n  }\n}\n\ntrait DurationCheckerProvider extends Spi {\n\n  val durationCheckerConfig: DurationCheckerConfig =\n    loadConfigOrThrow[DurationCheckerConfig](ConfigKeys.durationChecker)\n\n  def instance(actorSystem: ActorSystem, logging: Logging): DurationChecker\n}\n\nclass ElasticSearchDurationCheckResultFormat extends RootJsonFormat[DurationCheckResult] {\n  import ElasticSearchDurationChecker._\n  import spray.json.DefaultJsonProtocol._\n\n  /**\n   * Expected sample data\n      {\n          \"_shards\": {\n              \"failed\": 0,\n              \"skipped\": 0,\n              \"successful\": 5,\n              \"total\": 5\n          },\n          \"aggregations\": {\n              \"agg\": {\n                  \"value\": 14\n              }\n          },\n          \"hits\": {\n              \"hits\": [],\n              \"max_score\": 0,\n              \"total\": 3\n          },\n          \"timed_out\": false,\n          \"took\": 0\n      }\n   */\n  /**\n   * Expected sample data\n      {\n          \"_shards\": {\n              \"failed\": 0,\n              \"skipped\": 0,\n              \"successful\": 5,\n              \"total\": 5\n          },\n          \"aggregations\": {\n              \"pathAggregation\": {\n                  \"avg_duration\": {\n                      \"value\": 13\n                  },\n                  \"doc_count\": 3\n              }\n          },\n          \"hits\": {\n              \"hits\": [],\n              \"max_score\": 0,\n              \"total\": 6\n          },\n          \"timed_out\": false,\n          \"took\": 0\n      }\n   */\n  implicit def read(json: JsValue) = {\n    val jsObject = json.asJsObject\n\n    jsObject.getFields(\"aggregations\", \"took\", \"hits\") match {\n      case Seq(aggregations, took, hits) =>\n        val hitCount = hits.asJsObject.getFields(\"total\").headOption\n        val filterAggregations = aggregations.asJsObject.getFields(FilterAggregationName)\n        val averageAggregations = aggregations.asJsObject.getFields(AverageAggregationName)\n\n        (filterAggregations, averageAggregations, hitCount) match {\n          case (filterAggregations, _, Some(count)) if filterAggregations.nonEmpty =>\n            val averageDuration =\n              filterAggregations.headOption.flatMap(\n                _.asJsObject\n                  .getFields(AverageAggregationName)\n                  .headOption\n                  .flatMap(_.asJsObject.getFields(\"value\").headOption))\n\n            averageDuration match {\n              case Some(JsNull) =>\n                DurationCheckResult(None, count.convertTo[Long], took.convertTo[Long])\n\n              case Some(duration) =>\n                DurationCheckResult(Some(duration.convertTo[Double]), count.convertTo[Long], took.convertTo[Long])\n\n              case _ => deserializationError(\"Cannot deserialize ProductItem: invalid input. Raw input: \")\n            }\n\n          case (_, averageAggregations, Some(count)) if averageAggregations.nonEmpty =>\n            val averageDuration = averageAggregations.headOption.flatMap(_.asJsObject.getFields(\"value\").headOption)\n\n            averageDuration match {\n              case Some(JsNull) =>\n                DurationCheckResult(None, count.convertTo[Long], took.convertTo[Long])\n\n              case Some(duration) =>\n                DurationCheckResult(Some(duration.convertTo[Double]), count.convertTo[Long], took.convertTo[Long])\n\n              case t => deserializationError(s\"Cannot deserialize DurationCheckResult: invalid input. Raw input: $t\")\n            }\n\n          case t => deserializationError(s\"Cannot deserialize DurationCheckResult: invalid input. Raw input: $t\")\n        }\n\n      case other => deserializationError(s\"Cannot deserialize DurationCheckResult: invalid input. Raw input: $other\")\n    }\n\n  }\n\n  // This method would not be used.\n  override def write(obj: DurationCheckResult): JsValue = {\n    JsArray(JsNumber(obj.averageDuration.get), JsNumber(obj.hitCount), JsNumber(obj.took))\n  }\n}\n\ncase class DurationCheckerConfig(timeWindow: FiniteDuration)\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/queue/MemoryQueue.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue\n\nimport org.apache.pekko.actor.Status.{Failure => FailureMessage}\nimport org.apache.pekko.actor.{ActorRef, ActorSystem, Cancellable, FSM, Props, Stash}\nimport org.apache.pekko.util.Timeout\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.common.time.{Clock, SystemClock}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.ack.ActiveAck\nimport org.apache.openwhisk.core.connector.ContainerCreationError.{InvalidActionLimitError, ZeroNamespaceLimit}\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.containerpool.Interval\nimport org.apache.openwhisk.core.database.{NoDocumentException, UserContext}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.etcd.EtcdClient\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.containerPrefix\nimport org.apache.openwhisk.core.etcd.EtcdKV.{ContainerKeys, QueueKeys, ThrottlingKeys}\nimport org.apache.openwhisk.core.scheduler.grpc.{GetActivation, ActivationResponse => GetActivationResponse}\nimport org.apache.openwhisk.core.scheduler.message.{\n  ContainerCreation,\n  ContainerDeletion,\n  FailedCreationJob,\n  SuccessfulCreationJob\n}\nimport org.apache.openwhisk.core.scheduler.{SchedulerEndpoints, SchedulingConfig}\nimport org.apache.openwhisk.core.service._\nimport org.apache.openwhisk.http.Messages.{namespaceLimitUnderZero, tooManyConcurrentRequests}\nimport pureconfig.loadConfigOrThrow\nimport spray.json._\nimport pureconfig.generic.auto._\n\nimport scala.collection.JavaConverters._\nimport java.time.{Duration, Instant}\nimport java.util.concurrent.atomic.{AtomicInteger, AtomicLong}\nimport scala.annotation.tailrec\nimport scala.collection.immutable.Queue\nimport scala.collection.mutable\nimport scala.concurrent.duration._\nimport scala.concurrent.{duration, ExecutionContextExecutor, Future, Promise}\nimport scala.util.{Failure, Success}\n\n// States\nsealed trait MemoryQueueState\ncase object Uninitialized extends MemoryQueueState\ncase object Running extends MemoryQueueState\ncase object Idle extends MemoryQueueState\ncase object Flushing extends MemoryQueueState\ncase object Removing extends MemoryQueueState\ncase object Removed extends MemoryQueueState\ncase object ActionThrottled extends MemoryQueueState\ncase object NamespaceThrottled extends MemoryQueueState\n\n// Data\nsealed abstract class MemoryQueueData()\ncase class NoData() extends MemoryQueueData() {\n  override def toString = \"NoData\"\n}\ncase class NoActors() extends MemoryQueueData() {\n  override def toString = \"NoActors\"\n}\ncase class RunningData(schedulerActor: ActorRef, droppingActor: ActorRef) extends MemoryQueueData() {\n  override def toString = \"RunningData\"\n}\ncase class ThrottledData(schedulerActor: ActorRef, droppingActor: ActorRef) extends MemoryQueueData() {\n  override def toString = \"ThrottledData\"\n}\ncase class FlushingData(schedulerActor: ActorRef,\n                        droppingActor: ActorRef,\n                        error: ContainerCreationError,\n                        reason: String,\n                        activeDuringFlush: Boolean = false)\n    extends MemoryQueueData() {\n  override def toString = s\"ThrottledData(error: $error, reason: $reason, activeDuringFlush: $activeDuringFlush)\"\n}\ncase class RemovingData(schedulerActor: ActorRef, droppingActor: ActorRef, outdated: Boolean)\n    extends MemoryQueueData() {\n  override def toString = s\"RemovingData(outdated: $outdated)\"\n}\n\n// Events sent by the actor\ncase class QueueRemoved(invocationNamespace: String, action: DocInfo, leaderKey: Option[String])\ncase class QueueReactivated(invocationNamespace: String, action: FullyQualifiedEntityName, docInfo: DocInfo)\ncase class CancelPoll(promise: Promise[Either[MemoryQueueError, ActivationMessage]])\ncase object QueueRemovedCompleted\n\n// Events received by the actor\ncase object Start\ncase object VersionUpdated\ncase object StopSchedulingAsOutdated\n\nsealed trait RequiredAction\ncase object Skip extends RequiredAction\ncase object AddInitialContainer extends RequiredAction\ncase object AddContainer extends RequiredAction\ncase class EnableNamespaceThrottling(dropMsg: Boolean) extends RequiredAction\ncase object DisableNamespaceThrottling extends RequiredAction\ncase object EnableActionThrottling extends RequiredAction\ncase object DisableActionThrottling extends RequiredAction\ncase object Pausing extends RequiredAction\ncase class DecisionResults(required: RequiredAction, num: Int)\n\ncase class TimeSeriesActivationEntry(timestamp: Instant, msg: ActivationMessage)\n\nclass MemoryQueue(private val etcdClient: EtcdClient,\n                  private val durationChecker: DurationChecker,\n                  private val action: FullyQualifiedEntityName,\n                  messagingProducer: MessageProducer,\n                  schedulingConfig: SchedulingConfig,\n                  invocationNamespace: String,\n                  revision: DocRevision,\n                  endpoints: SchedulerEndpoints,\n                  actionMetaData: WhiskActionMetaData,\n                  dataManagementService: ActorRef,\n                  watcherService: ActorRef,\n                  containerManager: ActorRef,\n                  decisionMaker: ActorRef,\n                  schedulerId: SchedulerInstanceId,\n                  ack: ActiveAck,\n                  store: (TransactionId, WhiskActivation, UserContext) => Future[Any],\n                  getUserLimit: String => Future[Int],\n                  checkToDropStaleActivation: (Clock,\n                                               Queue[TimeSeriesActivationEntry],\n                                               Long,\n                                               AtomicLong,\n                                               String,\n                                               WhiskActionMetaData,\n                                               MemoryQueueState,\n                                               ActorRef) => Unit,\n                  queueConfig: QueueConfig)(implicit logging: Logging, clock: Clock)\n    extends FSM[MemoryQueueState, MemoryQueueData]\n    with Stash {\n\n  private implicit val ec: ExecutionContextExecutor = context.dispatcher\n  private implicit val actorSystem: ActorSystem = context.system\n  private implicit val timeout = Timeout(5.seconds)\n  private implicit val order: Ordering[BufferedRequest] = Ordering.by(_.containerId)\n\n  private val StaleDuration = Duration.ofMillis(schedulingConfig.staleThreshold.toMillis)\n  private val unversionedAction = action.copy(version = None)\n  private val leaderKey = QueueKeys.queue(invocationNamespace, unversionedAction, leader = true)\n  private val inProgressContainerPrefixKey =\n    containerPrefix(ContainerKeys.inProgressPrefix, invocationNamespace, action, Some(revision))\n  private val existingContainerPrefixKey =\n    containerPrefix(ContainerKeys.namespacePrefix, invocationNamespace, action, Some(revision))\n  private val namespaceThrottlingKey = ThrottlingKeys.namespace(EntityName(invocationNamespace))\n  private val actionThrottlingKey = ThrottlingKeys.action(invocationNamespace, unversionedAction)\n  private val pollTimeOut = 1.seconds\n  private var requestBuffer = mutable.PriorityQueue.empty[BufferedRequest]\n  private val memory = actionMetaData.limits.memory.megabytes.MB\n  private val queueRemovedMsg = QueueRemoved(invocationNamespace, action.toDocId.asDocInfo(revision), Some(leaderKey))\n  private val staleQueueRemovedMsg = QueueRemoved(invocationNamespace, action.toDocId.asDocInfo(revision), None)\n  private val actionRetentionTimeout = MemoryQueue.getRetentionTimeout(actionMetaData, queueConfig)\n\n  private[queue] var containers = java.util.concurrent.ConcurrentHashMap.newKeySet[String]().asScala\n  private[queue] var creationIds = java.util.concurrent.ConcurrentHashMap.newKeySet[String]().asScala\n\n  private[queue] var queue = Queue.empty[TimeSeriesActivationEntry]\n  private[queue] var in = new AtomicInteger(0)\n  private[queue] val lastActivationPulledTime = new AtomicLong(Instant.now.toEpochMilli)\n  private[queue] val namespaceContainerCount = NamespaceContainerCount(invocationNamespace, etcdClient, watcherService)\n  private[queue] var averageDuration: Option[Double] = None\n  private[queue] var averageDurationBuffer = AverageRingBuffer(queueConfig.durationBufferSize)\n  private[queue] var limit: Option[Int] = None\n  private[queue] var initialized = false\n\n  private val logScheduler: Cancellable = context.system.scheduler.scheduleWithFixedDelay(0.seconds, 1.seconds) { () =>\n    MetricEmitter.emitGaugeMetric(\n      LoggingMarkers\n        .SCHEDULER_QUEUE_WAITING_ACTIVATION(invocationNamespace, action.asString, action.toStringWithoutVersion),\n      queue.size)\n\n    MetricEmitter.emitGaugeMetric(\n      LoggingMarkers.SCHEDULER_NAMESPACE_CONTAINER(invocationNamespace),\n      namespaceContainerCount.existingContainerNumByNamespace)\n    MetricEmitter.emitGaugeMetric(\n      LoggingMarkers.SCHEDULER_NAMESPACE_INPROGRESS_CONTAINER(invocationNamespace),\n      namespaceContainerCount.inProgressContainerNumByNamespace)\n\n    MetricEmitter.emitGaugeMetric(\n      LoggingMarkers.SCHEDULER_ACTION_CONTAINER(invocationNamespace, action.asString, action.toStringWithoutVersion),\n      containers.size)\n    MetricEmitter.emitGaugeMetric(\n      LoggingMarkers\n        .SCHEDULER_ACTION_INPROGRESS_CONTAINER(invocationNamespace, action.asString, action.toStringWithoutVersion),\n      creationIds.size)\n  }\n\n  getAverageDuration()\n\n  private val watcherName = s\"memory-queue-$action-$revision\"\n  // watch existing containers for action and namespace\n  private val watchedKeys = Seq(inProgressContainerPrefixKey, existingContainerPrefixKey)\n\n  watchedKeys.foreach { key =>\n    watcherService ! WatchEndpoint(key, \"\", isPrefix = true, watcherName, Set(PutEvent, DeleteEvent))\n  }\n\n  startWith(Uninitialized, NoData())\n\n  when(Uninitialized) {\n    case Event(Start, _) =>\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName] a new queue is created, retentionTimeout: $actionRetentionTimeout, kind: ${actionMetaData.exec.kind}.\")\n      val (schedulerActor, droppingActor) = startMonitoring()\n      initializeThrottling()\n\n      watcherService ! WatchEndpoint(leaderKey, endpoints.serialize, isPrefix = false, watcherName, Set(DeleteEvent))\n\n      goto(Running) using RunningData(schedulerActor, droppingActor)\n\n    // this is the case that the action version is updated, so no data needs to be stored\n    case Event(VersionUpdated, _) =>\n      val (schedulerActor, droppingActor) = startMonitoring()\n\n      goto(Running) using RunningData(schedulerActor, droppingActor)\n\n    // other messages should not be handled in this state.\n    case _ =>\n      stash()\n      stay\n  }\n\n  when(Running, stateTimeout = queueConfig.idleGrace) {\n    case Event(EnableNamespaceThrottling(dropMsg), data: RunningData) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] Enable namespace throttling.\")\n      enableNamespaceThrottling()\n\n      if (dropMsg)\n        completeAllActivations(tooManyConcurrentRequests, isWhiskError = queueConfig.failThrottleAsWhiskError)\n      goto(NamespaceThrottled) using ThrottledData(data.schedulerActor, data.droppingActor)\n\n    case Event(StateTimeout, data: RunningData) =>\n      if (queue.isEmpty && (containers.size + creationIds.size) <= 0) {\n        logging.info(\n          this,\n          s\"[$invocationNamespace:$action:$stateName] No activations coming in ${queueConfig.idleGrace}\")\n        actorSystem.stop(data.schedulerActor)\n        actorSystem.stop(data.droppingActor)\n\n        goto(Idle) using NoActors()\n      } else {\n        logging.info(\n          this,\n          s\"[$invocationNamespace:$action:$stateName] The queue is timed out but there are still ${queue.size} activation messages or (running: ${containers.size} -> ${containers.toString}, in-progress: ${creationIds.size} -> ${creationIds.toString}) containers\")\n        stay\n      }\n\n    case Event(FailedCreationJob(creationId, _, _, _, error, message), RunningData(schedulerActor, droppingActor)) =>\n      creationIds -= creationId.asString\n      // when there is no container, it moves to the Flushing state as no activations can be invoked\n      if (containers.size <= 0) {\n        val isWhiskError = ContainerCreationError.whiskErrors.contains(error)\n        if (!isWhiskError) {\n          completeAllActivations(message, isWhiskError)\n        }\n        logging.error(\n          this,\n          s\"[$invocationNamespace:$action:$stateName] Failed to create an initial container due to ${if (isWhiskError) \"whiskError\"\n          else \"developerError\"}, reason: $message.\")\n\n        goto(Flushing) using FlushingData(schedulerActor, droppingActor, error, message)\n      } else\n        // if there are already some containers running, activations can be handled anyway.\n        stay\n  }\n\n  // there is no timeout for this state as when there is no further message, it would move to the Running state again.\n  when(NamespaceThrottled) {\n    case Event(msg: ActivationMessage, _: ThrottledData) =>\n      if (containers.size + creationIds.size == 0) {\n        completeErrorActivation(msg, tooManyConcurrentRequests, isWhiskError = queueConfig.failThrottleAsWhiskError)\n      } else {\n        handleActivationMessage(msg)\n      }\n      stay\n\n    case Event(DisableNamespaceThrottling, data: ThrottledData) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] Disable namespace throttling.\")\n      disableNamespaceThrottling()\n      goto(Running) using RunningData(data.schedulerActor, data.schedulerActor)\n  }\n\n  // there is no timeout for this state as when there is no further message, it would move to the Running state again.\n  when(ActionThrottled) {\n    // since there are already too many activation messages, it drops the new messages\n    case Event(msg: ActivationMessage, ThrottledData(_, _)) =>\n      completeErrorActivation(msg, tooManyConcurrentRequests, isWhiskError = queueConfig.failThrottleAsWhiskError)\n      stay\n  }\n\n  when(Idle, stateTimeout = queueConfig.stopGrace) {\n    case Event(msg: ActivationMessage, _: NoActors) =>\n      val (schedulerActor, droppingActor) = startMonitoring()\n      handleActivationMessage(msg)\n      goto(Running) using RunningData(schedulerActor, droppingActor)\n\n    case Event(request: GetActivation, _) if request.action == action =>\n      sender ! GetActivationResponse(Left(NoActivationMessage()))\n      stay\n\n    case Event(StateTimeout, _: NoActors) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] The queue is timed out, stop the queue.\")\n      cleanUpDataAndGotoRemoved()\n\n    case Event(GracefulShutdown, _: NoActors) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] Received GracefulShutdown, stop the queue.\")\n      cleanUpDataAndGotoRemoved()\n\n    case Event(StopSchedulingAsOutdated, _: NoActors) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] stop further scheduling.\")\n\n      cleanUpWatcher()\n\n      // let QueueManager know this queue is no longer in charge.\n      context.parent ! staleQueueRemovedMsg\n\n      // since the queue is outdated and there is no activation, delete all old containers.\n      containerManager ! ContainerDeletion(invocationNamespace, action, revision, actionMetaData)\n\n      goto(Removed) using NoData()\n  }\n\n  when(Flushing) {\n    // an initial container is successfully created.\n    case Event(SuccessfulCreationJob(creationId, _, _, _), FlushingData(schedulerActor, droppingActor, _, _, _)) =>\n      creationIds -= creationId.asString\n\n      goto(Running) using RunningData(schedulerActor, droppingActor)\n\n    case Event(FailedCreationJob(creationId, _, _, _, e, message), data: FlushingData) =>\n      e match {\n        // delete queue when container creation fails with action limit invalid error\n        case InvalidActionLimitError =>\n          logging.info(\n            this,\n            s\"[$invocationNamespace:$action:$stateName][$creationId] Clean up because the action limit is invalid\")\n          completeAllActivations(data.reason, ContainerCreationError.whiskErrors.contains(data.error))\n          cleanUpActorsAndGotoRemoved(data)\n\n        case _ =>\n          // log the failed information\n          creationIds -= creationId.asString\n          logging.info(\n            this,\n            s\"[$invocationNamespace:$action:$stateName][$creationId] Failed to create a container due to $message\")\n\n          // keep updating the reason\n          stay using data.copy(error = e, reason = message)\n      }\n\n    // since there is no container, activations cannot be handled.\n    case Event(msg: ActivationMessage, data: FlushingData) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] got a new activation message ${msg.activationId}\")(\n        msg.transid)\n      val whiskError = isWhiskError(data.error)\n      if (whiskError)\n        queue = queue.enqueue(TimeSeriesActivationEntry(clock.now(), msg))\n      else\n        completeErrorActivation(msg, data.reason, whiskError)\n      stay() using data.copy(activeDuringFlush = true)\n\n    // Since SchedulingDecisionMaker keep sending a message to create a container, this state is not automatically timed out.\n    // Instead, StateTimeout message will be sent by a timer.\n    case Event(StateTimeout | DropOld, data: FlushingData) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] Received StateTimeout, drop stale messages.\")\n      queue = MemoryQueue.dropOld(\n        clock,\n        queue,\n        Duration.ofMillis(actionRetentionTimeout),\n        data.reason,\n        completeErrorActivation)\n      if (data.activeDuringFlush || queue.nonEmpty)\n        stay using data.copy(activeDuringFlush = false)\n      else\n        cleanUpActorsAndGotoRemoved(data)\n\n    case Event(GracefulShutdown, data: FlushingData) =>\n      completeAllActivations(data.reason, isWhiskError(data.error))\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] Received GracefulShutdown, stop the queue.\")\n      cleanUpActorsAndGotoRemoved(data)\n\n    case Event(StopSchedulingAsOutdated, data: FlushingData) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] stop further scheduling.\")\n      completeAllActivations(data.reason, isWhiskError(data.error))\n      // let QueueManager know this queue is no longer in charge.\n      context.parent ! staleQueueRemovedMsg\n      cleanUpActors(data)\n      cleanUpData()\n\n      goto(Removed) using NoData()\n  }\n\n  // in case there is any activation in the queue, it waits until all of them are handled.\n  when(Removing, stateTimeout = queueConfig.gracefulShutdownTimeout) {\n    // When there is no message in the queue, SchedulingDecisionMaker would stop sending any message\n    // So the queue can be timed out on every gracefulShutdownTimeout\n    case Event(QueueRemovedCompleted | StateTimeout, data: RemovingData) =>\n      cleanUpActorsAndGotoRemovedIfPossible(data)\n\n    case Event(GracefulShutdown, data: RemovingData) =>\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName] The queue received GracefulShutdown trying to stop the queue.\")\n      cleanUpActorsAndGotoRemovedIfPossible(data)\n\n    case Event(StopSchedulingAsOutdated, data: RemovingData) =>\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName] The queue received StopSchedulingAsOutdated trying to stop the queue.\")\n\n      handleStaleActivationsWhenActionUpdated(context.parent)\n\n      cleanUpActorsAndGotoRemovedIfPossible(data.copy(outdated = true))\n  }\n\n  when(Removed, stateTimeout = queueConfig.gracefulShutdownTimeout) {\n    // since this Queue will be terminated, rescheduling the msg\n    case Event(msg: ActivationMessage, _: NoData) =>\n      context.parent ! msg\n      stay()\n\n    // this queue is going to stop so let client connect to a new queue\n    case Event(request: GetActivation, _: NoData) if request.action == action =>\n      implicit val tid = request.transactionId\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName] Get activation request ${request.containerId}, let client connect to a new queue.\")\n      forwardAllActivations(context.parent)\n      sender ! GetActivationResponse(Left(NoMemoryQueue()))\n\n      stay\n\n    // actors and data are already wiped\n    case Event(QueueRemovedCompleted, _: NoData) =>\n      logging.info(this, \"stop fsm\")\n      stop()\n\n    // This is not supposed to happen. This will ensure the queue does not run forever.\n    // This can happen when QueueManager could not respond with QueueRemovedCompleted for some reason.\n    // Note: Activation messages can be received while waiting to timeout which resets the state timeout.\n    // Therefore the state timeout must be set externally on transition to prevent the queue stuck waiting\n    // to remove forever cycling activations between the manager and this fsm.\n    case Event(StateTimeout, _: NoData) =>\n      context.parent ! queueRemovedMsg\n\n      stop()\n\n    // This queue is going to stop, do nothing\n    case Event(msg @ (StopSchedulingAsOutdated | GracefulShutdown), _: NoData) =>\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName] The queue received $msg but do nothing as it is going to stop.\")\n      stay\n  }\n\n  whenUnhandled {\n    // The queue endpoint is removed, trying to restore it.\n    case Event(WatchEndpointRemoved(_, `leaderKey`, value, false), data) =>\n      data match {\n        case RemovingData(_, _, _) =>\n          logging.info(\n            this,\n            s\"[$invocationNamespace:$action:$stateName] This queue is shutdown by `/disable` api, do nothing here.\")\n        case _ =>\n          dataManagementService ! RegisterInitialData(leaderKey, value, failoverEnabled = false, Some(self)) // the watcher is already setup\n      }\n      stay\n\n    // we don't care the storage results for namespaceThrottlingKey\n    case Event(InitialDataStorageResults(`namespaceThrottlingKey`, _), _) =>\n      stay\n\n    // The queue endpoint is restored\n    case Event(InitialDataStorageResults(`leaderKey`, Right(_)), _) =>\n      stay\n\n    // this can be a case that there is another queue already running.\n    // it can happen if a node is segregated by the temporal network rupture and the queue endpoint is removed.\n    case Event(InitialDataStorageResults(`leaderKey`, Left(_)), data) =>\n      logging.warn(this, s\"[$invocationNamespace:$action:$stateName] the queue is superseded by a new queue.\")\n      // let QueueManager know this queue is no longer in charge.\n      context.parent ! queueRemovedMsg\n\n      // forward all activations to the parent queue manager.\n      // parent queue manager is supposed to removed the reference of this queue and forward messages to a new queue\n      forwardAllActivations(context.parent)\n\n      // only clean up actors because etcd data is already being used by another queue\n      cleanUpActors(data)\n\n      goto(Removed) using NoData()\n\n    case Event(WatchEndpointRemoved(watchKey, key, _, true), _) =>\n      watchKey match {\n        case `inProgressContainerPrefixKey` =>\n          creationIds -= key.split(\"/\").last\n        case `existingContainerPrefixKey` =>\n          val containerId = key.split(\"/\").last\n          removeDeletedContainerFromRequestBuffer(containerId)\n          containers -= containerId\n        case _ =>\n      }\n      stay\n\n    case Event(WatchEndpointInserted(watchKey, key, _, true), _) =>\n      watchKey match {\n        case `inProgressContainerPrefixKey` =>\n          creationIds += key.split(\"/\").last\n        case `existingContainerPrefixKey` =>\n          containers += key.split(\"/\").last\n        case _ =>\n      }\n      stay\n\n    // common case for Running, NamespaceThrottled, ActionThrottled\n    case Event(SuccessfulCreationJob(creationId, _, _, _), _) =>\n      creationIds -= creationId.asString\n      stay()\n\n    // for other cases\n    case Event(FailedCreationJob(creationId, invocationNamespace, action, revision, _, message), _) =>\n      creationIds -= creationId.asString\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName][$creationId] Got failed creation job with revision $revision and error $message.\")\n      stay()\n\n    // common case for Running, NamespaceThrottled, ActionThrottled, Removing\n    case Event(cancel: CancelPoll, _) =>\n      cancel.promise.trySuccess(Left(NoActivationMessage()))\n\n      stay\n\n    // common case for Running, NamespaceThrottled, ActionThrottled, Removing\n    case Event(msg: ActivationMessage, _) =>\n      handleActivationMessage(msg)\n\n    // common case for Running, NamespaceThrottled, ActionThrottled, Removing\n    case Event(request: GetActivation, _) if request.action == action =>\n      implicit val tid = request.transactionId\n      if (request.alive) {\n        containers += request.containerId\n        handleActivationRequest(request)\n      } else {\n        logging.info(this, s\"Remove containerId because ${request.containerId} is not alive\")\n        removeDeletedContainerFromRequestBuffer(request.containerId)\n        sender ! GetActivationResponse(Left(NoActivationMessage()))\n        containers -= request.containerId\n        stay\n      }\n\n    // common case for Running, NamespaceThrottled, ActionThrottled, Removing\n    case Event(request: GetActivation, _) if request.action != action =>\n      implicit val tid = request.transactionId\n      logging.warn(this, s\"[$invocationNamespace:$action:$stateName] version mismatch ${request.action}\")\n      sender ! GetActivationResponse(Left(ActionMismatch()))\n\n      stay\n\n    case Event(DropOld, _) =>\n      if (queue.nonEmpty && Duration\n            .between(queue.head.timestamp, clock.now())\n            .compareTo(Duration.ofMillis(actionRetentionTimeout)) >= 0) {\n        logging.error(\n          this,\n          s\"[$invocationNamespace:$action:$stateName] Drop some stale activations for $revision, existing container is ${containers.size}, inProgress container is ${creationIds.size}, state data: $stateData, in is $in, current: ${queue.size}.\")\n        logging.error(\n          this,\n          s\"[$invocationNamespace:$action:$stateName] the head stale message: ${queue.head.msg.activationId}\")\n      }\n      queue = MemoryQueue.dropOld(\n        clock,\n        queue,\n        Duration.ofMillis(actionRetentionTimeout),\n        s\"Activation processing is not initiated for $actionRetentionTimeout ms\",\n        completeErrorActivation)\n\n      stay\n\n    // common case for all statuses\n    case Event(GetState, _) =>\n      sender ! StatusData(\n        invocationNamespace,\n        action.asString,\n        queue.toList.map(_.msg.activationId),\n        stateName.toString,\n        stateData.toString)\n      stay\n\n    // Common case for all cases\n    case Event(GracefulShutdown, data) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] Gracefully shutdown the memory queue.\")\n      // delete relative data, e.g leaderKey, namespaceThrottlingKey, actionThrottlingKey\n      cleanUpData()\n\n      goto(Removing) using getRemovingData(data, outdated = false)\n\n    // the version is updated. it's a shared case for all states\n    case Event(StopSchedulingAsOutdated, data) =>\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] stop further scheduling.\")\n      // let QueueManager know this queue is no longer in charge.\n      context.parent ! staleQueueRemovedMsg\n\n      handleStaleActivationsWhenActionUpdated(context.parent)\n\n      goto(Removing) using getRemovingData(data, outdated = true)\n\n    case Event(t: FailureMessage, _) =>\n      logging.error(this, s\"[$invocationNamespace:$action:$stateName] got an unexpected failure message: $t\")\n\n      stay\n\n    case Event(msg: DecisionResults, _) =>\n      val DecisionResults(result, num) = msg\n      result match {\n        case AddInitialContainer if num > 0 =>\n          initialized = true\n          val msgs = generateContainerCreationMessages(num)\n          containerManager ! ContainerCreation(msgs, memory, invocationNamespace)\n\n        case AddContainer if num > 0 =>\n          val msgs = generateContainerCreationMessages(num)\n          containerManager ! ContainerCreation(msgs, memory, invocationNamespace)\n\n        case enable: EnableNamespaceThrottling =>\n          if (num > 0) {\n            val msgs = generateContainerCreationMessages(num)\n            containerManager ! ContainerCreation(msgs, memory, invocationNamespace)\n          }\n          self ! enable\n\n        case DisableNamespaceThrottling =>\n          if (num > 0) {\n            val msgs = generateContainerCreationMessages(num)\n            containerManager ! ContainerCreation(msgs, memory, invocationNamespace)\n          }\n          self ! DisableNamespaceThrottling\n\n        case Pausing =>\n          logging.warn(\n            this,\n            s\"[$invocationNamespace:$action:$stateName] The limit value is less than 0. No activation can be handled so the queue becomes the Flushing state.\")\n          self ! FailedCreationJob(\n            CreationId.void,\n            invocationNamespace,\n            action,\n            revision,\n            ZeroNamespaceLimit,\n            namespaceLimitUnderZero)\n      }\n      stay\n\n    // this should not happen\n    case otherMsg =>\n      logging.warn(this, s\"[$invocationNamespace:$action:$stateName] received unexpected message: $otherMsg\")\n\n      stay\n  }\n\n  onTransition {\n    case Uninitialized -> _ => unstashAll()\n    case _ -> Flushing      => startTimerWithFixedDelay(\"StopQueue\", StateTimeout, queueConfig.flushGrace)\n    case Flushing -> _      => cancelTimer(\"StopQueue\")\n    case _ -> Removed       => startTimerWithFixedDelay(\"RemovedQueue\", StateTimeout, queueConfig.stopGrace)\n    case Removed -> _       => cancelTimer(\"RemovedQueue\") //Removed state is a sink so shouldn't be able to hit this.\n  }\n\n  onTermination {\n    case _ =>\n      // logscheduler must be canceled when FSM is terminated\n      logScheduler.cancel()\n\n      // the lifecycle of DecisionMaker conforms to the one of MemoryQueue\n      actorSystem.stop(decisionMaker)\n  }\n\n  initialize()\n\n  private def cleanUpDataAndGotoRemoved() = {\n    cleanUpWatcher()\n    cleanUpData()\n\n    context.parent ! queueRemovedMsg\n\n    goto(Removed) using NoData()\n  }\n\n  private def cleanUpActorsAndGotoRemoved(data: FlushingData) = {\n    cleanUpActors(data)\n    cleanUpData()\n\n    context.parent ! queueRemovedMsg\n\n    goto(Removed) using NoData()\n  }\n\n  private def cleanUpActorsAndGotoRemovedIfPossible(data: RemovingData) = {\n    requestBuffer = requestBuffer.filter(!_.promise.isCompleted)\n    if (queue.isEmpty && requestBuffer.isEmpty) {\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] No activation exist. Shutdown the queue.\")\n      // it can be safely called multiple times as it's idempotent\n      cleanUpActors(data)\n\n      // if the queue is outdated, remove old containers.\n      if (data.outdated) {\n        // let the container manager know this version of containers are outdated.\n        containerManager ! ContainerDeletion(invocationNamespace, action, revision, actionMetaData)\n      }\n\n      goto(Removed) using NoData()\n    } else {\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName] Queue is going to stop but there are still ${queue.size} activations and ${requestBuffer.size} request buffered.\")\n      stay // waiting for next timeout\n    }\n  }\n\n  private def getRemovingData(data: MemoryQueueData, outdated: Boolean): MemoryQueueData = {\n    data match {\n      case RunningData(schedulerActor, droppingActor) =>\n        RemovingData(schedulerActor, droppingActor, outdated)\n      case ThrottledData(schedulerActor, droppingActor) =>\n        RemovingData(schedulerActor, droppingActor, outdated)\n      case FlushingData(schedulerActor, droppingActor, _, _, _) =>\n        RemovingData(schedulerActor, droppingActor, outdated)\n      case data: RemovingData =>\n        data.copy(outdated = outdated)\n      case _ =>\n        NoData()\n    }\n  }\n\n  private def cleanUpWatcher(): Unit = {\n    watchedKeys.foreach { key =>\n      watcherService ! UnwatchEndpoint(key, isPrefix = true, watcherName)\n    }\n    watcherService ! UnwatchEndpoint(leaderKey, isPrefix = false, watcherName)\n    namespaceContainerCount.close()\n  }\n\n  private def cleanUpActors(data: MemoryQueueData): Unit = {\n    cleanUpWatcher()\n\n    data match {\n      case RunningData(schedulerActor, droppingActor) =>\n        actorSystem.stop(schedulerActor)\n        actorSystem.stop(droppingActor)\n\n      case ThrottledData(schedulerActor, droppingActor) =>\n        actorSystem.stop(schedulerActor)\n        actorSystem.stop(droppingActor)\n\n      case FlushingData(schedulerActor, droppingActor, _, _, _) =>\n        actorSystem.stop(schedulerActor)\n        actorSystem.stop(droppingActor)\n\n      case RemovingData(schedulerActor, droppingActor, _) =>\n        actorSystem.stop(schedulerActor)\n        actorSystem.stop(droppingActor)\n\n      case _ => // do nothing\n    }\n  }\n\n  private def cleanUpData(): Unit = {\n    dataManagementService ! UnregisterData(leaderKey)\n    dataManagementService ! UnregisterData(namespaceThrottlingKey)\n    dataManagementService ! UnregisterData(actionThrottlingKey)\n  }\n\n  private def initializeThrottling() = {\n    dataManagementService ! RegisterInitialData(namespaceThrottlingKey, false.toString, failoverEnabled = false)\n    dataManagementService ! RegisterData(actionThrottlingKey, false.toString, failoverEnabled = false)\n  }\n\n  private def tryEnableActionThrottling() = {\n    if (queue.size >= queueConfig.maxRetentionSize && stateName != ActionThrottled) {\n      logging.info(this, s\"[$invocationNamespace:$action:$stateName] Enable action throttling.\")\n      dataManagementService ! RegisterData(actionThrottlingKey, true.toString, failoverEnabled = false)\n\n      stateData match {\n        case RunningData(schedulerActor, droppingActor) =>\n          goto(ActionThrottled) using ThrottledData(schedulerActor, droppingActor)\n        case _ =>\n          stay\n      }\n    } else {\n      stay\n    }\n  }\n\n  private def tryDisableActionThrottling()(implicit tid: TransactionId) = {\n    (stateName, stateData) match {\n      case (ActionThrottled, ThrottledData(schedulerActor, droppingActor))\n          if queue.size <= queueConfig.maxRetentionSize * queueConfig.throttlingFraction =>\n        logging.info(this, s\"[$invocationNamespace:$action:$stateName] Disable action throttling.\")\n        dataManagementService ! RegisterData(actionThrottlingKey, false.toString, failoverEnabled = false)\n\n        // at this point, namespace throttling might be enabled,\n        // then the state will be changed to NamespaceThrottled automatically at the next tick\n        goto(Running) using RunningData(schedulerActor, droppingActor)\n      case _ => stay\n    }\n  }\n\n  private def disableNamespaceThrottling() = {\n    dataManagementService ! RegisterData(namespaceThrottlingKey, false.toString, failoverEnabled = false)\n  }\n\n  private def enableNamespaceThrottling() = {\n    dataManagementService ! RegisterData(namespaceThrottlingKey, true.toString, failoverEnabled = false)\n  }\n\n  private def completeErrorActivation(activation: ActivationMessage,\n                                      message: String,\n                                      isWhiskError: Boolean): Future[Any] = {\n    logging.error(\n      this,\n      s\"[$invocationNamespace:$action:$stateName] complete activation ${activation.activationId} with error $message\")(\n      activation.transid)\n\n    val totalTimeInScheduler = Interval(activation.transid.meta.start, Instant.now()).duration\n    MetricEmitter.emitHistogramMetric(\n      LoggingMarkers.SCHEDULER_WAIT_TIME(action.asString, action.toStringWithoutVersion),\n      totalTimeInScheduler.toMillis)\n\n    val activationResponse =\n      if (isWhiskError)\n        generateFallbackActivation(activation, ActivationResponse.whiskError(message))\n      else\n        generateFallbackActivation(activation, ActivationResponse.developerError(message))\n\n    // TODO change scheduler instance id\n    val instance = InvokerInstanceId(0, userMemory = 0.MB)\n\n    val ackMsg = if (activation.blocking) {\n      CombinedCompletionAndResultMessage(activation.transid, activationResponse, instance)\n    } else {\n      CompletionMessage(activation.transid, activationResponse, instance)\n    }\n\n    if (message == tooManyConcurrentRequests) {\n      val metric = Metric(\"ConcurrentRateLimit\", 1)\n      UserEvents.send(\n        messagingProducer,\n        EventMessage(\n          schedulerId.toString,\n          metric,\n          activation.user.subject,\n          invocationNamespace,\n          activation.user.namespace.uuid,\n          metric.typeName))\n    }\n\n    ack(\n      activation.transid,\n      activationResponse,\n      activation.blocking,\n      activation.rootControllerIndex,\n      activation.user.namespace.uuid,\n      ackMsg)\n      .andThen {\n        case Failure(t) =>\n          logging.error(this, s\"[$invocationNamespace:$action:$stateName] failed to send ack due to $t\")\n      }\n    store(activation.transid, activationResponse, UserContext(activation.user))\n      .andThen {\n        case Failure(t) =>\n          logging.error(this, s\"[$invocationNamespace:$action:$stateName] failed to store activation due to $t\")\n      }\n  }\n\n  private def forwardAllActivations(queueManager: ActorRef): Unit = {\n    while (queue.nonEmpty) {\n      val (TimeSeriesActivationEntry(_, msg), newQueue) = queue.dequeue\n      queue = newQueue\n      logging.info(this, s\"Forward msg ${msg.activationId} to the queue manager\")(msg.transid)\n      queueManager ! msg\n    }\n  }\n\n  private def handleStaleActivationsWhenActionUpdated(queueManager: ActorRef): Unit = {\n    if (queue.size > 0) {\n      // if doesn't exist old container to pull old memoryQueue's activation, send the old activations to queueManager\n      if (containers.size == 0) {\n        logging.warn(\n          this,\n          s\"[$invocationNamespace:$action:$stateName] does not exist old version container to fetch the old version activation\")\n        forwardAllActivations(queueManager)\n      } else {\n        logging.info(\n          this,\n          s\"[$invocationNamespace:$action:$stateName] old version activation would be fetched by old version container\")\n      }\n    }\n  }\n\n  private def completeAllActivations(reason: String, isWhiskError: Boolean): Unit = {\n    while (queue.nonEmpty) {\n      val (TimeSeriesActivationEntry(_, msg), newQueue) = queue.dequeue\n      queue = newQueue\n      completeErrorActivation(msg, reason, isWhiskError)\n    }\n  }\n\n  // since there is no initial delay, it will try to create a container at initialization time\n  // these schedulers will run forever and stop when the memory queue stops\n  private def startMonitoring(): (ActorRef, ActorRef) = {\n    val droppingScheduler = Scheduler.scheduleWaitAtLeast(schedulingConfig.dropInterval) { () =>\n      checkToDropStaleActivation(\n        clock,\n        queue,\n        actionRetentionTimeout,\n        lastActivationPulledTime,\n        invocationNamespace,\n        actionMetaData,\n        stateName,\n        self)\n      Future.successful(())\n    }\n\n    val monitoringScheduler = Scheduler.scheduleWaitAtLeast(schedulingConfig.checkInterval) { () =>\n      // the average duration is updated every checkInterval\n      if (averageDurationBuffer.nonEmpty) {\n        averageDuration = Some(averageDurationBuffer.average)\n      }\n\n      getUserLimit(invocationNamespace).andThen {\n        case Success(namespaceLimit) =>\n          // extra safeguard to use namespace limit if action limit exceeds due to namespace limit being lowered\n          // by operator after action is deployed\n          val actionLimit = actionMetaData.limits.instances\n            .map(limit =>\n              if (limit.maxConcurrentInstances > namespaceLimit) InstanceConcurrencyLimit(namespaceLimit) else limit)\n            .getOrElse(InstanceConcurrencyLimit(namespaceLimit))\n            .maxConcurrentInstances\n          decisionMaker ! QueueSnapshot(\n            initialized,\n            in,\n            queue.size,\n            containers.size,\n            creationIds.size,\n            getStaleActivationNum(0, queue),\n            namespaceContainerCount.existingContainerNumByNamespace,\n            namespaceContainerCount.inProgressContainerNumByNamespace,\n            averageDuration,\n            namespaceLimit,\n            actionLimit,\n            actionMetaData.limits.concurrency.maxConcurrent,\n            stateName,\n            self)\n        case Failure(_: NoDocumentException) =>\n          // no limit available for the namespace\n          self ! StopSchedulingAsOutdated\n      }\n    }\n    (monitoringScheduler, droppingScheduler)\n  }\n\n  private def getAverageDuration() = {\n    // check the duration only once\n    actorSystem.scheduler.scheduleOnce(duration.Duration.Zero) {\n      durationChecker.checkAverageDuration(invocationNamespace, actionMetaData) { durationCheckResult =>\n        if (durationCheckResult.hitCount > 0) {\n          averageDuration = durationCheckResult.averageDuration\n        }\n        durationCheckResult\n      }\n    }\n  }\n\n  @tailrec\n  private def getStaleActivationNum(count: Int, queue: Queue[TimeSeriesActivationEntry]): Int = {\n    if (queue.isEmpty || Duration\n          .between(queue.head.timestamp, clock.now())\n          .compareTo(StaleDuration) < 0) count\n    else\n      getStaleActivationNum(count + 1, queue.tail)\n  }\n\n  private def generateContainerCreationMessages(num: Int) = {\n    (1 to num).map { _ =>\n      val msg = ContainerCreationMessage(\n        TransactionId.containerCreation,\n        invocationNamespace,\n        action,\n        revision,\n        actionMetaData,\n        schedulerId,\n        endpoints.host,\n        endpoints.rpcPort)\n      creationIds += msg.creationId.asString\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName] Try to create a new container with creationId ${msg.creationId.asString}\")\n      msg\n    }.toList\n  }\n\n  /* take the first uncompleted request from requestBuffer. */\n  private def takeUncompletedRequest(): Option[Promise[Either[MemoryQueueError, ActivationMessage]]] = {\n    requestBuffer = requestBuffer.filter(!_.promise.isCompleted)\n    if (requestBuffer.nonEmpty) {\n      Some(requestBuffer.dequeue.promise)\n    } else None\n  }\n\n  private def removeDeletedContainerFromRequestBuffer(containerId: String): Unit = {\n    requestBuffer = requestBuffer.filter { buffer =>\n      if (buffer.containerId.drop(1) == containerId) {\n        buffer.promise.trySuccess(Left(NoActivationMessage()))\n        false\n      } else\n        true\n    }\n  }\n\n  private def handleActivationMessage(msg: ActivationMessage) = {\n    logging.info(this, s\"[$invocationNamespace:$action:$stateName] got a new activation message ${msg.activationId}\")(\n      msg.transid)\n    in.incrementAndGet()\n    takeUncompletedRequest()\n      .map { res =>\n        val totalTimeInScheduler = Interval(msg.transid.meta.start, Instant.now()).duration\n        MetricEmitter.emitHistogramMetric(\n          LoggingMarkers.SCHEDULER_WAIT_TIME(action.asString, action.toStringWithoutVersion),\n          totalTimeInScheduler.toMillis)\n        lastActivationPulledTime.set(Instant.now.toEpochMilli)\n        res.trySuccess(Right(msg))\n        in.decrementAndGet()\n        stay\n      }\n      .getOrElse {\n        queue = queue.enqueue(TimeSeriesActivationEntry(clock.now(), msg))\n        in.decrementAndGet()\n        tryEnableActionThrottling()\n      }\n  }\n\n  private def handleActivationRequest(request: GetActivation)(implicit tid: TransactionId) = {\n    request.lastDuration.foreach(averageDurationBuffer.add(_))\n\n    if (queue.nonEmpty) {\n      val (TimeSeriesActivationEntry(_, msg), newQueue) = queue.dequeue\n      queue = newQueue\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName] Get activation request ${request.containerId}, send one message: ${msg.activationId}\")(\n        msg.transid)\n      val totalTimeInScheduler = Interval(msg.transid.meta.start, Instant.now()).duration\n      MetricEmitter.emitHistogramMetric(\n        LoggingMarkers.SCHEDULER_WAIT_TIME(action.asString, action.toStringWithoutVersion),\n        totalTimeInScheduler.toMillis)\n      lastActivationPulledTime.set(Instant.now.toEpochMilli)\n\n      sender ! GetActivationResponse(Right(msg))\n      tryDisableActionThrottling()\n    } else {\n      pollForActivation(sender, request)\n      stay\n    }\n  }\n\n  /**\n   * Save promise in a Queue, once new activationMessage come, complete the promise with it, if timeout(1s), complete the\n   * promise with NoActivationMessage\n   */\n  private def pollForActivation(sender: ActorRef, request: GetActivation)(implicit tid: TransactionId): Unit = {\n    val promise = Promise[Either[MemoryQueueError, ActivationMessage]]()\n    val cancelPoll = actorSystem.scheduler.scheduleOnce(pollTimeOut) {\n      self ! CancelPoll(promise)\n    }\n\n    // \"1xxx\" is always bigger than \"0xxx\", so warmed containers will be took first while dequeue from `requestBuffer`\n    val warmedFlag = if (request.warmed) 1 else 0\n    requestBuffer.enqueue(BufferedRequest(warmedFlag + request.containerId, promise))\n    promise.future.onComplete {\n      case Success(value) =>\n        sender ! GetActivationResponse(value)\n        value match {\n          case Right(msg) =>\n            logging.info(\n              this,\n              s\"[$invocationNamespace:$action:$stateName] Send msg ${msg.activationId} to waiting request ${request.containerId}\")(\n              msg.transid)\n            cancelPoll.cancel()\n          case Left(_) => // do nothing\n        }\n      case Failure(t) => // this shouldn't happen\n        logging.error(\n          this,\n          s\"[$invocationNamespace:$action:$stateName] Unexpected error ${t.getMessage} while poll for activation.\")\n        sender ! GetActivationResponse(Left(NoActivationMessage()))\n        cancelPoll.cancel()\n    }\n  }\n\n  /** Generates an activation with zero runtime. Usually used for error cases */\n  private def generateFallbackActivation(msg: ActivationMessage, response: ActivationResponse): WhiskActivation = {\n    val now = clock.now()\n    val causedBy = if (msg.causedBySequence) {\n      Some(Parameters(WhiskActivation.causedByAnnotation, JsString(Exec.SEQUENCE)))\n    } else None\n\n    val limits = Parameters(WhiskActivation.limitsAnnotation, actionMetaData.limits.toJson)\n    val binding =\n      actionMetaData.binding.map(f => Parameters(WhiskActivation.bindingAnnotation, JsString(f.asString)))\n\n    WhiskActivation(\n      activationId = msg.activationId,\n      namespace = msg.user.namespace.name.toPath,\n      subject = msg.user.subject,\n      cause = msg.cause,\n      name = msg.action.name,\n      version = msg.action.version.getOrElse(SemVer()),\n      start = now,\n      end = now,\n      duration = Some(0),\n      response = response,\n      annotations = {\n        Parameters(WhiskActivation.pathAnnotation, JsString(msg.action.copy(version = None).asString)) ++\n          Parameters(WhiskActivation.kindAnnotation, JsString(actionMetaData.exec.kind)) ++\n          causedBy ++ limits ++ binding\n      })\n  }\n\n  private def isWhiskError(error: ContainerCreationError): Boolean = ContainerCreationError.whiskErrors.contains(error)\n}\n\nobject MemoryQueue {\n  private[queue] val queueConfig = loadConfigOrThrow[QueueConfig](ConfigKeys.schedulerQueue)\n\n  def props(etcdClient: EtcdClient,\n            durationChecker: DurationChecker,\n            fqn: FullyQualifiedEntityName,\n            messagingProducer: MessageProducer,\n            schedulingConfig: SchedulingConfig,\n            invocationNamespace: String,\n            revision: DocRevision,\n            endpoints: SchedulerEndpoints,\n            actionMetaData: WhiskActionMetaData,\n            dataManagementService: ActorRef,\n            watcherService: ActorRef,\n            containerManager: ActorRef,\n            decisionMaker: ActorRef,\n            schedulerId: SchedulerInstanceId,\n            ack: ActiveAck,\n            store: (TransactionId, WhiskActivation, UserContext) => Future[Any],\n            getUserLimit: String => Future[Int])(implicit logging: Logging): Props = {\n    implicit val clock: Clock = SystemClock\n    Props(\n      new MemoryQueue(\n        etcdClient,\n        durationChecker,\n        fqn: FullyQualifiedEntityName,\n        messagingProducer: MessageProducer,\n        schedulingConfig: SchedulingConfig,\n        invocationNamespace: String,\n        revision,\n        endpoints: SchedulerEndpoints,\n        actionMetaData,\n        dataManagementService,\n        watcherService,\n        containerManager,\n        decisionMaker,\n        schedulerId,\n        ack,\n        store,\n        getUserLimit,\n        checkToDropStaleActivation,\n        queueConfig))\n  }\n\n  @tailrec\n  def dropOld(\n    clock: Clock,\n    queue: Queue[TimeSeriesActivationEntry],\n    retention: Duration,\n    reason: String,\n    completeErrorActivation: (ActivationMessage, String, Boolean) => Future[Any]): Queue[TimeSeriesActivationEntry] = {\n    if (queue.isEmpty || Duration.between(queue.head.timestamp, clock.now()).compareTo(retention) < 0)\n      queue\n    else {\n      completeErrorActivation(queue.head.msg, reason, true)\n      dropOld(clock, queue.tail, retention, reason, completeErrorActivation)\n    }\n  }\n\n  def checkToDropStaleActivation(clock: Clock,\n                                 queue: Queue[TimeSeriesActivationEntry],\n                                 maxRetentionMs: Long,\n                                 lastActivationExecutedTime: AtomicLong,\n                                 invocationNamespace: String,\n                                 actionMetaData: WhiskActionMetaData,\n                                 stateName: MemoryQueueState,\n                                 queueRef: ActorRef)(implicit logging: Logging) = {\n    val action = actionMetaData.fullyQualifiedName(true)\n    logging.debug(\n      this,\n      s\"[$invocationNamespace:$action:$stateName] use the given retention timeout: $maxRetentionMs for this action kind: ${actionMetaData.exec.kind}.\")\n\n    if (queue.nonEmpty && Duration\n          .between(queue.head.timestamp, clock.now())\n          .compareTo(Duration.ofMillis(maxRetentionMs)) >= 0) {\n      logging.info(\n        this,\n        s\"[$invocationNamespace:$action:$stateName] some activations are stale msg: ${queue.head.msg.activationId}.\")\n      val timeSinceLastActivationGrabbed = clock.now().toEpochMilli - lastActivationExecutedTime.get()\n      if (timeSinceLastActivationGrabbed > maxRetentionMs && timeSinceLastActivationGrabbed > actionMetaData.limits.timeout.millis) {\n        MetricEmitter.emitCounterMetric(\n          LoggingMarkers\n            .SCHEDULER_QUEUE_NOT_PROCESSING(invocationNamespace, action.asString, action.toStringWithoutVersion))\n      }\n      queueRef ! DropOld\n    }\n  }\n\n  private def getRetentionTimeout(actionMetaData: WhiskActionMetaData, queueConfig: QueueConfig): Long = {\n    if (actionMetaData.exec.kind == ExecMetaDataBase.BLACKBOX) {\n      queueConfig.maxBlackboxRetentionMs\n    } else {\n      queueConfig.maxRetentionMs\n    }\n  }\n}\n\ncase class QueueSnapshot(initialized: Boolean,\n                         incomingMsgCount: AtomicInteger,\n                         currentMsgCount: Int,\n                         existingContainerCount: Int,\n                         inProgressContainerCount: Int,\n                         staleActivationNum: Int,\n                         existingContainerCountInNamespace: Int,\n                         inProgressContainerCountInNamespace: Int,\n                         averageDuration: Option[Double],\n                         namespaceLimit: Int,\n                         actionLimit: Int,\n                         maxActionConcurrency: Int,\n                         stateName: MemoryQueueState,\n                         recipient: ActorRef)\n\ncase class QueueConfig(idleGrace: FiniteDuration,\n                       stopGrace: FiniteDuration,\n                       flushGrace: FiniteDuration,\n                       gracefulShutdownTimeout: FiniteDuration,\n                       maxRetentionSize: Int,\n                       maxRetentionMs: Long,\n                       maxBlackboxRetentionMs: Long,\n                       throttlingFraction: Double,\n                       durationBufferSize: Int,\n                       failThrottleAsWhiskError: Boolean)\n\ncase class BufferedRequest(containerId: String, promise: Promise[Either[MemoryQueueError, ActivationMessage]])\n\ncase object DropOld\n\ncase class ContainerKeyMeta(revision: DocRevision, invokerId: Int, containerId: String)\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/queue/NoopDurationChecker.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.entity.WhiskActionMetaData\n\nimport scala.concurrent.Future\n\nobject NoopDurationCheckerProvider extends DurationCheckerProvider {\n  override def instance(actorSystem: ActorSystem, log: Logging): NoopDurationChecker = {\n    implicit val as: ActorSystem = actorSystem\n    implicit val logging: Logging = log\n    new NoopDurationChecker()\n  }\n}\n\nobject NoopDurationChecker {\n  implicit val serde = new ElasticSearchDurationCheckResultFormat()\n}\n\nclass NoopDurationChecker extends DurationChecker {\n  import scala.concurrent.ExecutionContext.Implicits.global\n\n  override def checkAverageDuration(invocationNamespace: String, actionMetaData: WhiskActionMetaData)(\n    callback: DurationCheckResult => DurationCheckResult): Future[DurationCheckResult] = {\n    Future {\n      DurationCheckResult(Option.apply(0), 0, 0)\n    }\n  }\n}\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/queue/QueueManager.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue\n\nimport java.nio.charset.StandardCharsets\nimport java.time.Instant\n\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, ActorSelection, PoisonPill, Props}\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.util.Timeout\nimport org.apache.openwhisk.common._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.WarmUp.isWarmUpAction\nimport org.apache.openwhisk.core.ack.ActiveAck\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.containerpool.Interval\nimport org.apache.openwhisk.core.database.{ArtifactStore, DocumentRevisionMismatchException, UserContext}\nimport org.apache.openwhisk.core.entity.{ActivationResponse => OriginActivationResponse, _}\nimport org.apache.openwhisk.core.etcd.EtcdKV.{QueueKeys, SchedulerKeys}\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdFollower, EtcdLeader}\nimport org.apache.openwhisk.core.scheduler.{SchedulerEndpoints, SchedulerStates}\nimport org.apache.openwhisk.core.service._\nimport pureconfig.loadConfigOrThrow\nimport spray.json.{DefaultJsonProtocol, _}\n\nimport scala.collection.JavaConverters._\nimport scala.collection.concurrent.TrieMap\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.util.{Failure, Try}\nimport pureconfig.generic.auto._\n\nobject QueueSize\ncase class MemoryQueueKey(invocationNamespace: String, docInfo: DocInfo)\ncase class MemoryQueueValue(queue: ActorRef, isLeader: Boolean)\ncase class UpdateMemoryQueue(oldAction: DocInfo,\n                             newAction: FullyQualifiedEntityName,\n                             activationMessage: ActivationMessage)\ncase class CreateNewQueue(activationMessage: ActivationMessage,\n                          action: FullyQualifiedEntityName,\n                          actionMetadata: WhiskActionMetaData)\n\ncase class RecoverQueue(activationMessage: ActivationMessage,\n                        action: FullyQualifiedEntityName,\n                        actionMetadata: WhiskActionMetaData)\n\ncase class QueueManagerConfig(maxRetriesToGetQueue: Int, maxSchedulingTime: FiniteDuration)\n\nclass QueueManager(\n  entityStore: ArtifactStore[WhiskEntity],\n  getWhiskActionMetaData: (ArtifactStore[WhiskEntity],\n                           DocId,\n                           DocRevision,\n                           Boolean,\n                           Boolean) => Future[WhiskActionMetaData],\n  etcdClient: EtcdClient,\n  schedulerEndpoints: SchedulerEndpoints,\n  schedulerId: SchedulerInstanceId,\n  dataManagementService: ActorRef,\n  watcherService: ActorRef,\n  ack: ActiveAck,\n  store: (TransactionId, WhiskActivation, UserContext) => Future[Any],\n  childFactory: (ActorRefFactory, String, FullyQualifiedEntityName, DocRevision, WhiskActionMetaData) => ActorRef,\n  schedulerConsumer: MessageConsumer,\n  queueManagerConfig: QueueManagerConfig = loadConfigOrThrow[QueueManagerConfig](ConfigKeys.schedulerQueueManager))(\n  implicit logging: Logging)\n    extends Actor {\n\n  val maxPeek = loadConfigOrThrow[Int](ConfigKeys.schedulerMaxPeek)\n\n  /** key: leader-key, value:DocRevision */\n  private val initRevisionMap = TrieMap[String, DocRevision]()\n\n  private val actorSelectionMap = TrieMap[String, ActorSelection]()\n\n  private val leaderElectionCallbacks = TrieMap[String, (Either[EtcdFollower, EtcdLeader], Boolean) => Unit]()\n\n  private implicit val askTimeout = Timeout(5.seconds)\n  private implicit val ec = context.dispatcher\n  private implicit val system = context.system\n\n  private val watcherName = \"queue-manager\"\n  // watch leaders and register them into actorSelectionMap\n  watcherService ! WatchEndpoint(QueueKeys.queuePrefix, \"\", isPrefix = true, watcherName, Set(PutEvent, DeleteEvent))\n\n  private var isShuttingDown = false\n\n  override def receive: Receive = {\n    case request: CreateQueue if isWarmUpAction(request.fqn) =>\n      logging.info(\n        this,\n        s\"The ${request.fqn} action is an action used to connect a network level connection. So drop the message without creating a queue.\")\n      sender ! CreateQueueResponse(request.invocationNamespace, request.fqn, success = true)\n\n    // note: action sent from the pool balancer already includes version\n    case request: CreateQueue =>\n      val receiver = sender\n      QueuePool.get(MemoryQueueKey(request.invocationNamespace, request.fqn.toDocId.asDocInfo(request.revision))) match {\n        case Some(_) =>\n          logging.info(this, s\"Queue already exist for ${request.invocationNamespace}/${request.fqn}\")\n          receiver ! CreateQueueResponse(request.invocationNamespace, request.fqn, success = true)\n\n        case None =>\n          logging.info(this, s\"Trying to create queue for ${request.invocationNamespace}/${request.fqn}\")\n          electLeaderAndCreateQueue(request, Some(receiver))\n      }\n\n    case msg: ElectionResult =>\n      msg.leadership match {\n        case Right(EtcdLeader(key, value, lease)) =>\n          leaderElectionCallbacks.remove(key).foreach { callback =>\n            callback(Right(EtcdLeader(key, value, lease)), isShuttingDown)\n          }\n\n        case Left(EtcdFollower(key, value)) =>\n          leaderElectionCallbacks.remove(key).foreach { callback =>\n            callback(Left(EtcdFollower(key, value)), isShuttingDown)\n          }\n      }\n\n    case msg: ActivationMessage =>\n      logging.info(\n        this,\n        s\"Got activation message ${msg.activationId} for ${msg.user.namespace}/${msg.action} from remote queue manager.\")(\n        msg.transid)\n\n      if (sender() == self) {\n        handleCycle(msg)(msg.transid)\n      } else {\n        handleActivationMessage(msg)\n      }\n\n    case UpdateMemoryQueue(oldAction, newAction, msg) =>\n      logging.info(\n        this,\n        s\"[${msg.activationId}] Update the memory queue for ${newAction.namespace}/${newAction.name}, old rev: ${oldAction.rev} new rev: ${msg.revision}, activationId: ${msg.activationId.asString}\")\n      implicit val transid = msg.transid\n      QueuePool.get(MemoryQueueKey(msg.user.namespace.name.asString, oldAction)) match {\n        case Some(memoryQueueValue) =>\n          QueuePool.put(\n            MemoryQueueKey(msg.user.namespace.name.asString, oldAction),\n            MemoryQueueValue(memoryQueueValue.queue, false))\n          memoryQueueValue.queue ! StopSchedulingAsOutdated\n\n        case _ =>\n        // do nothing because we will anyway create a new one\n      }\n      createNewQueue(newAction, msg)\n\n    case CreateNewQueue(msg, action, actionMetaData) =>\n      val memoryQueueKey = MemoryQueueKey(msg.user.namespace.name.asString, action.toDocId.asDocInfo(msg.revision))\n      QueuePool.get(memoryQueueKey) match {\n        case Some(queue) if queue.isLeader =>\n          queue.queue ! msg\n          logging.info(this, s\"Queue for action $action is already updated, skip\")(msg.transid)\n        case _ =>\n          val queue =\n            childFactory(context, msg.user.namespace.name.asString, action, msg.revision, actionMetaData)\n          queue ! VersionUpdated\n          QueuePool.put(\n            MemoryQueueKey(msg.user.namespace.name.asString, action.toDocId.asDocInfo(msg.revision)),\n            MemoryQueueValue(queue, true))\n          updateInitRevisionMap(getLeaderKey(msg.user.namespace.name.asString, msg.action), msg.revision)\n          queue ! msg\n          msg.transid.mark(this, LoggingMarkers.SCHEDULER_QUEUE_CREATE)\n          if (isShuttingDown) {\n            queue ! GracefulShutdown\n          }\n      }\n\n    case RecoverQueue(msg, action, actionMetaData) =>\n      QueuePool.keys.find(k =>\n        k.invocationNamespace == msg.user.namespace.name.asString && k.docInfo.id == action.toDocId) match {\n        // a newer queue is created, send msg to new queue\n        case Some(key) if key.docInfo.rev >= msg.revision =>\n          QueuePool.get(key) match {\n            case Some(queue) if queue.isLeader =>\n              queue.queue ! msg.copy(revision = key.docInfo.rev)\n              logging.info(this, s\"Queue for action $action is already recovered, skip\")(msg.transid)\n            case _ =>\n              recreateQueue(action, msg, actionMetaData)\n          }\n        case _ =>\n          recreateQueue(action, msg, actionMetaData)\n\n      }\n\n    // leaderKey is now optional, it becomes None when the stale queue is removed\n    case QueueRemoved(invocationNamespace, action, leaderKey) =>\n      (QueuePool.remove(MemoryQueueKey(invocationNamespace, action)), leaderKey) match {\n        case (Some(_), Some(key)) =>\n          logging.info(this, s\"Remove init revision map cause queue is removed, key: ${key}\")\n          initRevisionMap.remove(key)\n        case _ => // do nothing\n      }\n      sender ! QueueRemovedCompleted // notify queue that it can stop safely\n\n    // a Removed queue backed to Running\n    case QueueReactivated(invocationNamespace, action, docInfo) =>\n      QueuePool.put(MemoryQueueKey(invocationNamespace, docInfo), MemoryQueueValue(sender(), true))\n      updateInitRevisionMap(getLeaderKey(invocationNamespace, action), docInfo.rev)\n\n    // only handle prefix watcher\n    case WatchEndpointInserted(_, key, value, true) =>\n      if (key.contains(\"leader\") && value.contains(\"host\")) {\n        SchedulerEndpoints\n          .parse(value)\n          .map { endpoints =>\n            logging.info(this, s\"Endpoint inserted, key: $key, endpoints: $endpoints\")\n            actorSelectionMap.update(key, endpoints.getRemoteRef(QueueManager.actorName))\n          }\n          .recover {\n            case t =>\n              logging.error(this, s\"Unexpected error $t when put leaderKey: ${key}\")\n          }\n      }\n\n    // only handle prefix watcher\n    case WatchEndpointRemoved(_, key, _, true) =>\n      if (key.contains(\"leader\")) {\n        if (actorSelectionMap.contains(key)) {\n          logging.info(this, s\"Endpoint removed for key: $key\")\n          actorSelectionMap.remove(key)\n        } else {\n          logging.info(this, s\"Endpoint removed for key: $key but not in this scheduler\")\n        }\n      }\n\n    case GracefulShutdown =>\n      isShuttingDown = true\n      logging.info(this, s\"Gracefully shutdown the queue manager\")\n\n      watcherService ! UnwatchEndpoint(QueueKeys.queuePrefix, isPrefix = true, watcherName)\n      logScheduler.cancel()\n      healthReporter ! PoisonPill\n      dataManagementService ! UnregisterData(SchedulerKeys.scheduler(schedulerId))\n\n      QueuePool.values.foreach { queueInfo =>\n        //send GracefulShutdown as the queue is not outdated\n        queueInfo.queue ! GracefulShutdown\n      }\n\n      // this is for graceful shutdown of the feed as well.\n      // When the scheduler endpoint is removed, there can be some unprocessed data in Kafka\n      // So we would wait for some time to consume all messages in Kafka\n      org.apache.pekko.pattern.after(5.seconds, system.scheduler) {\n        feed ! GracefulShutdown\n        Future.successful({})\n      }\n\n    case QueueSize =>\n      sender ! QueuePool.size\n\n    case GetState =>\n      val result =\n        Future.sequence(QueuePool.values.map(_.queue.ask(GetState)(Timeout(5.seconds)).mapTo[StatusData]).toList)\n\n      sender ! result\n\n    case msg =>\n      logging.error(this, s\"failed to elect a leader for ${msg}\")\n\n  }\n\n  private def handler(bytes: Array[Byte]): Future[Unit] = {\n    Future(\n      ActivationMessage\n        .parse(new String(bytes, StandardCharsets.UTF_8)))\n      .flatMap(Future.fromTry)\n      .flatMap { msg =>\n        if (isWarmUpAction(msg.action)) {\n          logging.info(\n            this,\n            s\"The ${msg.action} action is an action used to connect a network level connection. So drop the message without executing activation\")\n        } else {\n          logging.info(\n            this,\n            s\"Got activation message ${msg.activationId} for ${msg.user.namespace}/${msg.action} from kafka.\")(\n            msg.transid)\n          handleActivationMessage(msg)\n        }\n        feed ! MessageFeed.Processed\n        Future.successful({})\n      }\n      .recover {\n        case t: DeserializationException =>\n          feed ! MessageFeed.Processed\n          logging.warn(this, s\"Failed to parse message to ActivationMessage, ${t.getMessage}\")\n      }\n  }\n\n  private val feed = system.actorOf(Props {\n    new MessageFeed(\"activation\", logging, schedulerConsumer, maxPeek, 1.second, handler)\n  })\n\n  private def updateInitRevisionMap(key: String, revision: DocRevision): Unit = {\n    logging.info(this, s\"Update init revision map, key: ${key}, rev: ${revision.rev}\")\n    initRevisionMap.update(key, revision)\n  }\n\n  private def recreateQueue(action: FullyQualifiedEntityName,\n                            msg: ActivationMessage,\n                            actionMetaData: WhiskActionMetaData): Unit = {\n    logging.warn(this, s\"recreate queue for ${msg.action}\")(msg.transid)\n    val queue = createAndStartQueue(msg.user.namespace.name.asString, action, msg.revision, actionMetaData)\n    queue ! msg\n    msg.transid.mark(this, LoggingMarkers.SCHEDULER_QUEUE_RECOVER)\n    if (isShuttingDown) {\n      queue ! GracefulShutdown\n    }\n  }\n\n  private def handleCycle(msg: ActivationMessage)(implicit transid: TransactionId): Unit = {\n    val action = msg.action\n    QueuePool.keys.find(k =>\n      k.invocationNamespace == msg.user.namespace.name.asString && k.docInfo.id == action.toDocId) match {\n      // a newer queue is created, send msg to new queue\n      case Some(key) if key.docInfo.rev >= msg.revision =>\n        QueuePool.get(key) match {\n          case Some(queue) if queue.isLeader =>\n            queue.queue ! msg.copy(revision = key.docInfo.rev)\n            logging.info(this, s\"Queue for action $action is already recovered, skip\")(msg.transid)\n          case _ =>\n            recoverQueue(msg)\n        }\n      case _ =>\n        recoverQueue(msg)\n    }\n  }\n\n  private def recoverQueue(msg: ActivationMessage)(implicit transid: TransactionId): Unit = {\n    val start = transid.started(this, LoggingMarkers.SCHEDULER_QUEUE_RECOVER)\n    logging.info(this, s\"Recover a queue for ${msg.action},\")\n    getWhiskActionMetaData(entityStore, msg.action.toDocId, msg.revision, false, false)\n      .map { actionMetaData: WhiskActionMetaData =>\n        actionMetaData.toExecutableWhiskAction match {\n          case Some(_) =>\n            self ! RecoverQueue(msg, msg.action.copy(version = Some(actionMetaData.version)), actionMetaData)\n            transid.finished(this, start, s\"recovering queue for ${msg.action.toDocId.asDocInfo(actionMetaData.rev)}\")\n\n          case None =>\n            val message =\n              s\"non-executable action: ${msg.action} with rev: ${msg.revision} reached queueManager\"\n            completeErrorActivation(msg, message)\n            transid.failed(this, start, message)\n        }\n      }\n      .recover {\n        case t =>\n          transid.failed(\n            this,\n            start,\n            s\"failed to fetch action ${msg.action} with rev: ${msg.revision}, error ${t.getMessage}\")\n          completeErrorActivation(msg, t.getMessage)\n      }\n  }\n\n  private def createNewQueue(newAction: FullyQualifiedEntityName, msg: ActivationMessage)(\n    implicit transid: TransactionId): Future[Any] = {\n    val start = transid.started(this, LoggingMarkers.SCHEDULER_QUEUE_UPDATE(\"version-mismatch\"))\n\n    logging.info(this, s\"Create a new queue for ${newAction}, rev: ${msg.revision}\")\n\n    getWhiskActionMetaData(entityStore, newAction.toDocId, msg.revision, msg.revision != DocRevision.empty, false)\n      .map { actionMetaData: WhiskActionMetaData =>\n        actionMetaData.toExecutableWhiskAction match {\n          // Always use revision got from Database, there can be 2 cases for the actionMetaData.rev\n          // 1. msg.revision == actionMetaData.rev => OK\n          // 2. msg.revision != actionMetaData.rev => the msg.revision must be empty, else an mismatch error will be\n          //                                          threw, we can use the revision got from Database\n          case Some(_) =>\n            self ! CreateNewQueue(\n              msg.copy(revision = actionMetaData.rev, action = msg.action.copy(version = Some(actionMetaData.version))),\n              newAction.copy(version = Some(actionMetaData.version)),\n              actionMetaData)\n            transid.finished(this, start, s\"action is updated to ${newAction.toDocId.asDocInfo(actionMetaData.rev)}\")\n\n          case None =>\n            val message = s\"non-executable action: ${newAction} with rev: ${msg.revision} reached queueManager\"\n            transid.failed(this, start, message)\n            completeErrorActivation(msg, message)\n        }\n      }\n      .recoverWith {\n        case DocumentRevisionMismatchException(_) =>\n          logging.warn(this, s\"Document revision is mismatched for ${newAction}, rev: ${msg.revision}\")\n          createNewQueue(newAction, msg.copy(revision = DocRevision.empty))\n        case t =>\n          transid.failed(\n            this,\n            start,\n            s\"failed to fetch action $newAction with rev: ${msg.revision}, error ${t.getMessage}\")\n          completeErrorActivation(msg, t.getMessage)\n      }\n  }\n\n  private def handleActivationMessage(msg: ActivationMessage): Any = {\n    implicit val transid = msg.transid\n\n    // Drop the message that has not been scheduled for a long time\n    val schedulingWaitTime = Interval(msg.transid.meta.start, Instant.now()).duration\n    MetricEmitter.emitHistogramMetric(LoggingMarkers.SCHEDULER_KAFKA_WAIT_TIME, schedulingWaitTime.toMillis)\n\n    if (schedulingWaitTime > queueManagerConfig.maxSchedulingTime) {\n      logging.warn(\n        this,\n        s\"[${msg.activationId}] the activation message has not been scheduled for ${queueManagerConfig.maxSchedulingTime.toSeconds} sec\")\n      completeErrorActivation(msg, \"The activation has not been processed: too old activation is arrived.\")\n    } else {\n      QueuePool.get(MemoryQueueKey(msg.user.namespace.name.asString, msg.action.toDocId.asDocInfo(msg.revision))) match {\n        case Some(memoryQueueValue) if memoryQueueValue.isLeader =>\n          memoryQueueValue.queue ! msg\n        case _ =>\n          val key = QueueKeys.queue(msg.user.namespace.name.asString, msg.action.copy(version = None), true)\n\n          initRevisionMap.get(key) match {\n            case Some(revision) =>\n              if (msg.revision > revision) {\n                logging.warn(\n                  this,\n                  s\"[${msg.activationId}] the action version is not matched for ${msg.action.path}/${msg.action.name}, current: ${revision}, received: ${msg.revision}\")\n                MetricEmitter.emitCounterMetric(LoggingMarkers.SCHEDULER_QUEUE_UPDATE(\"version-mismatch\"))\n                val newAction = msg.action.copy(binding = None)\n\n                self ! UpdateMemoryQueue(msg.action.toDocId.asDocInfo(revision), newAction, msg)\n              } else if (msg.revision < revision) {\n                // if revision is mismatched, the action may have been updated,\n                // so try again with the latest code\n                logging.warn(\n                  this,\n                  s\"[${msg.activationId}] activation message with an old revision arrived, it will be replaced with the latest revision and invoked, current: ${revision}, received: ${msg.revision}\")\n                sendActivationByLeaderKey(key, msg.copy(revision = revision))\n              } else {\n                // The code will not run here under normal cases. it's for insurance\n                logging.warn(\n                  this,\n                  s\"[${msg.activationId}] The code will not run here under normal cases, rev: ${msg.revision}\")\n                sendActivationByLeaderKey(key, msg)\n              }\n            case None =>\n              logging.info(\n                this,\n                s\"[${msg.activationId}] the key ${key} is not in the initRevisionMap. revision: ${msg.revision}\")\n              sendActivationByLeaderKey(key, msg)\n          }\n      }\n    }\n  }\n\n  private def sendActivationByLeaderKey(key: String, msg: ActivationMessage)(implicit transid: TransactionId) = {\n    actorSelectionMap.get(key) match {\n      case Some(actorSelect) =>\n        actorSelect ! msg\n      case None =>\n        sendActivationToRemoteQueue(key, msg)\n    }\n  }\n\n  private def sendActivationToRemoteQueue(key: String, msg: ActivationMessage)(\n    implicit transid: TransactionId): Future[Any] = {\n    logging.info(this, s\"[${msg.activationId}] send activation to remote queue, key: ${key} revision: ${msg.revision}\")\n\n    getQueueEndpoint(key) map { endPoint =>\n      Future(\n        SchedulerEndpoints\n          .parse(endPoint))\n        .flatMap(Future.fromTry)\n        .map(endPoint => {\n          val actorSelection = endPoint.getRemoteRef(QueueManager.actorName)\n          logging.info(this, s\"add a new actor selection to a map with key: $key\")\n          actorSelectionMap.update(key, actorSelection)\n          actorSelection ! msg\n        })\n        .recoverWith {\n          case t =>\n            logging.warn(this, s\"[${msg.activationId}] failed to parse endpoints (${t.getMessage})\")\n            completeErrorActivation(\n              msg,\n              \"The activation has not been processed: failed to parse the scheduler endpoint.\")\n        }\n\n    } recoverWith {\n      case t =>\n        logging.warn(this, s\"[${msg.activationId}] activation has been dropped (${t.getMessage})\")\n        completeErrorActivation(msg, \"The activation has not been processed: failed to get the queue endpoint.\")\n    }\n  }\n\n  private def getQueueEndpoint(key: String) = {\n    retryFuture(maxRetries = queueManagerConfig.maxRetriesToGetQueue) {\n      etcdClient.get(key).map { res =>\n        res.getKvsList.asScala.headOption match {\n          case Some(kv) => kv.getValue.toStringUtf8\n          case None     => throw new Exception(s\"Failed to get endpoint ($key)\")\n        }\n      }\n    }\n  }\n\n  private def retryFuture[T](maxRetries: Int = 13,\n                             retries: Int = 1,\n                             factor: Float = 2.0f,\n                             initWait: Int = 1,\n                             curWait: Int = 0)(fn: => Future[T]): Future[T] = {\n    fn recoverWith {\n      case e if retries <= maxRetries =>\n        val wait =\n          if (curWait == 0) initWait\n          else Math.ceil(curWait * factor).toInt\n        org.apache.pekko.pattern.after(wait.milliseconds, system.scheduler) {\n          val message = s\"${e.getMessage} retrying after ${wait}ms ($retries/$maxRetries)\"\n          if (retries == maxRetries) {\n            // if number of retries reaches maxRetries, print warning level log\n            logging.warn(this, message)\n          } else {\n            logging.info(this, message)\n          }\n          retryFuture(maxRetries, retries + 1, factor, initWait, wait)(fn)\n        }\n    }\n  }\n\n  private def electLeaderAndCreateQueue(request: CreateQueue, receiver: Option[ActorRef] = None) = {\n    request.whiskActionMetaData.toExecutableWhiskAction match {\n      case Some(_) =>\n        val leaderKey = getLeaderKey(request)\n\n        // callback will be executed after leader election\n        leaderElectionCallbacks.get(leaderKey) match {\n          case None =>\n            dataManagementService ! ElectLeader(leaderKey, schedulerEndpoints.serialize, self)\n            leaderElectionCallbacks.put(\n              leaderKey,\n              (electResult, isShuttingDown) => {\n                electResult match {\n                  case Right(EtcdLeader(_, _, _)) =>\n                    val queue = createAndStartQueue(\n                      request.invocationNamespace,\n                      request.fqn,\n                      request.revision,\n                      request.whiskActionMetaData)\n                    receiver.foreach(_ ! CreateQueueResponse(request.invocationNamespace, request.fqn, success = true))\n                    if (isShuttingDown) {\n                      queue ! GracefulShutdown\n                    }\n\n                  // in case of follower, do nothing\n                  case Left(EtcdFollower(_, _)) =>\n                    receiver.foreach(_ ! CreateQueueResponse(request.invocationNamespace, request.fqn, success = true))\n                }\n              })\n\n          // there is already a leader election for leaderKey, so skip it\n          case Some(_) =>\n            receiver foreach (_ ! CreateQueueResponse(request.invocationNamespace, request.fqn, success = true))\n        }\n\n      case None =>\n        logging.error(this, s\"non-executable action: ${request.fqn} with rev: ${request.revision} reached QueueManager\")\n        receiver match {\n          case Some(recipient) =>\n            recipient ! CreateQueueResponse(request.invocationNamespace, request.fqn, success = false)\n          case None =>\n          // do nothing\n        }\n\n    }\n  }\n\n  private def createAndStartQueue(invocationNamespace: String,\n                                  action: FullyQualifiedEntityName,\n                                  revision: DocRevision,\n                                  actionMetaData: WhiskActionMetaData): ActorRef = {\n    val queue =\n      childFactory(context, invocationNamespace, action, revision, actionMetaData)\n    queue ! Start\n    QueuePool.put(\n      MemoryQueueKey(invocationNamespace, action.toDocId.asDocInfo(revision)),\n      MemoryQueueValue(queue, true))\n    updateInitRevisionMap(getLeaderKey(invocationNamespace, action), revision)\n    queue\n  }\n\n  private val logScheduler = context.system.scheduler.scheduleAtFixedRate(0.seconds, 1.seconds)(() => {\n    MetricEmitter.emitHistogramMetric(LoggingMarkers.SCHEDULER_QUEUE, QueuePool.countLeader())\n  })\n\n  private val healthReporter = Scheduler.scheduleWaitAtLeast(1.seconds, 1.seconds) { () =>\n    val leaderCount = QueuePool.countLeader()\n    dataManagementService ! UpdateDataOnChange(\n      SchedulerKeys.scheduler(schedulerId),\n      SchedulerStates(schedulerId, leaderCount, schedulerEndpoints).serialize)\n    Future.successful({})\n  }\n\n  private def completeErrorActivation(activation: ActivationMessage, message: String): Future[Any] = {\n    val activationResponse =\n      generateFallbackActivation(activation, OriginActivationResponse.whiskError(message))\n\n    val ackMsg = if (activation.blocking) {\n      CombinedCompletionAndResultMessage(activation.transid, activationResponse, schedulerId)\n    } else {\n      CompletionMessage(activation.transid, activationResponse, schedulerId)\n    }\n\n    ack(\n      activation.transid,\n      activationResponse,\n      activation.blocking,\n      activation.rootControllerIndex,\n      activation.user.namespace.uuid,\n      ackMsg)\n      .andThen {\n        case Failure(t) =>\n          logging.error(this, s\"failed to send ack due to ${t}\")\n      }\n    store(activation.transid, activationResponse, UserContext(activation.user))\n  }\n\n  /** Generates an activation with zero runtime. Usually used for error cases */\n  private def generateFallbackActivation(msg: ActivationMessage,\n                                         response: OriginActivationResponse): WhiskActivation = {\n    val now = Instant.now\n    val causedBy = if (msg.causedBySequence) {\n      Some(Parameters(WhiskActivation.causedByAnnotation, JsString(Exec.SEQUENCE)))\n    } else None\n\n    val binding =\n      msg.action.binding.map(f => Parameters(WhiskActivation.bindingAnnotation, JsString(f.asString)))\n\n    WhiskActivation(\n      activationId = msg.activationId,\n      namespace = msg.user.namespace.name.toPath,\n      subject = msg.user.subject,\n      cause = msg.cause,\n      name = msg.action.name,\n      version = msg.action.version.getOrElse(SemVer()),\n      start = now,\n      end = now,\n      duration = Some(0),\n      response = response,\n      annotations = {\n        Parameters(WhiskActivation.pathAnnotation, JsString(msg.action.copy(version = None).asString)) ++\n          Parameters(WhiskActivation.kindAnnotation, JsString(Exec.UNKNOWN)) ++ causedBy ++ binding\n      })\n  }\n\n  private def getLeaderKey(request: CreateQueue) = {\n    QueueKeys.queue(request.invocationNamespace, request.fqn.copy(version = None), leader = true)\n  }\n\n  private def getLeaderKey(invocationNamespace: String, fqn: FullyQualifiedEntityName) = {\n    QueueKeys.queue(invocationNamespace, fqn.copy(version = None), leader = true)\n  }\n}\n\nobject QueueManager {\n  val actorName = \"QueueManager\"\n\n  def props(\n    entityStore: ArtifactStore[WhiskEntity],\n    getWhiskActionMetaData: (ArtifactStore[WhiskEntity],\n                             DocId,\n                             DocRevision,\n                             Boolean,\n                             Boolean) => Future[WhiskActionMetaData],\n    etcdClient: EtcdClient,\n    schedulerEndpoints: SchedulerEndpoints,\n    schedulerId: SchedulerInstanceId,\n    dataManagementService: ActorRef,\n    watcherService: ActorRef,\n    ack: ActiveAck,\n    store: (TransactionId, WhiskActivation, UserContext) => Future[Any],\n    childFactory: (ActorRefFactory, String, FullyQualifiedEntityName, DocRevision, WhiskActionMetaData) => ActorRef,\n    schedulerConsumer: MessageConsumer)(implicit logging: Logging): Props = {\n    Props(\n      new QueueManager(\n        entityStore,\n        getWhiskActionMetaData,\n        etcdClient,\n        schedulerEndpoints,\n        schedulerId,\n        dataManagementService,\n        watcherService,\n        ack,\n        store,\n        childFactory,\n        schedulerConsumer))\n  }\n}\n\nsealed trait MemoryQueueError extends Product {\n  val causedBy: String\n}\n\nobject MemoryQueueErrorSerdes {\n\n  private implicit val noMessageSerdes = NoActivationMessage.serdes\n  private implicit val noQueueSerdes = NoMemoryQueue.serdes\n  private implicit val mismatchSerdes = ActionMismatch.serdes\n\n  // format that discriminates based on an additional\n  // field \"type\" that can either be \"Cat\" or \"Dog\"\n  implicit val memoryQueueErrorFormat = new RootJsonFormat[MemoryQueueError] {\n    def write(obj: MemoryQueueError): JsValue =\n      JsObject((obj match {\n        case msg: NoActivationMessage => msg.toJson\n        case msg: NoMemoryQueue       => msg.toJson\n        case msg: ActionMismatch      => msg.toJson\n      }).asJsObject.fields + (\"type\" -> JsString(obj.productPrefix)))\n\n    def read(json: JsValue): MemoryQueueError =\n      json.asJsObject.getFields(\"type\") match {\n        case Seq(JsString(\"NoActivationMessage\")) => json.convertTo[NoActivationMessage]\n        case Seq(JsString(\"NoMemoryQueue\"))       => json.convertTo[NoMemoryQueue]\n        case Seq(JsString(\"ActionMismatch\"))      => json.convertTo[ActionMismatch]\n      }\n  }\n}\n\ncase class NoActivationMessage(noActivationMessage: String = NoActivationMessage.asString)\n    extends MemoryQueueError\n    with Message {\n  override val causedBy: String = noActivationMessage\n  override def serialize = NoActivationMessage.serdes.write(this).compactPrint\n}\n\nobject NoActivationMessage extends DefaultJsonProtocol {\n  val asString: String = \"no activation message exist\"\n  def parse(msg: String) = Try(serdes.read(msg.parseJson))\n  implicit val serdes = jsonFormat(NoActivationMessage.apply _, \"noActivationMessage\")\n}\n\ncase class NoMemoryQueue(noMemoryQueue: String = NoMemoryQueue.asString) extends MemoryQueueError with Message {\n  override val causedBy: String = noMemoryQueue\n  override def serialize = NoMemoryQueue.serdes.write(this).compactPrint\n}\n\nobject NoMemoryQueue extends DefaultJsonProtocol {\n  val asString: String = \"no memory queue exist\"\n  def parse(msg: String) = Try(serdes.read(msg.parseJson))\n  implicit val serdes = jsonFormat(NoMemoryQueue.apply _, \"noMemoryQueue\")\n}\n\ncase class ActionMismatch(actionMisMatch: String = ActionMismatch.asString) extends MemoryQueueError with Message {\n  override val causedBy: String = actionMisMatch\n  override def serialize = ActionMismatch.serdes.write(this).compactPrint\n}\n\nobject ActionMismatch extends DefaultJsonProtocol {\n  val asString: String = \"action version does not match\"\n  def parse(msg: String) = Try(serdes.read(msg.parseJson))\n  implicit val serdes = jsonFormat(ActionMismatch.apply _, \"actionMisMatch\")\n}\n\nobject QueuePool {\n  private val _queuePool = TrieMap[MemoryQueueKey, MemoryQueueValue]()\n\n  private[scheduler] def get(key: MemoryQueueKey) = _queuePool.get(key)\n\n  private[scheduler] def put(key: MemoryQueueKey, value: MemoryQueueValue) = _queuePool.put(key, value)\n\n  private[scheduler] def remove(key: MemoryQueueKey) = _queuePool.remove(key)\n\n  private[scheduler] def countLeader() = _queuePool.count(_._2.isLeader)\n\n  private[scheduler] def clear(): Unit = _queuePool.clear()\n\n  private[scheduler] def size = _queuePool.size\n\n  private[scheduler] def values = _queuePool.values\n\n  private[scheduler] def keys = _queuePool.keys\n}\n\ncase class CreateQueue(invocationNamespace: String,\n                       fqn: FullyQualifiedEntityName,\n                       revision: DocRevision,\n                       whiskActionMetaData: WhiskActionMetaData)\ncase class CreateQueueResponse(invocationNamespace: String, fqn: FullyQualifiedEntityName, success: Boolean)\n"
  },
  {
    "path": "core/scheduler/src/main/scala/org/apache/openwhisk/core/scheduler/queue/SchedulingDecisionMaker.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue\n\nimport org.apache.pekko.actor.{Actor, ActorSystem, Props}\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.entity.FullyQualifiedEntityName\nimport org.apache.openwhisk.core.scheduler.SchedulingConfig\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success}\n\nclass SchedulingDecisionMaker(\n  invocationNamespace: String,\n  action: FullyQualifiedEntityName,\n  schedulingConfig: SchedulingConfig)(implicit val actorSystem: ActorSystem, ec: ExecutionContext, logging: Logging)\n    extends Actor {\n\n  private val staleThreshold: Double = schedulingConfig.staleThreshold.toMillis.toDouble\n\n  override def receive: Receive = {\n    case msg: QueueSnapshot =>\n      decide(msg)\n        .andThen {\n          case Success(DecisionResults(Skip, _)) =>\n          // do nothing\n          case Success(result: DecisionResults) =>\n            msg.recipient ! result\n          case Failure(e) =>\n            logging.error(this, s\"failed to make a scheduling decision due to $e\");\n        }\n  }\n\n  private[queue] def decide(snapshot: QueueSnapshot) = {\n    val QueueSnapshot(\n      initialized,\n      incoming,\n      currentMsg,\n      existing,\n      inProgress,\n      staleActivationNum,\n      existingContainerCountInNs,\n      inProgressContainerCountInNs,\n      averageDuration,\n      namespaceLimit,\n      actionLimit,\n      maxActionConcurrency,\n      stateName,\n      _) = snapshot\n    val totalContainers = existing + inProgress\n    val availableMsg = currentMsg + incoming.get()\n    val actionCapacity = actionLimit - totalContainers\n    val namespaceCapacity = namespaceLimit - existingContainerCountInNs - inProgressContainerCountInNs\n    val overProvisionCapacity = ceiling(namespaceLimit * schedulingConfig.namespaceOverProvisionBeforeThrottleRatio) - existingContainerCountInNs - inProgressContainerCountInNs\n\n    if (Math.min(namespaceLimit, actionLimit) <= 0) {\n      // this is an error case, the limit should be bigger than 0\n      stateName match {\n        case Flushing => Future.successful(DecisionResults(Skip, 0))\n        case _        => Future.successful(DecisionResults(Pausing, 0))\n      }\n    } else {\n      val capacity = if (schedulingConfig.allowOverProvisionBeforeThrottle && totalContainers == 0) {\n        // if space available within the over provision ratio amount above namespace limit, create one container for new\n        // action so namespace traffic can attempt to re-balance without blocking entire action\n        if (overProvisionCapacity > 0) {\n          1\n        } else {\n          0\n        }\n      } else {\n        Math.min(namespaceCapacity, actionCapacity)\n      }\n\n      if (capacity <= 0) {\n        stateName match {\n\n          /**\n           * If the container is created later (for any reason), all activations fail(too many requests).\n           *\n           * However, if the container exists(totalContainers != 0), the activation is not treated as a failure and the activation is delivered to the container.\n           */\n          case Running\n              if !schedulingConfig.allowOverProvisionBeforeThrottle || (schedulingConfig.allowOverProvisionBeforeThrottle && overProvisionCapacity <= 0) =>\n            logging.info(\n              this,\n              s\"there is no capacity activations will be dropped or throttled, (availableMsg: $availableMsg totalContainers: $totalContainers, actionLimit: $actionLimit, namespaceLimit: $namespaceLimit, namespaceContainers: $existingContainerCountInNs, namespaceInProgressContainer: $inProgressContainerCountInNs) [$invocationNamespace:$action]\")\n            Future.successful(DecisionResults(EnableNamespaceThrottling(dropMsg = totalContainers == 0), 0))\n          case NamespaceThrottled if schedulingConfig.allowOverProvisionBeforeThrottle && overProvisionCapacity > 0 =>\n            Future.successful(DecisionResults(DisableNamespaceThrottling, 0))\n          // do nothing\n          case _ =>\n            // no need to print any messages if the state is already NamespaceThrottled\n            Future.successful(DecisionResults(Skip, 0))\n        }\n      } else {\n        (stateName, averageDuration) match {\n          // there is no container\n          case (Running, None) if totalContainers == 0 && !initialized =>\n            logging.info(\n              this,\n              s\"add one initial container if totalContainers($totalContainers) == 0 [$invocationNamespace:$action]\")\n            Future.successful(DecisionResults(AddInitialContainer, 1))\n\n          // Todo: when disabling throttling we may create some containers.\n          case (NamespaceThrottled, _) =>\n            Future.successful(DecisionResults(DisableNamespaceThrottling, 0))\n\n          // this is an exceptional case, create a container immediately\n          case (Running, _) if totalContainers == 0 && availableMsg > 0 =>\n            logging.info(\n              this,\n              s\"add one container if totalContainers($totalContainers) == 0 && availableMsg($availableMsg) > 0 [$invocationNamespace:$action]\")\n            Future.successful(DecisionResults(AddContainer, 1))\n\n          case (Flushing, _) if totalContainers == 0 =>\n            logging.info(\n              this,\n              s\"add one container case Paused if totalContainers($totalContainers) == 0 [$invocationNamespace:$action]\")\n            // it is highly likely the queue could not create an initial container if the limit is 0\n            Future.successful(DecisionResults(AddInitialContainer, 1))\n\n          // there is no activation result yet, but some activations became stale\n          // it may cause some over-provisioning if it takes much time to create a container and execution time is short.\n          // but it is a kind of trade-off and we place latency on top of over-provisioning\n          case (Running, None) if staleActivationNum > 0 =>\n            // we can safely get the value as we already checked the existence\n            val num = ceiling(staleActivationNum.toDouble / maxActionConcurrency.toDouble) - inProgress\n            // if it tries to create more containers than existing messages, we just create shortage\n            val actualNum = if (num > availableMsg) availableMsg else num\n            addServersIfPossible(\n              existing,\n              inProgress,\n              0,\n              availableMsg,\n              capacity,\n              namespaceCapacity,\n              actualNum,\n              staleActivationNum,\n              0.0,\n              Running)\n          // need more containers and a message is already processed\n          case (Running, Some(duration)) =>\n            // we can safely get the value as we already checked the existence, have extra protection in case duration is somehow negative\n            val containerThroughput = if (duration <= 0) {\n              maxActionConcurrency\n            } else {\n              (staleThreshold / duration) * maxActionConcurrency\n            }\n            val expectedTps = containerThroughput * (existing + inProgress)\n            val availableNonStaleActivations = availableMsg - staleActivationNum\n\n            var staleContainerProvision = 0\n            if (staleActivationNum > 0) {\n\n              val num = ceiling(staleActivationNum.toDouble / containerThroughput)\n              // if it tries to create more containers than existing messages, we just create shortage\n              staleContainerProvision = (if (num > staleActivationNum) staleActivationNum else num) - inProgress\n            }\n\n            if (availableNonStaleActivations >= expectedTps && existing + inProgress < availableNonStaleActivations && duration > 0) {\n              val num = ceiling((availableNonStaleActivations / containerThroughput) - existing - inProgress)\n              // if it tries to create more containers than existing messages, we just create shortage\n              val actualNum =\n                if (num + totalContainers > availableNonStaleActivations) availableNonStaleActivations - totalContainers\n                else num\n              addServersIfPossible(\n                existing,\n                inProgress,\n                containerThroughput,\n                availableMsg,\n                capacity,\n                namespaceCapacity,\n                actualNum + staleContainerProvision,\n                staleActivationNum,\n                duration,\n                Running)\n            } else if (staleContainerProvision > 0) {\n              addServersIfPossible(\n                existing,\n                inProgress,\n                containerThroughput,\n                availableMsg,\n                capacity,\n                namespaceCapacity,\n                staleContainerProvision,\n                staleActivationNum,\n                duration,\n                Running)\n            } else {\n              Future.successful(DecisionResults(Skip, 0))\n            }\n\n          // generally we assume there are enough containers for actions when shutting down the scheduler\n          // but if there were already too many activation in the queue with not enough containers,\n          // we should add more containers to quickly consume those messages.\n          // this case is for that as a last resort.\n          case (Removing, Some(duration)) if staleActivationNum > 0 =>\n            // we can safely get the value as we already checked the existence\n            val containerThroughput = if (duration <= 0) {\n              maxActionConcurrency\n            } else {\n              (staleThreshold / duration) * maxActionConcurrency\n            }\n            val num = ceiling(staleActivationNum.toDouble / containerThroughput)\n            // if it tries to create more containers than existing messages, we just create shortage\n            val actualNum = (if (num > staleActivationNum) staleActivationNum else num) - inProgress\n            addServersIfPossible(\n              existing,\n              inProgress,\n              containerThroughput,\n              availableMsg,\n              capacity,\n              namespaceCapacity,\n              actualNum,\n              staleActivationNum,\n              duration,\n              Running)\n\n          // same with the above case but no duration exist.\n          case (Removing, None) if staleActivationNum > 0 =>\n            // we can safely get the value as we already checked the existence\n            val num = ceiling(staleActivationNum.toDouble / maxActionConcurrency.toDouble) - inProgress\n            // if it tries to create more containers than existing messages, we just create shortage\n            val actualNum = if (num > availableMsg) availableMsg else num\n            addServersIfPossible(\n              existing,\n              inProgress,\n              0,\n              availableMsg,\n              capacity,\n              namespaceCapacity,\n              actualNum,\n              staleActivationNum,\n              0.0,\n              Running)\n\n          // do nothing\n          case _ =>\n            Future.successful(DecisionResults(Skip, 0))\n        }\n      }\n    }\n  }\n\n  private def addServersIfPossible(existing: Int,\n                                   inProgress: Int,\n                                   containerThroughput: Double,\n                                   availableMsg: Int,\n                                   capacity: Int,\n                                   namespaceCapacity: Int,\n                                   actualNum: Int,\n                                   staleActivationNum: Int,\n                                   duration: Double = 0.0,\n                                   state: MemoryQueueState) = {\n    if (actualNum > capacity) {\n      if (capacity >= namespaceCapacity) {\n        // containers can be partially created. throttling should be enabled\n        logging.info(\n          this,\n          s\"[$state] enable namespace throttling and add $capacity container, staleActivationNum: $staleActivationNum, duration: $duration, containerThroughput: $containerThroughput, availableMsg: $availableMsg, existing: $existing, inProgress: $inProgress, capacity: $capacity [$invocationNamespace:$action]\")\n        Future.successful(DecisionResults(EnableNamespaceThrottling(dropMsg = false), capacity))\n      } else {\n        logging.info(\n          this,\n          s\"[$state] reached max containers allowed for this action adding $capacity containers, but there is still capacity on the namespace so namespace throttling is not turned on.\" +\n            s\" staleActivationNum: $staleActivationNum, duration: $duration, containerThroughput: $containerThroughput, availableMsg: $availableMsg, existing: $existing, inProgress: $inProgress, capacity: $capacity [$invocationNamespace:$action]\")\n        Future.successful(DecisionResults(AddContainer, capacity))\n      }\n    } else if (actualNum <= 0) {\n      // it means nothing\n      Future.successful(DecisionResults(Skip, 0))\n    } else {\n      // create num containers\n      // we need to create one more container than expected because existing container would already took the message\n      logging.info(\n        this,\n        s\"[$state]add $actualNum container, staleActivationNum: $staleActivationNum, duration: $duration, containerThroughput: $containerThroughput, availableMsg: $availableMsg, existing: $existing, inProgress: $inProgress, capacity: $capacity [$invocationNamespace:$action]\")\n      Future.successful(DecisionResults(AddContainer, actualNum))\n    }\n  }\n\n  private def ceiling(d: Double) = math.ceil(d).toInt\n}\n\nobject SchedulingDecisionMaker {\n  def props(invocationNamespace: String, action: FullyQualifiedEntityName, schedulingConfig: SchedulingConfig)(\n    implicit actorSystem: ActorSystem,\n    ec: ExecutionContext,\n    logging: Logging): Props = {\n    Props(new SchedulingDecisionMaker(invocationNamespace, action, schedulingConfig))\n  }\n}\n"
  },
  {
    "path": "core/standalone/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nARG BASE=scala\nFROM ${BASE}\nARG OPENWHISK_JAR\nARG TARGETPLATFORM\n\nENV DOCKER_VERSION=18.06.3-ce\nENV WSK_VERSION=1.2.0\n\nADD bin/init /\nADD bin/stop bin/waitready /bin/\n\nRUN chmod +x /bin/stop /bin/waitready ;\n\nRUN if [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n      curl -sL https://download.docker.com/linux/static/stable/aarch64/docker-${DOCKER_VERSION}.tgz \\\n      | tar xzvf -  -C /usr/bin --strip 1 docker/docker ; \\\n      curl -sL https://github.com/apache/openwhisk-cli/releases/download/${WSK_VERSION}/OpenWhisk_CLI-${WSK_VERSION}-linux-arm64.tgz \\\n      | tar xzvf - -C /usr/bin wsk; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n      curl -sL https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz \\\n      | tar xzvf -  -C /usr/bin --strip 1 docker/docker ; \\\n      curl -sL https://github.com/apache/openwhisk-cli/releases/download/${WSK_VERSION}/OpenWhisk_CLI-${WSK_VERSION}-linux-amd64.tgz \\\n      | tar xzvf - -C /usr/bin wsk; \\\n    else \\\n        echo \"Unsupported platform: $TARGETPLATFORM\"; \\\n        exit 1; \\\n    fi\n\nADD ${OPENWHISK_JAR} /openwhisk-standalone.jar\nWORKDIR /\nEXPOSE 8080\nENTRYPOINT  [\"/init\"]\n"
  },
  {
    "path": "core/standalone/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# OpenWhisk Standalone Server\n\nOpenWhisk standalone server is meant to run a simple OpenWhisk server for local development and test purposes. It can be\nexecuted as a normal java application from command line.\n\n```bash\njava -jar openwhisk-standalone.jar\n```\n\nThis should start the OpenWhisk server on port 3233 by default and launch a Playground UI at port 3232.\n\n![Playground UI](../../docs/images/playground-ui.png)\n\nThe Playground UI can be used to try out simple actions. To make use of all OpenWhisk features [configure the cli][1] and\nthen try out the [samples][2].\n\nThis server by default uses a memory based store and does not depend on any other external service like Kafka and CouchDB.\nIt only needs Docker and Java to for running.\n\nFew key points related to it\n\n* Uses in memory store. Once the server is stopped all changes would be lost\n* Bootstraps the `guest` and `whisk.system` with default keys\n* Supports running on MacOS, Linux and Windows (experimental) setup\n* Can be customized to use any other storage like CouchDB\n\n\n### Build\n\nTo build this standalone server run\n\n```bash\n$ ./gradlew :core:standalone:build\n```\n\nThis would create the runnable jar in `bin/` directory. If you directly want to run the\nserver then you can use following command\n\n```bash\n$ ./gradlew :core:standalone:bootRun\n```\n\nTo pass argument to the run command use\n\n```bash\n$ ./gradlew :core:standalone:bootRun --args='-m runtimes.json'\n```\n\nYou can also build a standalone docker image with:\n\n```bash\n$ ./gradlew :core:standalone:distDocker\n```\n\n###  Usage\n\nOpenWhisk standalone server support various launch options\n\n```\n$ java -jar openwhisk-standalone.jar -h\n\n\n       /\\   \\    / _ \\ _ __   ___ _ __ | |  | | |__ (_)___| | __\n  /\\  /__\\   \\  | | | | '_ \\ / _ \\ '_ \\| |  | | '_ \\| / __| |/ /\n /  \\____ \\  /  | |_| | |_) |  __/ | | | |/\\| | | | | \\__ \\   <\n \\   \\  /  \\/    \\___/| .__/ \\___|_| |_|__/\\__|_| |_|_|___/_|\\_\\\n  \\___\\/ tm           |_|\n\n  -m, --manifest  <arg>               Manifest JSON defining the supported\n                                      runtimes\n  -c, --config-file  <arg>            application.conf which overrides the\n                                      default standalone.conf\n      --api-gw                        Enable API Gateway support\n      --couchdb                       Enable CouchDB support\n      --user-events                   Enable User Events along with Prometheus\n                                      and Grafana\n      --kafka                         Enable embedded Kafka support\n      --kafka-ui                      Enable Kafka UI\n\n      --all                           Enables all the optional services\n                                      supported by Standalone OpenWhisk like\n                                      CouchDB, Kafka etc\n      --api-gw-port  <arg>            API Gateway Port\n      --clean                         Clean any existing state like database\n  -d, --data-dir  <arg>               Directory used for storage\n      --dev-kcf                       Enables KubernetesContainerFactory for\n                                      local development\n      --dev-mode                      Developer mode speeds up the startup by\n                                      disabling preflight checks and avoiding\n                                      explicit pulls.\n      --dev-user-events-port  <arg>   Specify the port for the user-event\n                                      service. This mode can be used for local\n                                      development of user-event service by\n                                      configuring Prometheus to connect to\n                                      existing running service instance\n      --disable-color-logging         Disables colored logging\n      --enable-bootstrap              Enable bootstrap of default users and\n                                      actions like those needed for Api Gateway\n                                      or Playground UI. By default bootstrap is\n                                      done by default when using Memory store or\n                                      default CouchDB support. When using other\n                                      stores enable this flag to get bootstrap\n                                      done\n      --kafka-docker-port  <arg>      Kafka port for use by docker based\n                                      services. If not specified then 9091 or\n                                      some random free port (if 9091 is busy)\n                                      would be used\n      --kafka-port  <arg>             Kafka port. If not specified then 9092 or\n                                      some random free port (if 9092 is busy)\n                                      would be used\n      --no-ui                         Disable Playground UI\n      --ui-port  <arg>                Playground UI server port. If not specified\n                                      then 3232 or some random free port (if\n                                      org.apache.openwhisk.standalone.StandaloneOpenWhisk$@75a1cd57\n                                      is busy) would be used\n  -p, --port  <arg>                   Server port\n  -v, --verbose\n      --zk-port  <arg>                Zookeeper port. If not specified then 2181\n                                      or some random free port (if 2181 is busy)\n                                      would be used\n  -h, --help                          Show help message\n      --version                       Show version of this program\n\nOpenWhisk standalone server\n\n```\n\nSections below would illustrate some of the supported options\n\nTo change the default config you can provide a custom `application.conf` file via `-c` option. The application conf file\nmust always include the default `standalone.conf`\n\n```hocon\ninclude classpath(\"standalone.conf\")\n\nwhisk {\n  //Custom config\n}\n```\n\nThen pass this config file\n\n```bash\njava -jar openwhisk-standalone.jar -c custom.conf\n```\n\n#### Adding custom namespaces\n\nIf you need to register custom namespaces (aka users) then you can pass them via config file like below\n\n```hocon\ninclude classpath(\"standalone.conf\")\n\nwhisk {\n  users {\n    whisk-test = \"cafebabe-cafe-babe-cafe-babecafebabe:007zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\"\n  }\n}\n```\n\nThen pass this config file via `-c` option. You can check the users created from log\n\n```\n[2019-06-21T19:52:02.923Z] [INFO] [#tid_userBootstrap] [StandaloneOpenWhisk] Created user [guest]\n[2019-06-21T19:52:03.008Z] [INFO] [#tid_userBootstrap] [StandaloneOpenWhisk] Created user [whisk.system]\n[2019-06-21T19:52:03.094Z] [INFO] [#tid_userBootstrap] [StandaloneOpenWhisk] Created user [whisk.test]\n```\n\n#### Using custom runtimes\n\nTo use custom runtime pass the runtime manifest via `-m` option\n\n```json\n{\n  \"runtimes\": {\n    \"ruby\": [\n      {\n        \"kind\": \"ruby:2.5\",\n        \"default\": true,\n        \"deprecated\": false,\n        \"attached\": {\n          \"attachmentName\": \"codefile\",\n          \"attachmentType\": \"text/plain\"\n        },\n        \"image\": {\n          \"prefix\": \"openwhisk\",\n          \"name\": \"action-ruby-v2.5\",\n          \"tag\": \"latest\"\n        }\n      }\n    ]\n  }\n}\n```\n\nThe pass this file at launch time\n\n```bash\njava -jar openwhisk-standalone.jar -m custom-runtime.json\n```\n\nYou can then see the runtime config reflect in `http://localhost:3233`\n\n#### Using CouchDB\n\nIf you need to use CouchDB then you can launch the standalone server with `--couchdb` option. This would launch\na CouchDB server which would configured to store files in user home directory under `.openwhisk/standalone` folder.\n\nIf you need to connect to external CouchDB or any other supported artifact store then you can pass the required config\n\n```hocon\ninclude classpath(\"standalone.conf\")\n\nwhisk {\n  couchdb {\n    protocol = \"http\"\n    host = \"172.17.0.1\"\n    port = \"5984\"\n    username = \"whisk_admin\"\n    password = \"some_passw0rd\"\n    provider = \"CouchDB\"\n    databases {\n      WhiskAuth = \"whisk_local_subjects\"\n      WhiskEntity = \"whisk_local_whisks\"\n      WhiskActivation = \"whisk_local_activations\"\n    }\n  }\n}\n```\n\nThen pass this config file via `-c` option.\n\nNote that Standalone OpenWhisk will not bootstrap users and actions (e.g., API Gateway and Playground UI)\nwhen using an external database unless explicitly requested with `--enable-bootstrap`. This is to ensure\nthat default users and actions are not added to your external artifact store.\n\n#### Using API Gateway\n\nAPI Gateway mode can be enabled via `--api-gw` flag. In this mode upon launch a separate container for [OpenWhisk API gateway][3]\nwould be launched on port `3234` (can be changed with `--api-gw-port`). In this mode you can make use of the\n[API Gateway][4] support.\n\n#### Using Kafka\n\nStandalone OpenWhisk supports launching an [embedded kafka][5]. This mode is mostly useful for developers working on OpenWhisk\nimplementation itself.\n\n```\njava -jar openwhisk-standalone.jar --kafka\n```\n\nIt also supports launching a Kafka UI based on [Kafdrop 3][6] which enables seeing the topics created and structure of messages\nexchanged on those topics.\n\n```\njava -jar openwhisk-standalone.jar --kafka --kafka-ui\n```\n\nBy default the ui server would be accessible at http://localhost:9000. In case 9000 port is busy then a random port would\nbe selected. To find out the port look for message in log like below (or grep for `whisk-kafka-drop-ui`)\n\n```\n[ 9092  ] localhost:9092 (kafka)\n[ 9092  ] 192.168.65.2:9091 (kafka-docker)\n[ 2181  ] Zookeeper (zookeeper)\n[ 9000  ] http://localhost:9000 (whisk-kafka-drop-ui)\n```\n\n#### User Events\n\nStandalone OpenWhisk supports emitting [user events][7] and displaying them via Grafana Dashboard. The metrics are\nconsumed by [User Event Service][8] which converts them into metrics consumed via Prometheus.\n\n```\njava -jar openwhisk-standalone.jar --user-events\n```\n\nThis mode would launch an embedded Kafka, User Event service, Prometheus and Grafana with preconfigured dashboards.\n\n```\nLaunched service details\n\n[ 9092  ] localhost:9092 (kafka)\n[ 9091  ] 192.168.65.2:9091 (kafka-docker)\n[ 2181  ] Zookeeper (zookeeper)\n[ 3235  ] http://localhost:3235 (whisk-user-events)\n[ 9090  ] http://localhost:9090 (whisk-prometheus)\n[ 3000  ] http://localhost:3000 (whisk-grafana)\n```\n\n#### Using KubernetesContainerFactory\n\nStandalone OpenWhisk can be configured to use KubernetesContainerFactory (KCF) via `--dev-kcf` option. This mode can be used to\nsimplify developing KubernetesContainerFactory.\n\nBelow mentioned steps are based on [Kind][9] tool for running local Kubernetes clusters using Docker container \"nodes\".\nHowever this mode should work against any Kubernetes cluster if the the `KUBECONFIG` is properly set.\n\n##### 1. Install and configure Kind\n\nWe would use Kind to setup a local k8s. Follow the steps [here][10] to create a simple cluster.\n\n```bash\n$ kind create cluster --wait 5m\n\n# Export the kind config for kubectl usage\n$ export KUBECONFIG=\"$(kind get kubeconfig-path)\"\n\n# Configure the default namespace\n$ kubectl config set-context --current --namespace=default\n\n# See the config path\n$ kind get kubeconfig-path\n/Users/example/.kube/kind-config-kind\n```\n\n##### 2. Launch Standalone\n\n```bash\n# Launch it with `kubeconfig` system property set to kind config\n$ java  -Dkubeconfig=\"$(kind get kubeconfig-path)\" -jar bin/openwhisk-standalone.jar --dev-kcf\n```\n\nOnce started and required `.wskprops` configured to use the standalone server create a `hello.js` function\n\n```js\nfunction main(params) {\n    greeting = 'hello, world'\n    var hello = {payload: greeting}\n    var result = {...hello, ...process.env}\n    console.log(greeting);\n    return result\n}\n```\n\n```bash\n$ wsk action create hello hello.js\n$ wsk action invoke hello -br\n```\n\nThis shows an output like below indicating that KubernetesContainerFactory based invocation is working properly.\n\n```\n{\n    \"HOME\": \"/root\",\n    \"HOSTNAME\": \"wsk0-2-prewarm-nodejs10\",\n    \"KUBERNETES_PORT\": \"tcp://10.96.0.1:443\",\n    \"KUBERNETES_PORT_443_TCP\": \"tcp://10.96.0.1:443\",\n    \"KUBERNETES_PORT_443_TCP_ADDR\": \"10.96.0.1\",\n    \"KUBERNETES_PORT_443_TCP_PORT\": \"443\",\n    \"KUBERNETES_PORT_443_TCP_PROTO\": \"tcp\",\n    \"KUBERNETES_SERVICE_HOST\": \"10.96.0.1\",\n    \"KUBERNETES_SERVICE_PORT\": \"443\",\n    \"KUBERNETES_SERVICE_PORT_HTTPS\": \"443\",\n    \"NODE_VERSION\": \"10.15.3\",\n    \"PATH\": \"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n    \"PWD\": \"/nodejsAction\",\n    \"YARN_VERSION\": \"1.13.0\",\n    \"__OW_ACTION_NAME\": \"/guest/hello\",\n    \"__OW_ACTION_VERSION\": \"0.0.1\",\n    \"__OW_ACTIVATION_ID\": \"71e48d2d62e142eca48d2d62e192ec2d\",\n    \"__OW_API_HOST\": \"http://host.docker.internal:3233\",\n    \"__OW_DEADLINE\": \"1570223213407\",\n    \"__OW_NAMESPACE\": \"guest\",\n    \"__OW_TRANSACTION_ID\": \"iSOoNklk6V7l7eh8KJnvugidKEmaNJmv\",\n    \"payload\": \"hello, world\"\n}\n```\n\n## Launching OpenWhisk standalone with Docker\n\nIf you have docker and bash installed, you can launch the standalone openwhisk from the docker image with just:\n\n`bash <(curl -sL https://s.apache.org/openwhisk.sh)`\n\nThe script will start the standalone controller with Docker, and will also try to open the playground. It was tested on Linux, OSX and Windows with Git Bash. If a browser does not automatically open the OpenWhisk playground, you can access it at `http://localhost:3232`.\n\nThe default standalone controller image is published as `openwhisk/standalone:nightly` for convenience.\n\nIf you do not want to execute arbitrary code straight from the net, you can look at [this script](start.sh), check it and run it when you feel safe.\n\nIf the playground is not enough, you can then install the [wsk CLI](https://github.com/apache/openwhisk-cli/releases) and retrieve the command line to configure `wsk` with:\n\n`docker logs openwhisk | grep 'wsk property'`\n\nTo properly shut down OpenWhisk and any additional containers it has created, use [this script](stop.sh) or run the command:\n\n`docker exec openwhisk stop`\n\n### Extra Args for the Standalone OpenWhisk Docker Image\n\nYou can specify a different image to this script and/or pass additional parameters to Docker and/or to the standalone jar. The general format is:\n\n`bash <(curl -sL https://s.apache.org/openwhisk.sh) [<docker-parameters>...] [<image-name>] [<standalone-jar-options>...]`\n\ne.g.\n\n`bash <(curl -sL https://s.apache.org/openwhisk.sh) -e SOME_DOCKER_ENV=a openwhisk/standalone:nightly --no-ui`\n\nExtra args are useful to configure the JVM running OpenWhisk and to propagate additional environment variables to containers running images. This feature is useful for example to enable debugging for actions.\n\nYou can pass additional parameters (for example set system properties) to the JVM running OpenWhisk setting the environment variable `JVM_EXTRA_ARGS`. For example `-e JVM_EXTRA_ARGS=-Dconfig.loads` allows to enable tracing of configuration. You can set any OpenWhisk parameter with this feature.\n\nYou can also set additional environment variables for each container running actions invoked by OpenWhisk by setting `CONTAINER_EXTRA_ENV`. For example, setting `-e CONTAINER_EXTRA_ENV=__OW_DEBUG_PORT=8081` enables debugging for those images supporting starting the action under a debugger, like the typescript runtime.\n\n[1]: https://github.com/apache/openwhisk/blob/master/docs/cli.md\n[2]: https://github.com/apache/openwhisk/blob/master/docs/samples.md\n[3]: https://github.com/apache/openwhisk-apigateway\n[4]: https://github.com/apache/openwhisk/blob/master/docs/apigateway.md\n[5]: https://github.com/embeddedkafka/embedded-kafka\n[6]: https://github.com/obsidiandynamics/kafdrop\n[7]: https://github.com/apache/openwhisk/blob/master/docs/metrics.md#user-specific-metrics\n[8]: https://github.com/apache/openwhisk/blob/master/core/monitoring/user-events/README.md\n[9]: https://kind.sigs.k8s.io/\n[10]: https://kind.sigs.k8s.io/docs/user/quick-start/\n"
  },
  {
    "path": "core/standalone/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport org.apache.tools.ant.taskdefs.condition.Os\n\nplugins {\n    id 'maven-publish'\n    id 'org.scoverage'\n    id 'org.springframework.boot' version '2.7.18'\n    id 'scala'\n    id 'com.gorylenko.gradle-git-properties' version '2.4.2'\n}\n\next.dockerImageName = 'standalone'\next.dockerBuildArgs = [ \"OPENWHISK_JAR=build/libs/openwhisk-standalone-${version}.jar\"]\napply from: '../../gradle/docker.gradle'\ndistDocker.dependsOn 'bootJar'\n\nproject.archivesBaseName = \"openwhisk-standalone\"\n\nscoverage {\n    scoverageVersion.set(\"${gradle.scala.scoverageVersion}\")\n    scoverageScalaVersion.set(\"${gradle.scala.scoverageScalaVersion}\")\n}\n\ntask copySwagger(type: Copy) {\n    def version = \"3.6.0\"\n    mkdir(\"$buildDir/tmp/swagger\")\n    def destFile = file(\"$buildDir/tmp/swagger/swagger-ui.tar\")\n    def uiDir = file(\"$buildDir/tmp/swagger/swagger-ui\")\n    if (!destFile.exists()) {\n        ant.get(src: \"https://github.com/swagger-api/swagger-ui/archive/v${version}.tar.gz\", dest: destFile)\n    }\n    from(tarTree(resources.gzip(destFile))){\n        include(\"swagger-ui-${version}/dist/**\")\n    }\n    into(uiDir)\n    includeEmptyDirs = false\n    project.ext.swaggerUiDir = file(\"$buildDir/tmp/swagger/swagger-ui/swagger-ui-${version}/dist\")\n}\n\ndef apiGwActions = ['createApi', \"deleteApi\", \"getApi\"]\n\ntask copyGWActions() {\n    doLast {\n        def routeMgmtDir = new File(project.projectDir, \"../routemgmt\")\n        def commonDir = new File(routeMgmtDir, \"common\")\n        def routeBuildDir = mkdir(\"$buildDir/tmp/routemgmt\")\n        apiGwActions.each { actionName ->\n            def zipFileName = actionName + \".zip\"\n            def actionDir = new File(routeMgmtDir, actionName)\n            def zipFile = new File(routeBuildDir, zipFileName)\n            def npmCommand = (Os.isFamily(Os.FAMILY_WINDOWS)) ? \"npm.cmd\" : \"npm\"\n            if (!zipFile.exists()) {\n                ant.exec(dir:actionDir, executable:npmCommand, failonerror:true){\n                    arg(line:\"install\")\n                }\n                ant.zip(destfile:zipFile){\n                    fileset(dir:actionDir){\n                        exclude(name:zipFileName)\n                        //Somehow if in zip we add same file twice it causes issues\n                        //Its possible that installRouteMgmt.sh has been invoked which would\n                        //have copied the common files to each action dir.\n                        //Exclude such files to zipped twice\n                        commonDir.listFiles().each {f ->\n                            exclude(name:f.name)\n                        }\n                    }\n                    fileset(dir:commonDir)\n                }\n                logger.info(\"Create action zip $zipFileName\")\n            }\n        }\n    }\n}\n\ntask copyGrafanaConfig() {\n    doLast {\n        def grafanaDir = new File(project(':core:monitoring:user-events').projectDir, \"compose/grafana\")\n        def grafanaBuildDir = mkdir(\"$buildDir/tmp/grafana\")\n        def zipFileName = \"grafana-config.zip\"\n        def zipFile = new File(grafanaBuildDir, zipFileName)\n        if (!zipFile.exists()) {\n            ant.zip(destfile:zipFile){\n                fileset(dir:grafanaDir)\n            }\n            logger.info(\"Created grafana config zip $zipFileName\")\n        }\n    }\n}\n\nprocessResources.dependsOn copySwagger\nprocessResources.dependsOn copyGWActions\nprocessResources.dependsOn copyGrafanaConfig\n\nprocessResources {\n    from(new File(project.projectDir, \"../../ansible/files/runtimes.json\")) {\n        into(\".\")\n    }\n    from(new File(project.projectDir, \"../../ansible/files\")) {\n        include \"*.json\"\n        into(\"couch\")\n    }\n    from(file(\"$buildDir/tmp/grafana/grafana-config.zip\")){\n        into(\".\")\n    }\n    from(new File(project(':core:monitoring:user-events').projectDir, \"compose/prometheus/prometheus.yml\")){\n        into(\".\")\n    }\n    //Implement the logic present in controller Docker file\n    from(project.swaggerUiDir) {\n        include \"index.html\"\n        filter {\n            it.replace(\"http://petstore.swagger.io/v2/swagger.json\", \"/api/v1/api-docs\")\n        }\n        into(\"swagger-ui\")\n    }\n    from(project.swaggerUiDir) {\n        exclude \"index.html\"\n        into(\"swagger-ui\")\n    }\n    apiGwActions.each { action ->\n        from(file(\"$buildDir/tmp/routemgmt/${action}.zip\")){\n            into(\".\")\n        }\n    }\n}\n\ntask copyBootJarToBin(type:Copy){\n    from (\"${buildDir}/libs\")\n    into file(\"${project.rootProject.projectDir}/bin\")\n    rename(\"${project.archivesBaseName}-${version}.jar\", \"${project.archivesBaseName}.jar\")\n}\n\nbootJar {\n    mainClass = 'org.apache.openwhisk.standalone.StandaloneOpenWhisk'\n    finalizedBy copyBootJarToBin\n}\n\n// Support building standalone from source release (no .git)\ngitProperties {\n    failOnNoGitDirectory = false\n}\n\n// Gradle boot disables the default jar task. So need to now make\n// install task depend on bootJar such that it finds the required jar file\n// https://github.com/spring-projects/spring-boot/issues/13187\n// install.dependsOn(bootJar)\n\ndependencies {\n    implementation \"org.scala-lang:scala-library:${gradle.scala.version}\"\n    implementation project(':common:scala')\n    implementation project(':core:controller')\n    implementation project(':core:invoker')\n    implementation project(':tools:admin')\n    implementation \"org.rogach:scallop_${gradle.scala.depVersion}:3.3.2\"\n\n    implementation \"io.github.embeddedkafka:embedded-kafka_${gradle.scala.depVersion}:3.9.1\"\n    constraints {\n        implementation(\"io.github.embeddedkafka:embedded-kafka_${gradle.scala.depVersion}:3.9.1\")\n        implementation(\"org.apache.kafka:kafka-clients:3.9.1\")\n    }\n    implementation (\"org.apache.zookeeper:zookeeper:3.9.3\") {\n        exclude group: 'org.slf4j'\n        exclude group: 'log4j'\n        exclude group: 'jline'\n    }\n    implementation \"org.scala-lang:scala-reflect:${gradle.scala.version}\"\n    implementation \"org.apache.pekko:pekko-http-cors_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n\n    testImplementation \"junit:junit:4.11\"\n    testImplementation \"org.scalatest:scalatest_${gradle.scala.depVersion}:3.2.14\"\n    testImplementation \"org.scalatestplus:junit-4-13_${gradle.scala.depVersion}:3.2.14.0\"\n}\n\ngradle.projectsEvaluated {\n    tasks.withType(Test) {\n        testLogging {\n            events \"passed\", \"skipped\", \"failed\"\n            showStandardStreams = true\n            exceptionFormat = 'full'\n        }\n    }\n}\n\ntask copyRuntimeLibs(type:Copy) {\n    from configurations.runtimeClasspath\n    into \"$buildDir/dependency-libs\"\n}\n\ntasks.named(\"copyBootJarToBin\") {\n    dependsOn tasks.named(\"jar\")\n}\n"
  },
  {
    "path": "core/standalone/src/main/resources/logback-standalone.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  ~ Licensed to the Apache Software Foundation (ASF) under one or more\n  ~ contributor license agreements.  See the NOTICE file distributed with\n  ~ this work for additional information regarding copyright ownership.\n  ~ The ASF licenses this file to You under the Apache License, Version 2.0\n  ~ (the \"License\"); you may not use this file except in compliance with\n  ~ the License.  You may obtain a copy of the License at\n  ~\n  ~     http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<configuration>\n    <jmxConfigurator></jmxConfigurator>\n    <appender name=\"console\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>[%d{yyyy-MM-dd'T'HH:mm:ss.SSS'Z'}] %highlight([%p]) %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <!-- Apache HttpClient -->\n    <logger name=\"org.apache.http\" level=\"ERROR\" />\n\n    <!-- Kafka -->\n    <logger name=\"org.apache.kafka\" level=\"ERROR\" />\n    <logger name=\"org.apache.zookeeper\" level=\"ERROR\" />\n    <logger name=\"kafka\" level=\"ERROR\" />\n\n    <root level=\"${logback.log.level:-INFO}\">\n        <appender-ref ref=\"console\" />\n    </root>\n</configuration>"
  },
  {
    "path": "core/standalone/src/main/resources/playground/actions/playground-delete.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nvar openwhisk = require('openwhisk');\n\n// Deletes a deployed action (named according to the playgroundId and action name) if the action exists.\nfunction main(outerParam) {\n    let param = JSON.parse(outerParam['__ow_body'])\n    let playgroundId = param['playgroundId']\n    let actionName = param['actionName']\n    let wsk = openwhisk({ignore_certs: outerParam.__ignore_certs}) // ignores self-signed certs, necessary in some deployments\n    let fullName = 'user' + playgroundId + '/' + actionName\n    console.log(\"deleting action\", fullName)\n    return wsk.actions.delete(fullName).then(result => {\n      console.log('deleted user action')\n        return result\n    }).catch(err => {\n      console.error('action did not exist or error occurred', err)\n    })\n}\n"
  },
  {
    "path": "core/standalone/src/main/resources/playground/actions/playground-fetch.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nvar openwhisk = require('openwhisk');\n\n// Returns the code of a deployed action named according to the playgroundId action name\nfunction main(outerParam) {\n    let param = JSON.parse(outerParam['__ow_body'])\n    let playgroundId = param['playgroundId']\n    let actionName = param['actionName']\n    let wsk = openwhisk({ignore_certs: outerParam.__ignore_certs}) // ignores self-signed certs, necessary in some deployments\n    let fullName = 'user' + playgroundId + '/' + actionName\n    console.log(\"fetching action\", fullName)\n    return wsk.actions.get(fullName).then(result => {\n      console.log('got user action')\n        return result\n    }).catch(err => {\n        console.error('error retrieving action', err)\n    })\n}\n"
  },
  {
    "path": "core/standalone/src/main/resources/playground/actions/playground-run.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nvar openwhisk = require('openwhisk');\n\n// Deploys code as an action and optionally runs it.\n// The input parameters are\n//    code -- the code to run\n//    saveOnly -- if present and true, the action is not run but only deployed\n//    web-export -- if present and true, the action is deployed as a web action (annotated with web-export=true).  Implies saveOnly.\n//    params -- parameters to pass to the code when running it (ignored if saveOnly is present or implied)\n//    playgroundId -- the identity of the browser instance submitting the code (functions as a kind of user id but not enduring\n//          or authenticated).  Becomes part of the name of the action.\n//    action -- the name of the action as assigned by the user or one of the default sample names; combines with playgroundId to form\n//          the action name as viewed by OpenWhisk\n//    runtime -- the whisk runtime ('kind') value to use in running or saving the action.\nfunction main(outerParam) {\n    let t0 = new Date().getTime()\n    //console.log('outerParam: ', outerParam)\n    // Get parameters\n    let param = JSON.parse(outerParam['__ow_body'])\n    let saveOnly = param['saveOnly']\n    let webExport = param['web-export']\n    let code = param['code']\n    let codeParams = param['params']\n    let playgroundId = param['playgroundId']\n    let action = param['actionName']\n    let runtime = param['runtime']\n    // Deploy the action.  The action is left deployed after running it, which allows playground-fetch to fetch the code back\n    // for a later edit session.  In a saveOnly or web-export scenario, the code is not even run after that.\n    let wsk = openwhisk({ignore_certs: outerParam.__ignore_certs}) // ignores self-signed certs, necessary in some deployments\n    let actionName = 'user' + playgroundId + '/' + action\n    let annotations = {\"web-export\": webExport ? true : false }\n    var deployParams = {name: actionName, action: code, kind: runtime, annotations: annotations}\n    return wsk.actions.update(deployParams).then(uresult => {\n        // Unless saveOnly, run the action once deployed.\n        let t1 = new Date().getTime()\n        console.log('made user action')\n        if (saveOnly || webExport) {\n            return { saved: true }\n        } else {\n            return wsk.actions.invoke({ actionName: actionName, blocking: true, params: codeParams }).then(aresult => {\n                // Return the result\n                let t2 = new Date().getTime()\n                console.log('aresult: ', aresult)\n                let response = aresult['response']\n                let result = response['result']\n                return { param: param, result: result, deployTime: t1 - t0, runTime: t2 - t1 }\n            }).catch(err => {\n                console.error('error invoking action', err)\n                return {error: err}\n            })\n        }\n    }).catch(err => {\n        console.error('error creating action', err)\n        return {error: err}\n    })\n}\n"
  },
  {
    "path": "core/standalone/src/main/resources/playground/actions/playground-userpackage.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nvar openwhisk = require('openwhisk');\n\n// Returns the package structure for a given user, creating it if it doesn't exist.\n// Used to initialize playground state when an existing user loads the playground page and also to begin the\n// process with an empty package for a new user.\n// This code also maintains a lastSession date as a package annotation.  This denotes the last time\n// this user opened the playground and can be used to expire the package.\nfunction main(outerParam) {\n    let param = JSON.parse(outerParam['__ow_body'])\n    let playgroundId = param['playgroundId']\n    let wsk = openwhisk({ignore_certs: outerParam.__ignore_certs}) // ignores self-signed certs, necessary in some deployments\n    let name = \"user\" + playgroundId\n    let ts = new Date().toISOString()\n    let tsAnnotation = { key: \"lastSession\", value: ts }\n    return wsk.packages.get(name).then(result => {\n       console.log('found existing package', result)\n       let annotations = result.annotations\n       annotations.push(tsAnnotation)\n       return wsk.packages.update({\"name\": name, \"package\": {annotations: annotations}}).then(_ => {\n         // Return original response, which has the old timestamp.  Client does not use the timestamp in the response.\n         // The response from the update does not include the package list.\n         return result\n       }).catch(err => {\n         console.log(\"could not add lastSession annotation (proceeding)\", err)\n         return result // even if not updated\n       })\n    }).catch(err => {\n      console.log('package does not exist or other error')\n      if (err.statusCode === 404) {\n        // Simple not found error.  Just create the package\n        return wsk.packages.create({\"name\": name, \"package\": { annotations: [ tsAnnotation ]}}).then(result => {\n          console.log('created package', result)\n          return result\n        }).catch(err => {\n          console.error('error creating package', err)\n          return { error: err }\n        })\n      } else {\n        console.error('unrecoverable error retrieving package', err)\n        return { error: err }\n      }\n    })\n}\n"
  },
  {
    "path": "core/standalone/src/main/resources/playground/ui/index.html",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<!DOCTYPE html>\n<html>\n\n<head>\n<meta charset=\"utf-8\"/>\n<title>Function Playground</title>\n\n<!-- for the ACE editor component -->\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.2/ace.js\" type=\"text/javascript\" charset=\"utf-8\"></script>\n\n<!-- for the Google material UI icons -->\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/icon?family=Material+Icons\">\n\n<!-- begin - to enable panel resize -->\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js\"></script>\n<script src=\"https://rawgit.com/RickStrahl/jquery-resizable/master/src/jquery-resizable.js\"></script>\n\n<script>\n$(document).ready(function() {\n $(\"#panel-left\").resizable({\n    handleSelector: \".splitter-vertical\",\n    resizeHeight: false\n  });\n});\n</script>\n<!-- end - to enable panel resize -->\n\n<!-- The js needs to go after jquery is loaded because it uses jquery to run the init block after DOM loading.\n    The css is placed next to it to make the inlining easier.\n -->\n<!--Start Inlining-->\n<link rel=\"stylesheet\" type=\"text/css\" href=\"playground.css\">\n<script src=\"playgroundFunctions.js\"></script>\n<!--End Inlining-->\n\n<!-- OpenWhisk and Function Playground icons (svgomg was used to compact) -->\n<svg aria-hidden=\"true\" focusable=\"false\" style=\"display:none\" xmlns=\"http://www.w3.org/2000/svg\">\n  <symbol id=\"svg-logo-icon\" viewBox=\"0 0 32 32\">\n    <image xlink:href=\"https://openwhisk.apache.org/images/logo/apache-openwhisk-logo-only.png\" height=\"32\" width=\"32\" />\n  </symbol>\n  <symbol id=\"svg-logo-text\" viewBox=\"0 0 195 32\">\n    <text x=\"0.259234\" y=\"29.829633\" fill=\"#808080\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"10.667px\" font-weight=\"bold\" stroke=\"#000000\" stroke-width=\"0\" xml:space=\"preserve\">OpenWhisk</text>\n    <text transform=\"matrix(.89868 0 0 1 -.044817 0)\" x=\"-0.480163\" y=\"16.794799\" fill=\"#cccccc\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"18.667px\" font-weight=\"bold\" stroke-width=\"1px\" xml:space=\"preserve\">\n     <tspan x=\"-0.480163\" y=\"16.794799\" fill=\"#cccccc\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"18.667px\" font-weight=\"bold\">Function Playground</tspan>\n    </text>\n   </symbol>\n</svg>\n\n</head>\n\n<body id=\"body\" class=\"body-container\">\n  <div class=\"navbar\">\n    <div class=\"nav-item\">\n      <svg aria-hidden=\"true\" focusable=\"false\" class=\"logo-icon\"><use xlink:href=\"#svg-logo-icon\"/></svg>\n      <svg id=\"logo-text\" aria-hidden=\"true\" focusable=\"false\" class=\"logo-text nav-right-spacer\"><use xlink:href=\"#svg-logo-text\"/></svg>\n    </div>\n    <div class=\"nav-item\">\n       <button id=\"run\" class=\"nav-button\" type=\"button\" onclick=\"runClicked()\">\n         <i style=\"font-size:12pt !important;\" class=\"material-icons icon-size\">play_arrow</i>Run\n       </button>\n    </div>\n    <div class=\"nav-item\">\n       <button id=\"publish\" class=\"nav-button nav-right-spacer\" type=\"button\" onclick=\"publishClicked()\">\n         <i class=\"material-icons icon-size icon-extra-margin\">cloud_upload</i>Publish\n       </button>\n    </div>\n    <div class=\"nav-item\">\n      <select id=\"languageSelector\" class=\"nav-select\" onchange=\"languageChanged()\">\n        <option value=\"JavaScript\" selected=\"selected\">JavaScript</option>\n        <option value=\"Python\">Python</option>\n      </select>\n    </div>\n    <div class=\"nav-item\">\n      <select id=\"actionSelector\" class=\"nav-select\" onchange=\"actionChanged()\" select=\"\">\n        <option value=\"sampleJavaScript\" selected=\"selected\">sampleJavaScript</option>\n        <option value=\"--New Action--\">--New Action--</option>\n        <option value=\"--Rename--\">--Rename--</option>\n      </select>\n      <input id=\"nameInput\" class=\"nav-input\" onchange=\"processNewName()\" type=\"text\">\n    </div>\n    <div class=\"nav-item-last\">\n      <button id=\"theme\" class=\"nav-button\" type=\"button\" onclick=\"themeClicked()\">\n        <i class=\"material-icons icon-size icon-extra-margin\">web</i>\n        <span id=\"themeName\">Light</span>\n      </button>\n      </div>\n  </div>\n  <div class=\"central-container\">\n    <div id=\"panel-left\" class=\"panel-left\">\n      <div class=\"panel-header\">\n        <i style=\"margin-left: 4px;\" class=\"material-icons icon-size icon-extra-margin\">cloud_queue</i>\n        URL: <span id=\"urlText\">[editable, private]</span>\n      </div>\n      <div id=\"editor\" ace-editor [(text)]=\"text\"></div>\n   </div>\n    <div class=\"splitter-vertical\"></div>\n    <div class=\"panel-right\">\n      <div class=\"panel-header\">\n        <i class=\"material-icons icon-size icon-extra-margin\">input</i>INPUT PARAMETERS\n      </div>\n      <div class=\"panel-right-top\">\n        <textarea id=\"input\" spellcheck=\"false\" class=\"panel-right-input\">{ \"name\" : \"openwhisk\" }</textarea>\n      </div>\n      <div class=\"panel-header\">\n        <i class=\"material-icons icon-size icon-extra-margin\">access_time</i>EXECUTION TIME\n      </div>\n      <div class=\"panel-right-mid\">\n        <div id=\"timingText\" class=\"panel-right-box\"></div>\n      </div>\n      <div class=\"panel-header\">\n        <i class=\"material-icons icon-size icon-extra-margin\">done</i>OUTPUT\n      </div>\n      <div class=\"panel-right-bottom\">\n        <div id=\"resultText\" class=\"panel-right-box\"></div>\n      </div>\n    </div>\n  </div>\n</body>\n\n</html>\n"
  },
  {
    "path": "core/standalone/src/main/resources/playground/ui/playground.css",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nhtml, body {\n  height: 100%;\n  margin: 0px;\n}\n\n* {\n  box-sizing: border-box; /* include the border and padding in width / height calcuations */\n}\n\n#editor { \n  flex: 1 1 auto;\n}\n\n.body-container {\n  font-family: Arial, Helvetica, sans-serif;\n  margin: 0;\n  display: flex;\n  flex-direction: column;\n  background-color: #26282C;\n}\n\n.navbar {\n  flex: 0 0 auto;\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  margin-top: 8px;\n  margin-bottom: 8px;\n}\n\n.nav-item {\n  display: flex;\n  flex: 0 0 auto;\n  border-right: 12px solid transparent;\n}\n\n.nav-item-center {\n  display: flex;\n  align-items: center;\n}\n\n.nav-item-last {\n  flex: 1 0 auto;\n  text-align:right;\n  margin-right: 4px;\n}\n\n.logo-icon {\n  width: 32px;\n  height: 32px;\n  margin-left: 8px;\n  margin-right: 6px;\n}\n\n.logo-text {\n  width: 195px;\n  height: 32px;\n}\n\n.nav-button {\n  padding-top: 8px;\n  padding-bottom: 8px;\n  padding-left: 15px;\n  padding-right: 15px;\n  border: 2px solid #424446;\n  border-radius: 8px;\n  background-color: #26282C;\n  color: white;\n  text-align: center; \n  text-decoration: none;\n  display: inline-block; \n  font-size: 12pt; \n  cursor: pointer;\n}\n\n.icon-size {\n  font-size:12pt !important;\n  position: relative;\n  top: 2px;\n  margin-right: 4px;\n}\n\n.icon-extra-margin {\n  margin-right: 8px;\n}\n\n.nav-select {\n  color: white;\n  border: 2px solid #424446;\n  border-radius: 8px;\n  background-color: #26282C;\n  padding-top: 8px;\n  padding-bottom: 8px;\n  padding-left: 20px;\n  padding-right: 20px;\n  text-align: center; \n  text-decoration: none;\n  display: inline-block; \n  font-size: 10pt; \n  cursor: pointer;\n}\n\n.nav-input {\n  padding-top: 5px;\n  padding-bottom: 5px;\n  color: white;\n  background-color: black;\n  margin-left: 6px;\n  display: none;\n}\n\n.nav-right-spacer {\n  margin-right: 30px;\n}\n\n.nav-label {\n  color:#C0C0C0;\n}\n\n.central-container {\n  flex: 1 1 auto;\n  display: flex; /* make this a flex container */\n  /* by default flex-direction is row */\n  /* by default, elements will stretch vertically to the full height of the container */\n}\n\n.panel-left {\n  width: 65%;\n  flex: 0 1 auto;\n  min-height: 160px;\n  min-width: 160px;\n  display: flex;\n  flex-direction: column;\n}\n\n.splitter-vertical {\n  flex: 0 0 auto;\n  width: 12px;\n  min-width: 12px;\n  cursor: col-resize;\n  background-color: #26282C;\n}\n\n.panel-right {\n  flex: 1 1 auto;\n  min-width: 160px;\n  display: flex;\n  flex-direction: column;\n}\n\n.panel-right-box {\n  flex: 1 1 auto;\n  font-family: \"Courier New\", Courier, monospace;\n  font-size: 12pt;\n  padding: 4px;\n  color: white;\n  background-color: black;\n  margin: 0px;\n}\n\n.panel-right-input {\n  flex: 1 1 auto;\n  font-family: \"Courier New\", Courier, monospace;\n  font-size: 12pt;\n  padding: 4px;\n  color: white;\n  background-color: black;\n  border:solid 1px grey;\n  margin: 0px;\n  resize:none;\n}\n\n.panel-header {\n  padding-top: 4px;\n  padding-bottom: 4px;\n  font-size: 10pt; \n  font-weight: bold;\n  background-color: #202020;\n  color: #9098A0;\n}\n\n.panel-right-top {\n  flex: 3 1 auto; /* grow(relative size) shrink basis */\n  display: flex;\n  flex-direction: column;\n}\n\n.panel-right-mid {\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n}\n\n.panel-right-bottom {\n  flex: 3 1 auto;\n  display: flex;\n  flex-direction: column;\n}\n\n/* for smaller screens - get rid of text logo and shrink button margins */\n@media screen and (min-width: 0px) and (max-width: 1000px) {\n  #logo-text {\n    display: none;\n  }\n  .nav-button {\n    padding-left: 4px;\n    padding-right: 4px;\n    margin-left: 0px;\n    margin-right: 0px;\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/main/resources/playground/ui/playgroundFunctions.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n$(document).ready(function(){\n  // This is the location of the supporting API\n  // The host value may get replaced in PlaygroundLauncher to a specific host\n  window.APIHOST='http://localhost:3233'\n\n  // To install in a different namespace, change this value\n  window.PLAYGROUND='whisk.system'\n\n  // Keys for cookies\n  window.colorKey = 'colorId'\n  window.languageKey = 'language'\n  window.playgroundIdKey = 'playgroundId'\n  window.actionKey = 'actionName'\n\n  // Initialize GUI elements\n  window.editor = initializeEditor()\n  window.colorSetting = initializeColor()\n\n  // The language table (a JS object acting as an associative array)\n  // Maps from language symbol to structure (1) repeating the symbol as 'name', (2) the editor mode,\n  // (3) the whisk runtime 'kind' to use for the language, and (4) the starting example code for that language.\n  window.languages = {\n    JavaScript: {\n        name: \"JavaScript\",\n        editMode: \"ace/mode/javascript\",\n        kind: \"nodejs:default\",\n        example:`function main(args) {\n    let name = args.name || 'stranger'\n    let greeting = 'Hello ' + name + '!'\n    console.log(greeting)\n    return {\"body\":  greeting}\n}`\n    },\n\n    Python: {\n        name: \"Python\",\n        editMode: \"ace/mode/python\",\n        kind: \"python:default\",\n        example: `def main(args):\n    if 'name' in args:\n        name = args['name']\n    else:\n        name = \"stranger\"\n    greeting = \"Hello \" + name + \"!\"\n    print(greeting)\n    return {\"body\": greeting}\n`\n    },\n\n    Swift: {\n        name: \"Swift\",\n        editMode: \"ace/mode/swift\",\n        kind: \"swift:default\",\n        example:`func main(args: [String:Any]) -> [String:Any] {\n         if let name = args[\"name\"] as? String {\n        let greeting = \"Hello \\\\(name)!\"\n        print(greeting)\n        return [ \"body\" : greeting ]\n    } else {\n        let greeting = \"Hello stranger!\"\n        print(greeting)\n        return [ \"body\" : greeting ]\n    }\n}`\n    },\n\n    Go: {\n        name: 'Go',\n        editMode: 'ace/mode/go',\n        kind: `go:default`,\n        example: `package main\n\nfunc Main(args map[string]interface{}) map[string]interface{} {\n  name, ok := args[\"name\"].(string)\n  if !ok {\n    name = \"stranger\"\n  }\n  msg := make(map[string]interface{})\n  msg[\"body\"] = \"Hello, \" + name + \"!\"\n  return msg\n}`\n    },\n\n    PHP: {\n        name: 'PHP',\n        editMode: 'ace/mode/php',\n        kind: `php:default`,\n        example: `<?php\nfunction main(array $args) : array {\n    $name = $args[\"name\"] ?? \"stranger\";\n    $greeting = \"Hello $name!\";\n    echo $greeting;\n    return [\"body\" => $greeting];\n}`\n    }\n  }\n\n  // Other initialization\n  window.playgroundId = initializePlaygroundId()\n  window.EditSession = require(\"ace/edit_session\").EditSession  // Per ACE doc\n  window.activeSessions = []  // Contains triples {actionName, EditSession, webbiness} for actions visited in this browser session\n  window.editorContentsChanged = false  // A 'dirty' flag consulted as part of autosave logic\n  window.language = initializeLanguage()  // Requires languages table to exist\n  window.actionList = []   // Populated asynchronously by initializeUserPackage.  Contains pairs {actionName, actionKind}\n  window.currentAction = null // Name of the action displayed in the editor and actionSelector.  Initialized by initializeActionSelector.\n  window.entryFollowup = null // Function to execute when name entry completes (for renameAction and startNewAction).  Null except during name entry.\n  document.onkeydown = detectEscapeKey // Examine key presses to see if they indicate a desire to cancel name input mode\n\n  initializeUserPackage().then(initializeActionSelector).then(startAutosave)\n});\n\n// Start autosave polling\nfunction startAutosave() {\n  window.setInterval(maybeSave, 15 * 1000)\n}\n\n// Initialize the playgroundId\nfunction initializePlaygroundId() {\n  let playgroundId = getCookie(window.playgroundIdKey)\n  if (playgroundId == \"\") {\n    playgroundId = (new Date().getTime()) % 1000000\n    console.log('New playgroundId: ', playgroundId)\n  } else {\n    console.log('Existing playgroundId: ', playgroundId)\n  }\n  setCookie(window.playgroundIdKey, playgroundId) // regardless of whether it was set before; refreshes expiration\n  return playgroundId\n}\n\n// Initialize the actionList to reflect the user's package structure stored on the server, perhaps creating a new package for a new user\n// with no actions.  Returns a promise.  Initialization code dependent on the action list should be in the promise chain.\nfunction initializeUserPackage() {\n  console.log(\"Initializing user\", window.playgroundId)\n  return makeOpenWhiskRequest('playground-userpackage.json', { playgroundId: window.playgroundId }).then(result => {\n    console.log(\"userpackage raw response:\", result)\n    let userPackage = JSON.parse(result)\n    if (userPackage && userPackage.actions && Array.isArray(userPackage.actions)) {\n      for (action of userPackage.actions) {\n        let kind = getAnnotation(action, \"exec\")\n        window.actionList.push({ name: action.name, kind: kind } )\n      }\n    }\n    return window.actionList   // For definiteness, to carry on the promise chain.  actionList is also global.\n  }).catch(err => {\n    console.error(\"Error getting user package.\", err)\n  })\n}\n\n// Initialize the actions in the action selector and select one (also assigning currentAction) based on a user cookie.\n// Assumes the 'language' global variable is initialized.  Only actions in that language are listed.\n// If the cookie is not set, or it denotes an action for a non-selected language, we arbitrarily select the first action\n// of the selected language and also put it into the cookie.  End by calling 'imposeAction' to initialize the editor session\n// for the action, returning the result thereof which is a Promise.  Editor code may be filled in asynchronously.\nfunction initializeActionSelector(actionList) {\n  const selector = elem(\"actionSelector\")\n  // Determine the list of action names that should be used.\n  // Start with those that can be read from the user's package (pre-existing).\n  // Add a sample for the current language iff the user has no actions for that language.\n  let actions = actionList.filter(action => matchesLanguage(action))\n  console.log(\"read\", actions.length, \"actions from user package\")\n  if (actions.length == 0) {\n    console.log(\"adding sample for\", window.language.name)\n    actions.push({ name: \"sample\" + window.language.name, kind: window.language.kind} )\n  }\n  // Place the action names in the selector's options\n  selector.options.length = 0\n  for (action of actions) {\n    console.log(\"adding action to selector\", action.name)\n    selector.options[selector.options.length] = new Option(action.name, action.name)\n  }\n  // Add other capabilities to the action list.\n  // Add --New Action-- iff the user is within his quota.  Add --Delete-- iff there is more than one action.\n  // Add --Rename-- unconditionally.  However, --Rename-- and --Delete-- are also enabled/disabled as part of\n  // the imposeWebbiness function (when the action isn't editable it seems illogical that you can rename and delete it)\n  if (actions.length < 10) { // quota is arbitrary\n    let other = \"--New Action--\"\n    console.log(\"adding capability\", other)\n    selector.options[selector.options.length] = new Option(other, other)\n  }\n  if (actions.length > 1) {\n    let other = \"--Delete--\"\n    console.log(\"adding capability\", other)\n    selector.options[selector.options.length] = new Option(other, other)\n  }\n  let other = \"--Rename--\"\n  console.log(\"adding capability\", other)\n  selector.options[selector.options.length] = new Option(other, other)\n  // Now select the action according to the user's cookie (if present and applicable) else arbitrarily choose\n  // the first (or only) list element.  The list has at least one action at this point.\n  const cookieVal = getCookie(window.actionKey)\n  window.currentAction = (cookieVal != \"\" && matchesLanguageByName(cookieVal)) ? cookieVal : actions[0].name\n  selector.value = window.currentAction\n  setCookie(window.actionKey, window.currentAction)\n  return imposeAction(window.currentAction)\n}\n\n// Initialize the editor\nfunction initializeEditor() {\n    editor = ace.edit(\"editor\");\n  editor.setTheme(\"ace/theme/monokai\");\n  editor.setShowPrintMargin(false);\n  elem('editor').style.fontSize='12pt';\n  return editor\n}\n\n// Initialize the color theme\nfunction initializeColor() {\n  let color = getCookie(window.colorKey)\n  if (color == \"\") {\n    color = \"dark\"\n  }\n  imposeColor(color)\n  return color\n}\n\n// Initialize the language\nfunction initializeLanguage() {\n  // First initialize the options of the language selector from the language table\n  var selector = elem(\"languageSelector\")\n  selector.options.length = 0 // probably unneeded but just in case this gets done more than once\n  for (member in window.languages) {\n    let languageName = window.languages[member].name\n    console.log(\"Adding language \" + languageName + \" to selector\")\n    selector.options[selector.options.length] = new Option(languageName, languageName)\n  }\n  console.log(\"Selector now has \" + selector.options.length + \" choices\")\n  // Retrieve the language choice from the cookie or set to default\n  var language = window.languages.JavaScript // Default\n  let languageName = getCookie(window.languageKey)\n  if (languageName != \"\") {\n    language = window.languages[languageName]\n    console.log(\"Language \" + languageName + \" was retrieved from the cookie\")\n  } else {\n    console.log(\"Language defaulted to \" + language.name)\n    setCookie(window.languageKey, language.name)\n  }\n  // Set the language into the selector\n  selector.value = language.name\n  return language\n}\n\n// Examine key presses looking for esc\nfunction detectEscapeKey(evt) {\n  evt = evt || window.event;\n  var isEscape = false;\n  if (\"key\" in evt) {\n    isEscape = (evt.key == \"Escape\" || evt.key == \"Esc\");\n  } else {\n    isEscape = (evt.keyCode == 27);\n  }\n  if (isEscape && window.entryFollowup != null) {\n    console.log(\"Cancel detected via esc key\")\n    endNameEntry()\n  }\n}\n\n// Test whether an action (from the action list) matches the current language (the action {name, kind} pair is the argument)\nfunction matchesLanguage(action) {\n  console.log(\"matching\", action.name, \"for kind\", window.language.kind)\n  let matched = action.kind === window.language.kind\n  console.log(\"matched\", matched)\n  return matched\n}\n\n// Test whether an action matches the current language (language name given)\n// Answers false if the action isn't found.\nfunction matchesLanguageByName(actionName) {\n  let action = getAction(actionName)\n  return action ? matchesLanguage(action) : false\n}\n\n// Lookup an action by name in the actionList.\nfunction getAction(actionName) {\n  let index = indexOfAction(actionName)\n  if (index < 0) {\n    return undefined\n  }\n  return window.actionList[index]\n}\n\n// Find the index of an action name in the action list\nfunction indexOfAction(actionName) {\n  for (i = 0; i < window.actionList.length; i++) {\n    if (window.actionList[i].name == actionName) {\n      return i\n    }\n  }\n  return -1\n}\n\n// Change the language in response to a change in the language selector\nfunction languageChanged() {\n  const newName = elem(\"languageSelector\").value\n  if (window.language.name == newName) {\n    // Avoid disruption if not really changed (not sure if this can actually happen but just in case)\n    return\n  }\n  maybeSave()   // Before language change: saves previous contents.  Save is asynchronous but racing with the\n  // following is ok because the asynchronous part of save follows the network send.  Once the network send\n  // has occurred, the local state is free to change (if the save fails there is no real recovery).\n  // Change the language global variable and reset the cookie\n  window.language = window.languages[newName]\n  setCookie(window.languageKey, newName)\n  // Redo action selector initialization.  This returns a promise but we need not hook it because\n  // we are running in response to a UI event and things can settle in any order.\n  initializeActionSelector(window.actionList)\n}\n\n// Change the selected action or process the special options (new/rename/delete) that are handled via that selector\nfunction actionChanged() {\n  let newAction = elem(\"actionSelector\").value\n  if (newAction == window.currentAction) {\n    return\n  } else if (newAction.startsWith(\"--\")) {\n    switch (newAction.charAt(2)) {\n    case 'N':\n      nameEntry(completeNewAction)\n      break\n    case 'R':\n      nameEntry(completeRename)\n      break\n    case 'D':\n      deleteAction()\n      break\n    }\n  } else {\n    maybeSave()   // Save previous contents. Save is asynchronous but racing with the following is ok because the\n    // asynchronous part of save follows the network send.  Once the network send has occurred, the local state is\n    // free to change (if the save fails there is no real recovery).\n    window.currentAction = newAction\n    setCookie(window.actionKey, window.currentAction)\n    imposeAction(window.currentAction)\n  }\n}\n\n// Start a name entry sequence (for rename or new action)\nfunction nameEntry(followup) {\n  window.entryFollowup = followup\n  const selector = elem(\"actionSelector\")\n  const entry = elem(\"nameInput\")\n  selector.style.display = \"none\"\n  entry.style.display = \"block\"\n  entry.value = \"\"\n  entry.focus()\n}\n\n// End the name entry phase, either after processing a valid name or after cancellation\nfunction endNameEntry() {\n  window.entryFollowup = null\n  const selector = elem(\"actionSelector\")\n  const entry = elem(\"nameInput\")\n  selector.style.display = \"block\"\n  entry.style.display = \"none\"\n  console.log(\"Name entry ending.  Setting selector to the correct action\", window.currentAction)\n  selector.value = window.currentAction\n}\n\n// Followup after user enters the name of a new action\nfunction completeNewAction(newName) {\n  window.actionList.push({ name: newName, kind: window.language.kind })\n  window.currentAction = newName\n  endNameEntry()\n  setCookie(window.actionKey, window.currentAction)\n  initializeActionSelector(window.actionList)\n}\n\n// Followup after user renames an existing action\nfunction completeRename(newName) {\n  let action = getAction(window.currentAction)\n  if (action) {\n    let oldName = window.currentAction\n    // Rename locally\n    action.name = newName\n    window.currentAction = newName\n    // Resave under the new name, delete old copy on success\n    let web = elem(\"publish\").value != 'Publish' // The presence of a Publish button means locally editable.\n    save(web).then(_ => deleteRemote(oldName))\n    // Restabilize action selector and editor\n    setCookie(window.actionKey, newName)\n    initializeActionSelector(window.actionList)\n  } else {\n    // Should not happen\n    console.log(window.currentAction, \"not found in action list\", window.actionList)\n  }\n  endNameEntry()\n}\n\n// Delete the current action\nfunction deleteAction() {\n  // Get index of current action in action list\n  let index = indexOfAction(window.currentAction)\n  if (index < 0) {\n    // Should not happen\n    console.log(\"current action not found in action list\", window.currentAction)\n    endNameEntry()\n    return\n  }\n  // Remove locally\n  window.actionList.splice(index, 1)\n  // Remove remotely\n  deleteRemote(window.currentAction)\n  // Restabilize the action selector, window.currentAction, and current cookie based on what's left in the list\n  initializeActionSelector(window.actionList)\n  // Don't end name entry until a new currentAction has been nominated\n  endNameEntry()\n}\n\n// Delete the remote copy of an action if present.  If absent, no error is indicated except on the console.  Local processing\n// proceeds in either case.\nfunction deleteRemote(actionName) {\n  return makeOpenWhiskRequest('playground-delete.json', { playgroundId: window.playgroundId, actionName: actionName }).then(result => {\n    console.log(\"deleted\", actionName)\n    console.log(\"full result\", result)\n  }).catch(err => {\n    console.log(\"not deleted (perhaps doesn't exist)\", actionName)\n    console.log(\"full error object\", err)\n  })\n}\n\n// Fetch code from a deployed action.   Returns a promise, for chaining purposes, but both the resolve and the reject path simply provide the\n// action name.  Code, if retrieved, is placed directly in the editor.  Failure to retrieve code is tolerated as a sometimes-expected condition.\nfunction getCode(actionName) {\n  return makeOpenWhiskRequest('playground-fetch.json', { playgroundId: window.playgroundId, actionName: actionName }).then(result => {\n       let response = JSON.parse(result)\n       console.log(\"getCode response\", response)\n       if ('exec' in response) {\n         console.log(\"Code retrieved from deployed action\")\n           let exec = response.exec\n           let code = exec.code\n           window.editor.setValue(code)\n           editorContentsChanged = false // Setting the editor contents will fire the change event but there is no need to re-save.\n       } else {\n         console.log(\"No deployed action, no code retrieved\")\n       }\n       let webbiness = isWeb(response)\n       imposeWebbiness(webbiness)\n       return actionName\n  }).catch(err => {\n    console.error(\"Error retrieving code\", err)\n    imposeWebbiness(false)\n    return actionName\n  })\n}\n\n// Determine if an action being fetched is a web action by examining its annotations.  The argument is the response to a wsk get operation on the\n// action.   If there are no annotations in the response, the answer is false.\nfunction isWeb(response) {\n  return getAnnotation(response, \"web-export\") === true // ensures boolean\n}\n\n// Get an annotation from an object that may or may not have an 'annotations' member (as whisk responses generally do).  Returns undefined if\n// (1) The 'annotations' member is absent.  (2) The 'annotations' member's members are not key value pairs.  (3) The 'annotations' member does not\n// contain a key value pair matching the requested annotation.  On a match, returns the value of the annotation.\nfunction getAnnotation(object, name) {\n  if ('annotations' in object && Array.isArray(object.annotations)) {\n    for (i = 0; i < object.annotations.length; i++) {\n      let member = object.annotations[i]\n      if (member.key === name) { // false if no key\n        return member.value // undefined if no value\n      }\n    }\n  }\n  return undefined\n}\n\n// Impose the local conventions for a currently published (web) action (argument is true) or a private (non-web) action (argument is false)\nfunction imposeWebbiness(isWeb) {\n  console.log(\"Webbiness being set to \" + isWeb)\n  let button = elem(\"publish\")\n  let urlText = elem(\"urlText\")\n  let actionSelector = elem(\"actionSelector\")\n  let mutableOptions = []  // For some reason, select.options doesn't support 'filter' (backlevel JS?)\n  for (i = 0; i < actionSelector.options.length; i++) {\n    let option = actionSelector.options[i]\n    if (option.value == \"--Rename--\" || option.value == \"--Delete--\") {\n      mutableOptions.push(option)\n    }\n  }\n  if (isWeb) {\n    button.innerHTML = '<i class=\"material-icons icon-size icon-extra-margin\">cloud_download</i>Edit'\n    setReadOnly(true)\n    const url = window.APIHOST + '/api/v1/web/' + window.PLAYGROUND + '/user' + window.playgroundId + '/' + window.currentAction\n    urlText.innerHTML = \"Readonly, public at <a style='text-decoration:none;color:#488' href='\" + url + \"'>\" + url + \"</a>\"\n    for (opt of mutableOptions) {\n      opt.disabled = true\n    }\n  } else {\n    button.innerHTML = '<i class=\"material-icons icon-size icon-extra-margin\">cloud_upload</i>Publish'\n    setReadOnly(false)\n    urlText.innerHTML = \"[ editable, private ]\"\n    for (opt of mutableOptions) {\n      opt.disabled = false\n    }\n  }\n  // Record the webbiness in the session record\n  getSession(window.currentAction).isWeb = isWeb\n  // Since this may be called as part of publish or edit, remove focus from the button\n  button.blur()\n}\n\n// Sets the readonly properties of the editor on or off.  A thorough job, including a proper visual indication,\n// requires taggling several properties\nfunction setReadOnly(on) {\n  window.editor.setOptions({readOnly: on, highlightActiveLine: !on, highlightGutterLine: !on});\n  window.editor.renderer.$cursorLayer.element.style.display = on ? \"none\" : \"\"\n  if (on) {\n    window.editor.clearSelection()\n  }\n}\n\n// Parse out a specific cookie by key\nfunction getCookie(key) {\n  let keyPrefix = key + \"=\";\n    let cookie = decodeURIComponent(document.cookie)\n    let parts = cookie.split(';');\n    for(var i = 0; i <parts.length; i++) {\n      let p = parts[i].trim()\n        if (p.startsWith(keyPrefix)) {\n          return p.substring(keyPrefix.length)\n        }\n    }\n    return \"\"\n}\n\n// Set a specific cookie by key (note that the document.cookie field has asymmetric behavior: on reference you get all the cookies but\n// on setting you provide a single cookie and it is added to the list)\nfunction setCookie(key, value) {\n  let age = String(60 * 60 * 24 * 7) // one week: kind of arbitrary\n  document.cookie = key + \"=\" + String(value) + \";max-age=\" + age\n}\n\n// Respond to click of the theme button\nfunction themeClicked() {\n    window.colorSetting = (window.colorSetting == \"dark\") ? \"light\" : \"dark\"\n    imposeColor(window.colorSetting)\n}\n\n// Impose a color scheme.  Called at startup and when theme is clicked\nfunction imposeColor(color) {\n    let $white = 'white';\n    let $black = 'black'\n    $reverseTheme = 'Light';\n    if (color == 'light') {\n      $white = 'black';\n      $black = 'white';\n      $reverseTheme = 'Dark';\n      editor.setTheme('ace/theme/xcode');\n    } else {\n      editor.setTheme('ace/theme/terminal');\n    }\n    elem('themeName').textContent = $reverseTheme;\n    elem('input').style.color = $white;\n    elem('input').style.background = $black;\n    elem('timingText').style.color = $white;\n    elem('timingText').style.background = $black;\n    elem('resultText').style.color = $white;\n    elem('resultText').style.background = $black;\n    setCookie(window.colorKey, color)\n}\n\n// Get the active session for a given action if present\nfunction getSession(actionName) {\n  for (i in window.activeSessions) {\n    let candidate = window.activeSessions[i]\n    if (candidate.name == actionName) {\n      return candidate\n    }\n  }\n  return null\n}\n\n// Impose a specific action on the editor.  Each action that the user has visited or created gets its own session and at most one\n// session can exist for each action.  Returns a Promise, which is either the result of calling getCode (truly asynchronous)\n// or a vacuous promise that simply continues the resolve chain (if an existing session was used).\n// Assumes that the 'language' global variable is correctly initialized for the action.\nfunction imposeAction(actionName) {\n  // Check whether we already have an ACE EditSession going for the action.  If so, just switch to it.\n  let candidate = getSession(actionName)\n  if (candidate != null) {\n    console.log(\"Used existing session for action \" + actionName)\n    window.editor.setSession(candidate.session)\n    imposeWebbiness(candidate.isWeb)\n    return Promise.resolve(actionName)\n  }\n  // If we are making a new session, we initialize it here with example code.  This may be overwritten by saved\n  // code.  However, if there is no saved code, getCode will do nothing but will resolve to the action name rather\n  // than rejecting.  This will leave the sample code in place\n  let session = new window.EditSession(language.example)\n  session.setMode(language.editMode)\n  session.on(\"change\", codeChanged)\n  window.activeSessions[window.activeSessions.count] = { name: actionName, session: session, isWeb: false }\n  window.editor.setSession(session)\n  return getCode(actionName)\n}\n\n// Called when code changes\nfunction codeChanged(delta) {\n  window.editorContentsChanged = true\n}\n\n// Open a request session to nimbella\nfunction makeOpenWhiskRequest(actionName, args) {\n  return new Promise(function (resolve, reject) {\n      const xhr = new XMLHttpRequest()\n      const url = window.APIHOST + '/api/v1/web/' + window.PLAYGROUND + '/default/' + actionName\n      xhr.open('POST', url)\n      xhr.onload = function () {\n        if (this.status >= 200 && this.status < 300) {\n          resolve(xhr.responseText)\n          } else {\n          console.log(\"calling reject with status\", this.status)\n          reject({status: this.status, statusText: xhr.statusText})\n          }\n      }\n      xhr.onerror = function () {\n        console.log(\"calling reject with network error\")\n        reject({statusText: \"Network error\"})\n      }\n        xhr.send(JSON.stringify(args))\n  })\n}\n\n// Conditionally save the code from the current editor without actually running it (and only if contents of the editor\n// have changed since initialization or last save).  Invoked periodically (\"autosave\").\nfunction maybeSave() {\n  if (window.editorContentsChanged) {\n    save(false)\n  }\n}\n\n// Save the code without running it, either as a standard action or a webaction.   Called for autosaving iff editor contents changed\n// and when imposing webbiness or non-webbiness.\nfunction save(web) {\n  elem(\"run\").disabled = true  // Suppress run while saving\n  console.log(\"Saving editor contents\")\n  let contents = window.editor.getValue()\n    let arg = { code : contents, playgroundId: window.playgroundId, actionName: window.currentAction, runtime: window.language.kind }\n    if (web) {\n      arg['web-export'] = true\n    } else {\n      arg['saveOnly'] = true\n    }\n    return makeOpenWhiskRequest('playground-run.json', arg).then(result => {\n    window.editorContentsChanged = false  // regardless of error.  We don't want to keep trying if it isn't going to work.\n    elem(\"run\").disabled = false  // Save is over, run is ok\n    let response = JSON.parse(result)\n    if (\"error\" in response) { // this is error as defined by the remote action, not xhr\n      let error = response.error\n      console.log(\"Error response: \" + error)\n    } else if (\"saved\" in response) { // success\n      console.log(\"Saved\")\n    } else {\n      console.log(\"Unexpected\", response)\n    }\n    }).catch(err => {\n    console.error(\"Error performing save action\", err)\n    })\n}\n\n// Set the contents of a text display area\nfunction setAreaContents(areaID, contents, error) {\n  let innerHTML = error ? \"<p style=\\\"color:red\\\">\" + contents + \"</p>\" : contents\n  elem(areaID).innerHTML = innerHTML\n}\n\n// Respond to click of the publish/retract button\nfunction publishClicked() {\n    let button = elem(\"publish\")\n    let session = getSession(currentAction)\n    let newWebbiness = !session.isWeb\n  save(newWebbiness).then(imposeWebbiness(newWebbiness)).catch(button.blur())\n}\n\n// Process a new name entered in the nameInput area\nfunction processNewName() {\n  if (window.entryFollowup == null) {\n    // Can happen because of cancelling with escape key after some data was entered\n    console.log(\"Not processing new name due to previous cancellation\")\n    return\n  }\n  let newName = elem(\"nameInput\").value\n  if (newName.trim() == \"\") {\n    // Cancel request\n    console.log(\"Cancel detected as empty name\")\n    endNameEntry()\n    return\n  }\n  console.log(\"Processing new name\", newName)\n  if (isInvalidActionName(newName)) {\n    postNameError(\"Invalid name\")\n  } else if (isConflictingActionName(newName)) {\n    postNameError(\"Conflicting Name\")\n  } else {\n    console.log(\"Valid new name\", newName)\n    window.entryFollowup(newName) // leave remainder to the individual followups\n  }\n}\n\n// Check for valid syntax of action name.  Returns true IF INVALID!  Rule:\n// The first character must be an alphanumeric character, or an underscore.\n// The subsequent characters can be alphanumeric, spaces, or any of the following values: _, @, ., -.\n// The last character can't be a space.\nfunction isInvalidActionName(newName) {\n  if (newName.trim() !== newName) {\n    return true\n  }\n  let valid = /^[0-9a-zA-Z_][ 0-9a-zA-Z_@.-]*$/\n  return !valid.test(newName)\n}\n\n// Check for conflict between a proposed action name and any existing action in the same package\nfunction isConflictingActionName(newName) {\n  for (action of window.actionList) {\n    if (action.name == newName) {\n      return true\n    }\n  }\n  return false\n}\n\n// Post an error over the name entry area\nfunction postNameError(msg) {\n  console.log(\"Posting name error\", msg)\n  let nameInput = elem(\"nameInput\")\n  let savedValue = nameInput.value\n  let savedColor = nameInput.style.color\n  nameInput.style.color = \"red\"\n  nameInput.value = msg\n  setTimeout(function() {\n    nameInput.value = savedValue\n    nameInput.style.color = savedColor\n  }, 2000)\n}\n\n// abbreviation for document.getElementById\nfunction elem(name) {\n  return document.getElementById(name)\n}\n\n// Respond to click of the run button\nfunction runClicked() {\n  window.editorContentsChanged = false  // don't permit save to run in parallel\n  let contents = window.editor.getValue()\n    console.log(\"Contents: \", contents)\n    setAreaContents(\"resultText\", \"Running...\")\n    let t0 = new Date().getTime()\n    let inputStr = elem(\"input\").value\n    let params = JSON.parse(inputStr)\n    let arg = { code : contents, params: params, playgroundId: window.playgroundId, actionName: window.currentAction, runtime: window.language.kind }\n    return makeOpenWhiskRequest('playground-run.json', arg).then(result => {\n    let elapsed = new Date().getTime() - t0\n    let response = JSON.parse(result)\n    if (\"error\" in response) {\n      let msg = response.error.response.result.error // seems the more readable form of the error is buried here\n      let inx = msg.indexOf(\"\\n\")\n      let usermsg = inx > 0 ? msg.substring(0, inx) : msg\n      console.log(\"Error response: \" + msg)\n      setAreaContents(\"resultText\", usermsg, true)\n      setAreaContents(\"timingText\", \"\", false)\n    } else {\n      console.log('response: ', response)\n      console.log('elapsed: ', elapsed)\n      let result = response['result']\n      let deploy = response['deployTime']\n      let exec = response['runTime']\n      let network = elapsed - (deploy + exec)\n\n      if (result.body && result.headers && result.headers['content-type'] == 'image/jpeg') {\n        setAreaContents(\"resultText\", '<img src=\"data:image/png;base64, ' + result.body + '\">', false)\n      } else {\n        setAreaContents(\"resultText\", JSON.stringify(result, null, 4), false)\n      }\n\n      let timingStr = \"Network: \" + network + \" ms<br>Deploy: \" + deploy + \" ms<br>Exec: \" + exec + \" ms\"\n      setAreaContents(\"timingText\", timingStr, false)\n    }\n    }).catch(err => {\n        console.log(\"Error contacting service\", err)\n        setAreaContents(\"resultText\", \"Error contacting service, status = \" + err.status, true)\n        setAreaContents(\"timingText\", \"\", false)\n   });\n}\n"
  },
  {
    "path": "core/standalone/src/main/resources/standalone-kcf.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ninclude classpath(\"standalone.conf\")\n\nwhisk {\n  spi {\n    ContainerFactoryProvider = \"org.apache.openwhisk.core.containerpool.kubernetes.KubernetesContainerFactoryProvider\"\n    LogStoreProvider = \"org.apache.openwhisk.core.containerpool.logging.DockerToActivationLogStoreProvider\"\n    LoadBalancerProvider = \"org.apache.openwhisk.core.loadBalancer.LeanBalancer\"\n    EntitlementSpiProvider = \"org.apache.openwhisk.core.entitlement.LocalEntitlementProvider\"\n    InvokerProvider = \"org.apache.openwhisk.core.invoker.InvokerReactive\"\n    InvokerServerProvider = \"org.apache.openwhisk.core.invoker.DefaultInvokerServer\"\n    DurationCheckerProvider = \"org.apache.openwhisk.core.scheduler.queue.NoopDurationCheckerProvider\"\n  }\n  kubernetes {\n    timeouts {\n      # Use higher timeout for run as in local dev the required Docker images may not be pre pulled\n      run = 10 minute\n      logs = 1 minute\n    }\n    user-pod-node-affinity {\n      enabled = false\n    }\n    port-forwarding-enabled = true\n  }\n  helm.release = \"release\"\n  runtime.delete.timeout = \"30 seconds\"\n}\n"
  },
  {
    "path": "core/standalone/src/main/resources/standalone.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ninclude classpath(\"application.conf\")\n\nkamon {\n  modules {\n    # Disable statsd in standalone mode as well.\n    statsd-reporter {\n      enabled = false\n    }\n  }\n}\n\nwhisk {\n  metrics {\n    kamon-enabled = true\n    kamon-tags-enabled = true\n    prometheus-enabled = true\n  }\n\n  spi {\n    ArtifactStoreProvider = \"org.apache.openwhisk.core.database.memory.MemoryArtifactStoreProvider\"\n    MessagingProvider = \"org.apache.openwhisk.connector.lean.LeanMessagingProvider\"\n    LoadBalancerProvider = \"org.apache.openwhisk.core.loadBalancer.LeanBalancer\"\n    # Use cli based log store for all setups as its more stable to use\n    # and does not require root user access\n    LogStoreProvider = \"org.apache.openwhisk.core.containerpool.docker.DockerCliLogStoreProvider\"\n    ContainerFactoryProvider = \"org.apache.openwhisk.core.containerpool.docker.StandaloneDockerContainerFactoryProvider\"\n    EntitlementSpiProvider = \"org.apache.openwhisk.core.entitlement.LocalEntitlementProvider\"\n    InvokerProvider = \"org.apache.openwhisk.core.invoker.InvokerReactive\"\n    InvokerServerProvider = \"org.apache.openwhisk.core.invoker.DefaultInvokerServer\"\n    DurationCheckerProvider = \"org.apache.openwhisk.core.scheduler.queue.NoopDurationCheckerProvider\"\n  }\n\n  info {\n    build-no = \"standalone\"\n    date = \"???\"\n  }\n\n  config {\n    controller-instances = 1\n    limits-actions-sequence-maxLength = 50\n    limits-triggers-fires-perMinute = 60\n    limits-actions-invokes-perMinute = 60\n    limits-actions-invokes-concurrent = 30\n  }\n\n  controller {\n    protocol = http\n\n    # Bound only to localhost by default for better security\n    interface = localhost\n  }\n\n  # Default set of users which are bootstrapped upon start\n  users {\n    whisk-system = \"789c46b1-71f6-4ed5-8c54-816aa4f8c502:abczO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\"\n    guest = \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\"\n  }\n\n  docker {\n    # Path to docker executuable. Generally its /var/lib/docker\n    # executable =\n    standalone.container-factory {\n      #If enabled then pull would also be attempted for standard OpenWhisk images under`openwhisk` prefix\n      pull-standard-images = true\n    }\n\n    container-factory {\n      # Disable runc by default to keep things stable\n      use-runc = false\n    }\n  }\n  swagger-ui {\n    file-system = false\n    dir-path = \"BOOT-INF/classes/swagger-ui\"\n  }\n\n  standalone {\n    redis {\n      image = \"redis:4.0\"\n    }\n\n    api-gateway {\n      image = \"openwhisk/apigateway:0.11.0\"\n    }\n\n    couchdb {\n      image = \"apache/couchdb:2.3\"\n      user = \"whisk_admin\"\n      password = \"some_passw0rd\"\n      prefix = \"whisk_local_\"\n      volumes-enabled = true\n      subject-views = [\n        \"auth_design_document_for_subjects_db_v2.0.0.json\",\n        \"filter_design_document.json\",\n        \"namespace_throttlings_design_document_for_subjects_db.json\"\n      ]\n      whisk-views = [\n        \"whisks_design_document_for_entities_db_v2.1.0.json\",\n        \"filter_design_document.json\"\n      ]\n      activation-views = [\n        \"whisks_design_document_for_activations_db_v2.1.0.json\",\n        \"whisks_design_document_for_activations_db_filters_v2.1.1.json\",\n        \"filter_design_document.json\",\n        \"activations_design_document_for_activations_db.json\",\n        \"logCleanup_design_document_for_activations_db.json\"\n      ]\n    }\n\n    user-events {\n      image = \"openwhisk/user-events:nightly\"\n      prometheus-image = \"prom/prometheus:v2.5.0\"\n      grafana-image = \"grafana/grafana:6.1.6\"\n    }\n  }\n  apache-client {\n    retry-no-http-response-exception = true\n  }\n  container-factory {\n    container-args {\n      extra-args {\n        env += ${?CONTAINER_EXTRA_ENV}\n      }\n    }\n  }\n}\n\npekko-http-cors {\n  allow-generic-http-requests = yes\n  allow-credentials = yes\n  allowed-origins = \"*\"\n  allowed-headers = \"*\"\n  allowed-methods = [\"GET\", \"POST\", \"HEAD\", \"OPTIONS\"]\n  exposed-headers = []\n  max-age = 1800 seconds\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/core/ExecManifestSupport.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core\n\nimport org.apache.openwhisk.core.entity.ExecManifest\n\n/**\n * Helper utility class to enable access to core scoped ExecManifest and related classes\n */\nobject ExecManifestSupport {\n  def getDefaultImage(familyName: String): Option[String] = {\n    val runtimes = ExecManifest.runtimesManifest\n    runtimes.resolveDefaultRuntime(s\"$familyName:default\").map(_.image.resolveImageName())\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/ApiGwLauncher.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport org.apache.pekko.actor.{ActorSystem, Scheduler}\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport org.apache.pekko.pattern.RetrySupport\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.standalone.StandaloneDockerSupport.{containerName, createRunCmd}\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass ApiGwLauncher(docker: StandaloneDockerClient, apiGwApiPort: Int, apiGwMgmtPort: Int, serverPort: Int)(\n  implicit logging: Logging,\n  ec: ExecutionContext,\n  actorSystem: ActorSystem,\n  tid: TransactionId)\n    extends RetrySupport {\n  private implicit val scd: Scheduler = actorSystem.scheduler\n  case class RedisConfig(image: String)\n  case class ApiGwConfig(image: String)\n  private val redisConfig = loadConfigOrThrow[RedisConfig](StandaloneConfigKeys.redisConfigKey)\n  private val apiGwConfig = loadConfigOrThrow[ApiGwConfig](StandaloneConfigKeys.apiGwConfigKey)\n\n  def run(): Future[Seq[ServiceContainer]] = {\n    for {\n      (redis, redisSvcs) <- runRedis()\n      _ <- waitForRedis(redis)\n      (_, apiGwSvcs) <- runApiGateway(redis)\n      _ <- waitForApiGw()\n    } yield Seq(redisSvcs, apiGwSvcs).flatten\n  }\n\n  def runRedis(): Future[(StandaloneDockerContainer, Seq[ServiceContainer])] = {\n    val defaultRedisPort = 6379\n    val redisPort = StandaloneDockerSupport.checkOrAllocatePort(defaultRedisPort)\n    logging.info(this, s\"Starting Redis at $redisPort\")\n\n    val params = Map(\"-p\" -> Set(s\"$redisPort:6379\"))\n    val name = containerName(\"redis\")\n    val args = createRunCmd(name, dockerRunParameters = params)\n    val f = docker.runDetached(redisConfig.image, args, shouldPull = true)\n    val sc = ServiceContainer(redisPort, s\"http://localhost:$redisPort\", name)\n    f.map(c => (c, Seq(sc)))\n  }\n\n  def waitForRedis(c: StandaloneDockerContainer): Future[Unit] = {\n    retry(() => isRedisUp(c), 12, 5.seconds)\n  }\n\n  private def isRedisUp(c: StandaloneDockerContainer) = {\n    val args = Seq(\n      \"run\",\n      \"--rm\",\n      \"--name\",\n      containerName(\"redis-test\"),\n      redisConfig.image,\n      \"redis-cli\",\n      \"-h\",\n      c.addr.host,\n      \"-p\",\n      \"6379\",\n      \"ping\")\n    docker.runCmd(args, docker.clientConfig.timeouts.run).map(out => require(out.toLowerCase == \"pong\"))\n  }\n\n  def runApiGateway(redis: StandaloneDockerContainer): Future[(StandaloneDockerContainer, Seq[ServiceContainer])] = {\n    val hostIp = StandaloneDockerSupport.getLocalHostIp()\n    val env = Map(\n      \"BACKEND_HOST\" -> s\"http://$hostIp:$serverPort\",\n      \"REDIS_HOST\" -> redis.addr.host,\n      \"REDIS_PORT\" -> \"6379\",\n      //This is the name used to render the final url. So should be localhost\n      //as that would be used by end user outside of docker\n      \"PUBLIC_MANAGEDURL_HOST\" -> StandaloneDockerSupport.getLocalHostName(),\n      \"PUBLIC_MANAGEDURL_PORT\" -> apiGwMgmtPort.toString)\n\n    logging.info(this, s\"Starting Api Gateway at api port: $apiGwApiPort, management port: $apiGwMgmtPort\")\n    val name = containerName(\"apigw\")\n    val params = Map(\"-p\" -> Set(s\"$apiGwApiPort:9000\", s\"$apiGwMgmtPort:8080\"))\n    val args = createRunCmd(name, env, params)\n\n    //TODO ExecManifest is scoped to core. Ideally we would like to do\n    // ExecManifest.ImageName(apiGwConfig.image).prefix.contains(\"openwhisk\")\n    val pull = apiGwConfig.image.startsWith(\"openwhisk\")\n    val f = docker.runDetached(apiGwConfig.image, args, pull)\n    val sc = Seq(\n      ServiceContainer(apiGwApiPort, s\"http://localhost:$apiGwApiPort\", s\"$name, Api Gateway - Api Service \"),\n      ServiceContainer(apiGwMgmtPort, s\"http://localhost:$apiGwMgmtPort\", s\"$name, Api Gateway - Management Service\"))\n    f.map(c => (c, sc))\n  }\n\n  def waitForApiGw(): Future[Unit] = {\n    new ServerStartupCheck(\n      Uri(s\"http://${StandaloneDockerSupport.getLocalHostName()}:$apiGwApiPort/v1/apis\"),\n      \"ApiGateway\")\n      .waitForServerToStart()\n    Future.successful(())\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/CouchDBLauncher.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.File\nimport java.net.URLEncoder\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.headers.{Accept, Authorization, BasicHttpCredentials}\nimport org.apache.pekko.http.scaladsl.model.{\n  HttpHeader,\n  HttpMethods,\n  HttpRequest,\n  MediaTypes,\n  StatusCode,\n  StatusCodes,\n  Uri\n}\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport com.typesafe.config.ConfigFactory\nimport org.apache.commons.io.IOUtils\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.database.CouchDbRestClient\nimport org.apache.openwhisk.http.PoolingRestClient\nimport org.apache.openwhisk.http.PoolingRestClient._\nimport org.apache.openwhisk.standalone.StandaloneDockerSupport.{containerName, createRunCmd}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass CouchDBLauncher(docker: StandaloneDockerClient, port: Int, dataDir: File)(implicit logging: Logging,\n                                                                                ec: ExecutionContext,\n                                                                                actorSystem: ActorSystem,\n                                                                                tid: TransactionId) {\n  case class CouchDBConfig(image: String,\n                           user: String,\n                           password: String,\n                           prefix: String,\n                           volumesEnabled: Boolean,\n                           subjectViews: List[String],\n                           whiskViews: List[String],\n                           activationViews: List[String])\n  private val dbConfig = loadConfigOrThrow[CouchDBConfig](StandaloneConfigKeys.couchDBConfigKey)\n  private val couchClient = new PoolingRestClient(\"http\", StandaloneDockerSupport.getLocalHostName(), port, 100)\n  private val baseHeaders: List[HttpHeader] =\n    List(Authorization(BasicHttpCredentials(dbConfig.user, dbConfig.password)), Accept(MediaTypes.`application/json`))\n  private val subjectDb = dbConfig.prefix + \"subjects\"\n  private val activationsDb = dbConfig.prefix + \"activations\"\n  private val whisksDb = dbConfig.prefix + \"whisks\"\n  private val resourcePrefix = \"couch\"\n\n  def run(): Future[ServiceContainer] = {\n    if (dataDir.list().nonEmpty) {\n      logging.info(this, s\"Using pre-existing database from ${dataDir.getAbsolutePath}\")\n    } else {\n      logging.info(this, s\"Creating new database at ${dataDir.getAbsolutePath}\")\n    }\n    for {\n      (_, dbSvcs) <- runCouch()\n      _ <- waitForCouchDB()\n      _ <- createDbIfNotExist(\"_users\")\n      _ <- createDbWithViews(subjectDb, dbConfig.subjectViews)\n      _ <- createDbWithViews(whisksDb, dbConfig.whiskViews)\n      _ <- createDbWithViews(activationsDb, dbConfig.activationViews)\n      _ <- {\n        updateConfig()\n        logging.info(\n          this,\n          s\"CouchDB started successfully at http://${StandaloneDockerSupport\n            .getLocalHostName()}:$port/_utils . Username: [${dbConfig.user}], Password: [${dbConfig.password}]\")\n        Future.successful(Done)\n      }\n    } yield dbSvcs\n  }\n\n  def runCouch(): Future[(StandaloneDockerContainer, ServiceContainer)] = {\n    logging.info(this, s\"Starting CouchDB at $port\")\n    val baseParams = Map(\"-p\" -> Set(s\"$port:5984\"))\n    val params =\n      if (dbConfig.volumesEnabled) baseParams + (\"-v\" -> Set(s\"${dataDir.getAbsolutePath}:/opt/couchdb/data\"))\n      else baseParams\n    val env = Map(\"COUCHDB_USER\" -> dbConfig.user, \"COUCHDB_PASSWORD\" -> dbConfig.password)\n    val name = containerName(\"couch\")\n    val args = createRunCmd(name, env, params)\n    val f = docker.runDetached(dbConfig.image, args, shouldPull = true)\n    val sc = ServiceContainer(\n      port,\n      s\"http://localhost:$port/_utils\",\n      s\"$name, Username: [${dbConfig.user}], Password: [${dbConfig.password}]\")\n    f.map(c => (c, sc))\n  }\n\n  def waitForCouchDB(): Future[Done] = {\n    new ServerStartupCheck(Uri(s\"http://${StandaloneDockerSupport.getLocalHostName()}:$port/_utils/\"), \"CouchDB\")\n      .waitForServerToStart()\n    Future.successful(Done)\n  }\n\n  private def createDbWithViews(dbName: String, views: List[String]): Future[Done] = {\n    for {\n      _ <- createDbIfNotExist(dbName)\n      _ <- createDocs(views, dbName)\n    } yield Done\n  }\n\n  private def createDbIfNotExist(dbName: String): Future[Done] = {\n    for {\n      userDbExist <- doesDbExist(dbName)\n      _ <- if (userDbExist) Future.successful(Done) else createDb(dbName)\n    } yield Done\n  }\n\n  private def doesDbExist(dbName: String): Future[Boolean] = {\n    requestString(mkRequest(HttpMethods.HEAD, uri(dbName), headers = baseHeaders))\n      .map {\n        case Right(_)                   => true\n        case Left(StatusCodes.NotFound) => false\n        case Left(s)                    => throw new IllegalStateException(\"Unknown status code while checking user db\" + s)\n      }\n  }\n\n  private def createDb(dbName: String): Future[Done] = {\n    requestString(mkRequest(HttpMethods.PUT, uri(dbName), headers = baseHeaders))\n      .map {\n        case Right(_) => Done\n        case Left(s)  => throw new IllegalStateException((\"Unknown status code while creating user db\" + s))\n      }\n  }\n\n  private def createDocs(jsonFiles: List[String], dbName: String): Future[Done] = {\n    val client = createDbClient(dbName)\n    val f = jsonFiles\n      .map { p =>\n        val s = IOUtils.resourceToString(s\"/$resourcePrefix/$p\", UTF_8)\n        val js = s.parseJson.asJsObject\n        logging.info(this, s\"Creating view doc from file $p for db $dbName\")\n        createDocIfRequired(js, dbName, client)\n      }\n\n    Future.sequence(f).map(_ => client.shutdown()).map(_ => Done)\n  }\n\n  private def createDocIfRequired(doc: JsObject, dbName: String, client: CouchDbRestClient): Future[Done] = {\n    val id = doc.fields(\"_id\").convertTo[String]\n    for {\n      jsOpt <- getDoc(id, client)\n      _ <- jsOpt match {\n        case Some(js) => {\n          val rev = js.fields(\"_rev\").convertTo[String]\n          val docWithRev = JsObject(doc.fields + (\"_rev\" -> JsString(rev)))\n          if (docWithRev != js) {\n            logging.info(this, s\"Updating doc $id for db ${dbName}\")\n            createDoc(id, Some(rev), doc, client)\n          } else Future.successful(Done)\n        }\n        case None => {\n          logging.info(this, s\"Creating doc $id for db ${dbName}\")\n          createDoc(id, None, doc, client)\n        }\n      }\n    } yield Done\n  }\n\n  private def getDoc(id: String, client: CouchDbRestClient): Future[Option[JsObject]] = {\n    client.getDoc(id).map {\n      case Right(js)                  => Some(js)\n      case Left(StatusCodes.NotFound) => None\n      case Left(s)                    => throw new IllegalStateException(s\"Unknown status code while fetching doc $id\" + s)\n    }\n  }\n\n  private def createDoc(id: String, rev: Option[String], doc: JsObject, client: CouchDbRestClient): Future[Done] = {\n    val f = rev match {\n      case Some(r) => client.putDoc(id, r, doc)\n      case None    => client.putDoc(id, doc)\n    }\n    f.map {\n      case Right(_) => Done\n      case Left(s)  => throw new IllegalStateException(s\"Unknown status code while creating doc $id\" + s)\n    }\n  }\n\n  // Properly encodes the potential slashes in each segment.\n  private def uri(segments: Any*): Uri = {\n    val encodedSegments = segments.map(s => URLEncoder.encode(s.toString, UTF_8.name))\n    Uri(s\"/${encodedSegments.mkString(\"/\")}\")\n  }\n\n  private def createDbClient(dbName: String) =\n    new NonEscapingClient(\n      \"http\",\n      StandaloneDockerSupport.getLocalHostName(),\n      port,\n      dbConfig.user,\n      dbConfig.password,\n      dbName)(actorSystem, logging)\n\n  private def updateConfig(): Unit = {\n    //The config needs to pushed via system property and then the Typesafe ConfigFactory cache\n    //should be purged such that config gets read again and hence read these system properties\n    setp(\"host\", StandaloneDockerSupport.getLocalHostName())\n    setp(\"port\", port.toString)\n    setp(\"password\", dbConfig.password)\n    setp(\"username\", dbConfig.user)\n    setp(\"protocol\", \"http\")\n    setp(\"provider\", \"CouchDB\")\n    setp(\"databases.WhiskActivation\", activationsDb)\n    setp(\"databases.WhiskAuth\", subjectDb)\n    setp(\"databases.WhiskEntity\", whisksDb)\n\n    System.setProperty(s\"whisk.spi.ArtifactStoreProvider\", \"org.apache.openwhisk.core.database.CouchDbStoreProvider\")\n    ConfigFactory.invalidateCaches()\n    logging.info(this, \"Invalidated config cached\")\n  }\n\n  private def setp(key: String, value: String): Unit = {\n    System.setProperty(s\"whisk.couchdb.$key\", value)\n  }\n\n  /**\n   * This is similar to PoolingRestClient#requestJson just that here we materialize to String. As some of the db\n   * related operation return with empty body\n   */\n  private def requestString(futureRequest: Future[HttpRequest]): Future[Either[StatusCode, String]] = {\n    couchClient.request(futureRequest).flatMap { response =>\n      if (response.status.isSuccess) {\n        Unmarshal(response.entity.withoutSizeLimit).to[String].map(Right.apply)\n      } else {\n        Unmarshal(response.entity).to[String].flatMap { body =>\n          val statusCode = response.status\n          val reason =\n            if (body.nonEmpty) s\"${statusCode.reason} (details: $body)\" else statusCode.reason\n          val customStatusCode = StatusCodes\n            .custom(intValue = statusCode.intValue, reason = reason, defaultMessage = statusCode.defaultMessage)\n          // This is important, as it drains the entity stream.\n          // Otherwise the connection stays open and the pool dries up.\n          response.discardEntityBytes().future.map(_ => Left(customStatusCode))\n        }\n      }\n    }\n  }\n}\n\nprivate class NonEscapingClient(protocol: String,\n                                host: String,\n                                port: Int,\n                                username: String,\n                                password: String,\n                                db: String)(implicit system: ActorSystem, logging: Logging)\n    extends CouchDbRestClient(protocol, host, port, username, password, db) {\n\n  //Do not escape the design doc id like _design/subjects etc\n  override protected def uri(segments: Any*): Uri = Uri(s\"/${segments.mkString(\"/\")}\")\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/DockerVersion.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport scala.util.control.NonFatal\n\ncase class DockerVersion(major: Int, minor: Int, patch: Int) extends Ordered[DockerVersion] {\n  import scala.math.Ordered.orderingToOrdered\n  def compare(that: DockerVersion): Int =\n    (this.major, this.minor, this.patch) compare (that.major, that.minor, that.patch)\n\n  override def toString = s\"$major.$minor.$patch\"\n}\n\nobject DockerVersion {\n  implicit val ord: Ordering[DockerVersion] = Ordering.by(unapply)\n  private val pattern = \".*Docker version ([\\\\d.]+).*\".r\n\n  def apply(str: String): DockerVersion = {\n    try {\n      val parts = if (str != null && str.nonEmpty) str.split('.') else Array[String]()\n      val major = if (parts.length >= 1) parts(0).toInt else 0\n      val minor = if (parts.length >= 2) parts(1).toInt else 0\n      val patch = if (parts.length >= 3) parts(2).toInt else 0\n      DockerVersion(major, minor, patch)\n    } catch {\n      case NonFatal(_) => throw new IllegalArgumentException(s\"bad docker version $str\")\n    }\n  }\n\n  def fromVersionCommand(str: String): DockerVersion = {\n    val pattern(version) = str\n    apply(version)\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/InstallRouteMgmt.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.File\n\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport org.apache.commons.io.{FileUtils, IOUtils}\nimport org.apache.openwhisk.common.TransactionId.systemPrefix\nimport org.apache.openwhisk.common.{Logging, TransactionId}\n\nimport scala.sys.process.ProcessLogger\nimport scala.util.Try\nimport scala.sys.process._\n\ncase class InstallRouteMgmt(workDir: File,\n                            authKey: String,\n                            apiHost: Uri,\n                            namespace: String,\n                            gatewayUrl: Uri,\n                            wsk: String)(implicit log: Logging) {\n  case class Action(name: String, desc: String)\n  private val noopLogger = ProcessLogger(_ => ())\n  private implicit val tid: TransactionId = TransactionId(systemPrefix + \"apiMgmt\")\n  val actionNames = Array(\n    Action(\"createApi\", \"Create an API\"),\n    Action(\"deleteApi\", \"Delete the API\"),\n    Action(\"getApi\", \"Retrieve the specified API configuration (in JSON format)\"))\n\n  def run(): Unit = {\n    require(wskExists, s\"wsk command not found at $wsk. Route management actions cannot be installed\")\n    log.info(this, packageUpdateCmd.!!.trim)\n    //TODO Optimize to ignore this if package already installed\n    actionNames.foreach { action =>\n      val name = action.name\n      val actionZip = new File(workDir, s\"$name.zip\")\n      FileUtils.copyURLToFile(IOUtils.resourceToURL(s\"/$name.zip\"), actionZip)\n      val cmd = createActionUpdateCmd(action, name, actionZip)\n      val result = cmd.!!.trim\n      log.info(this, s\"Installed $name - $result\")\n      FileUtils.deleteQuietly(actionZip)\n    }\n    //This log message is used by tests to confirm that actions are installed\n    log.info(this, \"Installed Route Management Actions\")\n  }\n\n  private def createActionUpdateCmd(action: Action, name: String, actionZip: File) = {\n    Seq(\n      wsk,\n      \"--apihost\",\n      apiHost.toString(),\n      \"--auth\",\n      authKey,\n      \"action\",\n      \"update\",\n      s\"$namespace/apimgmt/$name\",\n      actionZip.getAbsolutePath,\n      \"-a\",\n      \"description\",\n      action.desc,\n      \"--kind\",\n      \"nodejs:default\",\n      \"-a\",\n      \"web-export\",\n      \"true\",\n      \"-a\",\n      \"final\",\n      \"true\")\n  }\n\n  private def packageUpdateCmd = {\n    Seq(\n      wsk,\n      \"--apihost\",\n      apiHost.toString(),\n      \"--auth\",\n      authKey,\n      \"package\",\n      \"update\",\n      s\"$namespace/apimgmt\",\n      \"--shared\",\n      \"no\",\n      \"-a\",\n      \"description\",\n      \"This package manages the gateway API configuration.\",\n      \"-p\",\n      \"gwUrlV2\",\n      gatewayUrl.toString())\n  }\n\n  def wskExists: Boolean = Try(s\"$wsk property get --cliversion\".!(noopLogger)).getOrElse(-1) == 0\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/KafkaLauncher.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.File\nimport org.apache.pekko.actor.ActorSystem\nimport io.github.embeddedkafka.{EmbeddedKafka, EmbeddedKafkaConfig}\nimport org.apache.commons.io.FileUtils\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.WhiskConfig.kafkaHosts\nimport org.apache.openwhisk.core.entity.ControllerInstanceId\nimport org.apache.openwhisk.core.loadBalancer.{LeanBalancer, LoadBalancer, LoadBalancerProvider}\nimport org.apache.openwhisk.standalone.StandaloneDockerSupport.{checkOrAllocatePort, containerName, createRunCmd}\n\nimport java.nio.file.FileSystems\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.reflect.io.Directory\nimport scala.util.Try\n\nclass KafkaLauncher(\n  docker: StandaloneDockerClient,\n  kafkaPort: Int,\n  kafkaDockerPort: Int,\n  zkPort: Int,\n  workDir: File,\n  kafkaUi: Boolean)(implicit logging: Logging, ec: ExecutionContext, actorSystem: ActorSystem, tid: TransactionId) {\n\n  def run(): Future[Seq[ServiceContainer]] = {\n    for {\n      kafkaSvcs <- runKafka()\n      uiSvcs <- if (kafkaUi) runKafkaUI() else Future.successful(Seq.empty[ServiceContainer])\n    } yield kafkaSvcs ++ uiSvcs\n  }\n\n  def runKafka(): Future[Seq[ServiceContainer]] = {\n\n    //Below setting based on https://rmoff.net/2018/08/02/kafka-listeners-explained/\n    // We configure two listeners where one is used for host based application and other is used for docker based application\n    // to connect to Kafka server running on host\n    // Here controller / invoker will use LISTENER_LOCAL since they run in the same JVM as the embedded Kafka\n    // and Kafka UI will run in a Docker container and use LISTENER_DOCKER\n    val brokerProps = Map(\n      \"listeners\" -> s\"LISTENER_LOCAL://localhost:$kafkaPort,LISTENER_DOCKER://localhost:$kafkaDockerPort\",\n      \"advertised.listeners\" -> s\"LISTENER_LOCAL://localhost:$kafkaPort,LISTENER_DOCKER://${StandaloneDockerSupport\n        .getLocalHostIp()}:$kafkaDockerPort\",\n      \"listener.security.protocol.map\" -> \"LISTENER_LOCAL:PLAINTEXT,LISTENER_DOCKER:PLAINTEXT\",\n      \"inter.broker.listener.name\" -> \"LISTENER_LOCAL\")\n    implicit val config: EmbeddedKafkaConfig =\n      EmbeddedKafkaConfig(kafkaPort = kafkaPort, zooKeeperPort = zkPort, customBrokerProperties = brokerProps)\n\n    val t = Try {\n      createDir(\"zookeeper\")\n      createDir(\"kafka\")\n      EmbeddedKafka.startZooKeeper(FileSystems.getDefault.getPath(workDir.getPath, \"zookeeper\"))\n      EmbeddedKafka.startKafka(FileSystems.getDefault.getPath(workDir.getPath, \"kafka\"))\n    }\n\n    Future\n      .fromTry(t)\n      .map(\n        _ =>\n          Seq(\n            ServiceContainer(kafkaPort, s\"localhost:$kafkaPort\", \"kafka\"),\n            ServiceContainer(\n              kafkaDockerPort,\n              s\"${StandaloneDockerSupport.getLocalHostIp()}:$kafkaDockerPort\",\n              \"kafka-docker\"),\n            ServiceContainer(zkPort, \"Zookeeper\", \"zookeeper\")))\n  }\n\n  def runKafkaUI(): Future[Seq[ServiceContainer]] = {\n    val hostIp = StandaloneDockerSupport.getLocalHostIp()\n    val port = checkOrAllocatePort(9000)\n    val env = Map(\n      \"ZOOKEEPER_CONNECT\" -> s\"$hostIp:$zkPort\",\n      \"KAFKA_BROKERCONNECT\" -> s\"$hostIp:$kafkaDockerPort\",\n      \"JVM_OPTS\" -> \"-Xms32M -Xmx64M\",\n      \"SERVER_SERVLET_CONTEXTPATH\" -> \"/\")\n\n    logging.info(this, s\"Starting Kafka Drop UI port: $port\")\n    val name = containerName(\"kafka-drop-ui\")\n    val params = Map(\"-p\" -> Set(s\"$port:9000\"))\n    val args = createRunCmd(name, env, params)\n\n    val f = docker.runDetached(\"obsidiandynamics/kafdrop\", args, true)\n    f.map(_ => Seq(ServiceContainer(port, s\"http://localhost:$port\", name)))\n  }\n\n  private def createDir(name: String) = {\n    val dir = new File(workDir, name)\n    FileUtils.forceMkdir(dir)\n    Directory(dir)\n  }\n}\n\nobject KafkaAwareLeanBalancer extends LoadBalancerProvider {\n  override def requiredProperties: Map[String, String] = LeanBalancer.requiredProperties ++ kafkaHosts\n\n  override def instance(whiskConfig: WhiskConfig, instance: ControllerInstanceId)(implicit actorSystem: ActorSystem,\n                                                                                  logging: Logging): LoadBalancer =\n    LeanBalancer.instance(whiskConfig, instance)\n}\n\nobject KafkaLauncher {\n  val preferredKafkaPort = 9092\n  val preferredKafkaDockerPort = preferredKafkaPort - 1\n  val preferredZkPort = 2181\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/LogbackConfigurator.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.ByteArrayInputStream\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport org.apache.pekko.event.LoggingAdapter\nimport ch.qos.logback.classic.joran.JoranConfigurator\nimport ch.qos.logback.classic.{Level, LoggerContext}\nimport ch.qos.logback.core.joran.spi.JoranException\nimport ch.qos.logback.core.util.StatusPrinter\nimport org.apache.commons.io.IOUtils\nimport org.apache.openwhisk.common.{PekkoLogging, TransactionId}\nimport org.slf4j.LoggerFactory\n\nimport scala.io.AnsiColor\nimport scala.util.Try\n\n/**\n * Resets the Logback config if logging is configure via non standard file\n */\nobject LogbackConfigurator {\n\n  def initLogging(conf: Conf): Unit = {\n    val ctx = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]\n    //Only tweak the log level if verbose option is used\n    if (conf.verbose() != 0) {\n      ctx.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).setLevel(toLevel(conf.verbose()))\n    }\n  }\n\n  private def toLevel(v: Int) = {\n    v match {\n      case 0 => Level.INFO\n      case 1 => Level.DEBUG\n      case _ => Level.ALL\n    }\n  }\n\n  def configureLogbackFromResource(resourceName: String): Unit = {\n    Try(configureLogback(IOUtils.resourceToString(\"/\" + resourceName, UTF_8))).failed.foreach(t =>\n      println(s\"Could not load resource $resourceName- ${t.getMessage}\"))\n  }\n\n  private def configureLogback(fileContent: String): Unit = {\n    val context = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]\n\n    try {\n      val configurator = new JoranConfigurator\n      configurator.setContext(context)\n      // Call context.reset() to clear any previous configuration, e.g. default\n      // configuration. For multi-step configuration, omit calling context.reset().\n      context.reset()\n      val is = new ByteArrayInputStream(fileContent.getBytes(UTF_8))\n      configurator.doConfigure(is)\n    } catch {\n      case _: JoranException =>\n      // StatusPrinter will handle this\n    }\n    StatusPrinter.printInCaseOfErrorsOrWarnings(context)\n  }\n}\n\n/**\n * Similar to PekkoLogging but with color support\n */\nclass ColoredPekkoLogging(loggingAdapter: LoggingAdapter) extends PekkoLogging(loggingAdapter) with AnsiColor {\n  import ColorOutput.clr\n\n  override protected def format(id: TransactionId, name: String, logmsg: String) =\n    s\"[${clr(id.toString, BOLD, true)}] [${clr(name.toString, CYAN, true)}] $logmsg\"\n}\n\nobject ColorOutput extends AnsiColor {\n  def clr(s: String, code: String, clrEnabled: Boolean) = if (clrEnabled) s\"$code$s$RESET\" else s\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/PlaygroundLauncher.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.server.directives.FileAndResourceDirectives.ResourceFile\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.http.cors.scaladsl.CorsDirectives._\nimport org.apache.commons.io.IOUtils\nimport org.apache.commons.lang3.SystemUtils\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ExecManifestSupport\nimport org.apache.openwhisk.http.BasicHttpService\nimport pureconfig._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext}\nimport scala.sys.process._\nimport scala.util.{Failure, Success, Try}\n\nclass PlaygroundLauncher(\n  host: String,\n  extHost: String,\n  controllerPort: Int,\n  pgPort: Int,\n  authKey: String,\n  devMode: Boolean,\n  noBrowser: Boolean)(implicit logging: Logging, ec: ExecutionContext, actorSystem: ActorSystem, tid: TransactionId) {\n  private val interface = loadConfigOrThrow[String](\"whisk.controller.interface\")\n  private val jsFileName = \"playgroundFunctions.js\"\n  private val jsContentType = ContentType(MediaTypes.`application/javascript`, HttpCharsets.`UTF-8`)\n\n  private val uiPath = {\n    //Depending on fact the run is done from within IDE or from terminal the classpath prefix needs to be adapted\n    val res = getClass.getResource(s\"/playground/ui/$jsFileName\")\n    Try(ResourceFile(res)) match {\n      case Success(_) => \"playground/ui\"\n      case Failure(_) => \"BOOT-INF/classes/playground/ui\"\n    }\n  }\n\n  private val jsFileContent = {\n    val js = resourceToString(jsFileName, \"ui\")\n    val content =\n      js.replace(\"window.APIHOST='http://localhost:3233'\", s\"window.APIHOST='http://$extHost:$controllerPort'\")\n    content.getBytes(UTF_8)\n  }\n\n  private val pg = \"playground\"\n  private val pgUrl = s\"http://${StandaloneDockerSupport.getLocalHostName()}:$pgPort/$pg\"\n\n  private val wsk = new Wsk(host, controllerPort, authKey)\n\n  def run(): ServiceContainer = {\n    BasicHttpService.startHttpService(PlaygroundService.route, pgPort, None, interface)(actorSystem)\n    ServiceContainer(pgPort, pgUrl, \"Playground\")\n  }\n\n  def install(): Unit = {\n    val actions = List(\"delete\", \"fetch\", \"run\", \"userpackage\")\n    val f = Source(actions)\n      .mapAsync(1) { name =>\n        val actionName = s\"playground-$name\"\n        val js = resourceToString(s\"playground-$name.js\", \"actions\")\n        val r = wsk.updatePgAction(actionName, js)\n        r.foreach(_ => logging.info(this, s\"Installed action $actionName\"))\n        r\n      }\n      .runWith(Sink.ignore)\n    Await.result(f, 5.minutes)\n    Try {\n      if (!devMode) {\n        prePullDefaultImages()\n      }\n      if (!noBrowser) {\n        launchBrowser(pgUrl)\n        logging.info(this, s\"Launched browser $pgUrl\")\n      }\n    }.failed.foreach(t => logging.warn(this, \"Failed to launch browser \" + t))\n  }\n\n  private def launchBrowser(url: String): Unit = {\n    if (SystemUtils.IS_OS_MAC) {\n      s\"open $url\".!!\n    } else if (SystemUtils.IS_OS_WINDOWS) {\n      s\"\"\"start \"$url\" \"\"\".!!\n    } else if (SystemUtils.IS_OS_LINUX) {\n      s\"xdg-open $url\".!!\n    }\n  }\n\n  private def prePullDefaultImages(): Unit = {\n    ExecManifestSupport.getDefaultImage(\"nodejs\").foreach { imageName =>\n      StandaloneDockerSupport.prePullImage(imageName)\n    }\n  }\n\n  object PlaygroundService extends BasicHttpService {\n    override def routes(implicit transid: TransactionId): Route =\n      path(PathEnd | Slash | pg) { redirect(s\"/$pg/ui/index.html\", StatusCodes.Found) } ~\n        cors() {\n          pathPrefix(pg / \"ui\" / Segment) { fileName =>\n            get {\n              if (fileName == jsFileName) {\n                complete(HttpEntity(jsContentType, jsFileContent))\n              } else {\n                getFromResource(s\"$uiPath/$fileName\")\n              }\n            }\n          }\n        }\n  }\n\n  private def resourceToString(name: String, resType: String) =\n    IOUtils.resourceToString(s\"/playground/$resType/$name\", UTF_8)\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/PreFlightChecks.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport com.typesafe.config.{Config, ConfigFactory}\nimport org.apache.commons.lang3.StringUtils\nimport org.apache.openwhisk.standalone.StandaloneDockerSupport.isPortFree\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.io.AnsiColor\nimport scala.sys.process._\nimport scala.util.Try\n\ncase class PreFlightChecks(conf: Conf) extends AnsiColor {\n  import ColorOutput.clr\n  private val noopLogger = ProcessLogger(_ => ())\n  private val clrEnabled = conf.colorEnabled\n  private val separator = \"=\" * 80\n  private val pass = st(\"OK\")\n  private val failed = st(\"FAILURE\")\n  private val warn = st(\"WARN\")\n  private val cliDownloadUrl = \"https://s.apache.org/openwhisk-cli-download\"\n  private val dockerUrl = \"https://docs.docker.com/install/\"\n\n  //Support for host.docker.internal is from 18.03\n  private val supportedDockerVersion = DockerVersion(\"18.03\")\n\n  def run(): Unit = {\n    println(separator)\n    println(\"Running pre flight checks ...\")\n    println()\n    println(s\"Local Host Name: ${StandaloneDockerSupport.getLocalHostName()}\")\n    println(s\"Local Internal Name: ${StandaloneDockerSupport.getLocalHostInternalName()}\")\n    println()\n    checkForDocker()\n    checkForWsk()\n    checkForPorts()\n    println()\n    println(separator)\n  }\n\n  def checkForDocker() = {\n    val dockerExistsResult = Try(\"docker --version\".!(noopLogger)).getOrElse(-1)\n    if (dockerExistsResult != 0) {\n      println(s\"$failed 'docker' cli not found.\")\n      println(s\"\\t Install docker from $dockerUrl\")\n    } else {\n      val versionCmdOutput = dockerVersion\n      println(s\"$pass 'docker' cli found. $versionCmdOutput\")\n\n      //Wrap in try to discard any issue related to version parsing\n      Try(checkDockerVersion(versionCmdOutput)).failed.foreach(t =>\n        println(s\"Error occurred while parsing version - ${t.getMessage}\"))\n\n      checkDockerIsRunning()\n      //Other things we can possibly check for\n      //1. add check for minimal supported docker version\n      //2. should we also run `docker run hello-world` to see if we can execute docker run command\n      //This command takes 2-4 secs. So running it by default for every run should be avoided\n    }\n  }\n\n  private def checkDockerVersion(versionCmdOutput: String)(implicit ordering: Ordering[DockerVersion]): Unit = {\n    val dv = DockerVersion.fromVersionCommand(versionCmdOutput)\n    if (dv < supportedDockerVersion) {\n      println(s\"$failed 'docker' version $dv older than minimum supported $supportedDockerVersion\")\n    } else {\n      println(s\"$pass 'docker' version $dv is newer than minimum supported $supportedDockerVersion\")\n    }\n  }\n\n  private def dockerVersion = version(\"docker --version '{{.Client.Version}}'\")\n\n  private def version(cmd: String) = Try(cmd !! (noopLogger)).map(v => s\"(${v.trim})\").getOrElse(\"\")\n\n  private def checkDockerIsRunning(): Unit = {\n    val dockerInfoResult = Try(\"docker info\".!(noopLogger)).getOrElse(-1)\n    if (dockerInfoResult != 0) {\n      println(s\"$failed 'docker' not found to be running. Failed to run 'docker info'\")\n    } else {\n      println(s\"$pass 'docker' is running.\")\n    }\n  }\n\n  def checkForWsk(): Unit = {\n    val wskExistsResult = Try(\"wsk property get --cliversion\".!(noopLogger)).getOrElse(-1)\n    if (wskExistsResult != 0) {\n      println(s\"$failed 'wsk' cli not found.\")\n      println(s\"\\tDownload the cli from $cliDownloadUrl\")\n    } else {\n      println(s\"$pass 'wsk' cli found. $wskCliVersion\")\n      checkWskProps()\n    }\n  }\n\n  def checkWskProps(): Unit = {\n    val users = loadConfigOrThrow[Map[String, String]](loadConfig(), StandaloneConfigKeys.usersConfigKey)\n\n    val configuredAuth = \"wsk property get --auth\".!!.trim\n    val apihost = \"wsk property get --apihost\".!!.trim\n\n    val requiredHostValue = s\"http://${StandaloneDockerSupport.getLocalHostName()}:${conf.port()}\"\n    val externalHostValue = s\"http://${StandaloneDockerSupport.getExternalHostName()}:${conf.port()}\"\n\n    //We can use -o option to get raw value. However as its a recent addition\n    //using a lazy approach where we check if output ends with one of the configured auth keys or\n    val matchedAuth = users.find { case (_, auth) => configuredAuth.endsWith(auth) }\n    val hostMatched = apihost.endsWith(requiredHostValue)\n\n    if (matchedAuth.isDefined && hostMatched) {\n      println(s\"$pass 'wsk' configured for namespace [${matchedAuth.get._1}].\")\n      println(s\"$pass 'wsk' configured to connect to $requiredHostValue.\")\n    } else {\n      val guestUser = users.find { case (ns, _) => ns == \"guest\" }\n      //Only if guest user is found suggest wsk command to use that. Otherwise user is using a non default setup\n      //which may not be used for wsk based access like for tests\n      guestUser match {\n        case Some((ns, guestAuth)) =>\n          println(s\"$warn Configure wsk via below command to connect to this server as [$ns]\")\n          println()\n          println(clr(s\"wsk property set --apihost '$externalHostValue' --auth '$guestAuth'\", MAGENTA, clrEnabled))\n        case None =>\n      }\n    }\n  }\n\n  def checkForPorts(): Unit = {\n    if (isPortFree(conf.port())) {\n      println(s\"$pass Server port [${conf.port()}] is free\")\n    } else {\n      println(s\"$failed Server port [${conf.port()}] is not free. Standalone server cannot start\")\n    }\n\n    if (conf.apiGw()) {\n      val port = conf.apiGwPort()\n      if (isPortFree(conf.apiGwPort())) {\n        println(s\"$pass Api gateway port [$port] is free\")\n      } else {\n        println(s\"$warn Api gateway port [$port] is not free. Api gateway cannot start\")\n      }\n    }\n  }\n\n  private def wskCliVersion = version(\"wsk property get --cliversion -o raw\")\n\n  private def loadConfig(): Config = {\n    conf.configFile.toOption match {\n      case Some(f) =>\n        require(f.exists(), s\"Config file $f does not exist\")\n        ConfigFactory.parseFile(f)\n      case None =>\n        ConfigFactory.parseResources(\"standalone.conf\")\n    }\n  }\n\n  private def st(level: String) = {\n    val maxLength = \"FAILURE\".length\n    val (msg, code) = level match {\n      case \"OK\"   => (StringUtils.center(\"OK\", maxLength), GREEN)\n      case \"WARN\" => (StringUtils.center(\"WARN\", maxLength), MAGENTA)\n      case _      => (\"FAILURE\", RED)\n    }\n    s\"[${clr(msg, code, clrEnabled)}]\"\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/ServerStartupCheck.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.net.{HttpURLConnection, URL}\n\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport com.google.common.base.Stopwatch\nimport org.apache.openwhisk.utils.retry\n\nimport scala.concurrent.duration._\n\nclass ServerStartupCheck(uri: Uri, serverName: String) {\n\n  def waitForServerToStart(): Unit = {\n    val w = Stopwatch.createStarted()\n    retry({\n      println(s\"Waiting for $serverName server at $uri to start since $w\")\n      require(getResponseCode() == 200)\n    }, 30, Some(1.second))\n  }\n\n  private def getResponseCode(): Int = {\n    val u = new URL(uri.toString())\n    val hc = u.openConnection().asInstanceOf[HttpURLConnection]\n    hc.setRequestMethod(\"GET\")\n    hc.connect()\n    hc.getResponseCode\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/ServiceInfoLogger.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.File\n\nimport org.apache.commons.lang3.StringUtils\nimport org.apache.openwhisk.standalone.ColorOutput.clr\n\nimport scala.io.AnsiColor\n\nclass ServiceInfoLogger(conf: Conf, services: Seq[ServiceContainer], workDir: File) extends AnsiColor {\n  private val separator = \"=\" * 80\n\n  def run(): Unit = {\n    println(separator)\n    println(\"Launched service details\")\n    println()\n    services.foreach(logService)\n    println()\n    println(s\"Local working directory - ${workDir.getAbsolutePath}\")\n    println(separator)\n  }\n\n  private def logService(s: ServiceContainer): Unit = {\n    val msg = s\"${portInfo(s.port)} ${s.description} (${clr(s.name, BOLD, conf.colorEnabled)})\"\n    println(msg)\n  }\n\n  private def portInfo(port: Int) = {\n    val msg = StringUtils.center(port.toString, 7)\n    s\"[${clr(msg, GREEN, conf.colorEnabled)}]\"\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/StandaloneDockerSupport.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.FileNotFoundException\nimport java.net.{ServerSocket, Socket}\nimport java.nio.file.{Files, Paths}\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.{ActorSystem, CoordinatedShutdown}\nimport org.apache.commons.lang3.SystemUtils\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.docker.{\n  BrokenDockerContainer,\n  DockerClient,\n  DockerClientConfig,\n  WindowsDockerClient\n}\nimport org.apache.openwhisk.core.containerpool.{ContainerAddress, ContainerId}\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext, Future}\nimport scala.sys.process._\nimport scala.util.Try\n\nclass StandaloneDockerSupport(docker: DockerClient)(implicit logging: Logging,\n                                                    ec: ExecutionContext,\n                                                    actorSystem: ActorSystem) {\n  CoordinatedShutdown(actorSystem)\n    .addTask(\n      CoordinatedShutdown.PhaseBeforeActorSystemTerminate,\n      \"cleanup containers launched for Standalone Server support\") { () =>\n      cleanup()\n      Future.successful(Done)\n    }\n\n  def cleanup(): Unit = {\n    implicit val transid = TransactionId(TransactionId.systemPrefix + \"standalone\")\n    val cleaning =\n      docker.ps(filters = Seq(\"name\" -> StandaloneDockerSupport.prefix), all = true).flatMap { containers =>\n        logging.info(this, s\"removing ${containers.size} containers launched for Standalone server support.\")\n        val removals = containers.map { id =>\n          docker.rm(id)\n        }\n        Future.sequence(removals)\n      }\n    Await.ready(cleaning, 30.seconds)\n  }\n}\n\ncase class ServiceContainer(port: Int, description: String, name: String)\n\nobject StandaloneDockerSupport {\n  val prefix = \"whisk-\"\n  val network = \"bridge\"\n\n  def checkOrAllocatePort(preferredPort: Int): Int = {\n    if (isPortFree(preferredPort)) preferredPort else freePort()\n  }\n\n  private def freePort(): Int = {\n    val socket = new ServerSocket(0)\n    try socket.getLocalPort\n    finally if (socket != null) socket.close()\n  }\n\n  def isPortFree(port: Int): Boolean = {\n    Try(new Socket(\"localhost\", port).close()).isFailure\n  }\n\n  def createRunCmd(name: String,\n                   environment: Map[String, String] = Map.empty,\n                   dockerRunParameters: Map[String, Set[String]] = Map.empty): Seq[String] = {\n    val environmentArgs = environment.flatMap {\n      case (key, value) => Seq(\"-e\", s\"$key=$value\")\n    }\n\n    val params = dockerRunParameters.flatMap {\n      case (key, valueList) => valueList.toList.flatMap(Seq(key, _))\n    }\n\n    Seq(\"--name\", name, \"--network\", network) ++\n      environmentArgs ++ params\n  }\n\n  def containerName(name: String) = {\n    prefix + name\n  }\n\n  /**\n   * Returns the hostname to access the playground.\n   * It defaults to localhost but it can be overridden\n   * and it is useful when the standalone is run in a container.\n   */\n  def getExternalHostName(): String = {\n    sys.props.get(\"whisk.standalone.host.external\").getOrElse(getLocalHostName())\n  }\n\n  /**\n   * Returns the address to be used by code running outside of container to connect to\n   * server. On non linux setups its 'localhost'. However for Linux setups its the ip used\n   * by docker for docker0 network to refer to host system\n   */\n  def getLocalHostName(): String = {\n    sys.props\n      .get(\"whisk.standalone.host.name\")\n      .getOrElse(if (SystemUtils.IS_OS_LINUX) hostIpLinux\n      else \"localhost\")\n  }\n\n  def getLocalHostIp(): String = {\n    sys.props\n      .get(\"whisk.standalone.host.ip\")\n      .getOrElse(\n        if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS)\n          hostIpNonLinux\n        else hostIpLinux)\n  }\n\n  /**\n   * Determines the name/ip which code running within container can use to connect back to Controller\n   */\n  def getLocalHostInternalName(): String = {\n    sys.props\n      .get(\"whisk.standalone.host.internal\")\n      .getOrElse(\n        if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS)\n          \"host.docker.internal\"\n        else hostIpLinux)\n  }\n\n  def prePullImage(imageName: String)(implicit logging: Logging): Unit = {\n    //docker images openwhisk/action-nodejs-v14:nightly\n    //REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE\n    //openwhisk/action-nodejs-v14   nightly             dbb0f8e1a050        5 days ago          967MB\n    val imageResult = s\"$dockerCmd images $imageName\".!!\n    val imageExist = imageResult.linesIterator.toList.size > 1\n    if (!imageExist || imageName.contains(\":nightly\")) {\n      logging.info(this, s\"Docker Pre pulling $imageName\")\n      s\"$dockerCmd pull $imageName\".!!\n    }\n  }\n\n  private lazy val hostIpLinux: String = {\n    //Gets the hostIp for linux https://github.com/docker/for-linux/issues/264#issuecomment-387525409\n    // Typical output would be like and we need line with default\n    // $ docker run --rm alpine ip route\n    // default via 172.17.0.1 dev eth0\n    // 172.17.0.0/16 dev eth0 scope link  src 172.17.0.2\n    val cmdResult = s\"$dockerCmd run --rm alpine ip route\".!!\n    cmdResult.linesIterator\n      .find(_.contains(\"default\"))\n      .map(_.split(' ').apply(2).trim)\n      .getOrElse(throw new IllegalStateException(s\"'ip route' result did not match expected output - \\n$cmdResult\"))\n  }\n\n  private lazy val hostIpNonLinux: String = {\n    //Gets the hostIp as names like host.docker.internal do not resolve for some reason in api gateway\n    //Based on https://unix.stackexchange.com/a/20793\n    //$ docker run --rm alpine getent hosts host.docker.internal\n    //192.168.65.2      host.docker.internal  host.docker.internal\n    val hostName = \"host.docker.internal\"\n    val cmdResult = s\"$dockerCmd run --rm alpine getent hosts $hostName\".!!\n    cmdResult.linesIterator\n      .find(_.contains(hostName))\n      .map(_.split(\" \").head.trim)\n      .getOrElse(throw new IllegalStateException(\n        s\"'getent hosts host.docker.internal' result did not match expected output - \\n$cmdResult\"))\n  }\n\n  private lazy val dockerCmd = {\n    //TODO Logic duplicated from DockerClient and WindowsDockerClient for now\n    val executable = loadConfig[String](\"whisk.docker.executable\").toOption\n    val alternatives =\n      List(\n        \"/usr/bin/docker\",\n        \"/usr/local/bin/docker\",\n        \"\"\"C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe\"\"\",\n        \"\"\"C:\\Program Files\\Docker\\Docker\\resources\\docker.exe\"\"\") ++ executable\n    Try {\n      alternatives.find(a => Files.isExecutable(Paths.get(a))).get\n    } getOrElse {\n      throw new FileNotFoundException(s\"Couldn't locate docker binary (tried: ${alternatives.mkString(\", \")}).\")\n    }\n  }\n}\n\nclass StandaloneDockerClient(pullDisabled: Boolean)(implicit log: Logging, as: ActorSystem, ec: ExecutionContext)\n    extends DockerClient()(ec)\n    with WindowsDockerClient {\n\n  override def pull(image: String)(implicit transid: TransactionId): Future[Unit] = {\n    if (pullDisabled) Future.successful(()) else super.pull(image)\n  }\n\n  override def runCmd(args: Seq[String], timeout: Duration, maskedArgs: Option[Seq[String]] = None)(\n    implicit transid: TransactionId): Future[String] =\n    super.runCmd(args, timeout, maskedArgs)\n\n  val clientConfig: DockerClientConfig = loadConfigOrThrow[DockerClientConfig](ConfigKeys.dockerClient)\n\n  def runDetached(image: String, args: Seq[String], shouldPull: Boolean)(\n    implicit tid: TransactionId): Future[StandaloneDockerContainer] = {\n    for {\n      _ <- if (shouldPull) pull(image) else Future.successful(())\n      id <- run(image, args).recoverWith {\n        case t @ BrokenDockerContainer(brokenId, _, _) =>\n          // Remove the broken container - but don't wait or check for the result.\n          // If the removal fails, there is nothing we could do to recover from the recovery.\n          rm(brokenId)\n          Future.failed(t)\n        case t => Future.failed(t)\n      }\n      ip <- inspectIPAddress(id, StandaloneDockerSupport.network).recoverWith {\n        // remove the container immediately if inspect failed as\n        // we cannot recover that case automatically\n        case e =>\n          rm(id)\n          Future.failed(e)\n      }\n    } yield StandaloneDockerContainer(id, ip)\n  }\n}\n\ncase class StandaloneDockerContainer(id: ContainerId, addr: ContainerAddress)\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/StandaloneOpenWhisk.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.{ByteArrayInputStream, File}\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.util.Properties\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.event.slf4j.SLF4JLogging\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport org.apache.commons.io.{FileUtils, FilenameUtils, IOUtils}\nimport org.apache.openwhisk.common.TransactionId.systemPrefix\nimport org.apache.openwhisk.common.{Config, Logging, PekkoLogging, TransactionId}\nimport org.apache.openwhisk.core.cli.WhiskAdmin\nimport org.apache.openwhisk.core.controller.Controller\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.standalone.ColorOutput.clr\nimport org.apache.openwhisk.standalone.StandaloneDockerSupport.checkOrAllocatePort\nimport org.rogach.scallop.ScallopConf\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext}\nimport scala.io.AnsiColor\nimport scala.util.{Failure, Success, Try}\nimport KafkaLauncher._\n\nclass Conf(arguments: Seq[String]) extends ScallopConf(Conf.expandAllMode(arguments)) {\n  import StandaloneOpenWhisk.preferredPgPort\n  banner(StandaloneOpenWhisk.banner)\n  footer(\"\\nOpenWhisk standalone server\")\n  StandaloneOpenWhisk.gitInfo.foreach(g => version(s\"Git Commit - ${g.commitId}\"))\n\n  this.printedName = \"openwhisk\"\n  val configFile =\n    opt[File](descr = \"application.conf which overrides the default standalone.conf\", validate = _.canRead)\n  val manifest = opt[File](descr = \"Manifest JSON defining the supported runtimes\", validate = _.canRead)\n  val port = opt[Int](descr = \"Server port\", default = Some(3233))\n\n  val verbose = tally()\n  val disableColorLogging = opt[Boolean](descr = \"Disables colored logging\", noshort = true)\n  val apiGw = opt[Boolean](descr = \"Enable API Gateway support\", noshort = true)\n  val couchdb = opt[Boolean](descr = \"Enable CouchDB support\", noshort = true)\n  val clean = opt[Boolean](descr = \"Clean any existing state like database\", noshort = true)\n  val devMode = opt[Boolean](\n    descr = \"Developer mode speeds up the startup by disabling preflight checks and avoiding explicit pulls.\",\n    noshort = true)\n  val apiGwPort = opt[Int](descr = \"API Gateway Port\", default = Some(3234), noshort = true)\n  val dataDir = opt[File](descr = \"Directory used for storage\", default = Some(StandaloneOpenWhisk.defaultWorkDir))\n\n  val kafka = opt[Boolean](descr = \"Enable embedded Kafka support\", noshort = true)\n  val kafkaUi = opt[Boolean](descr = \"Enable Kafka UI\", noshort = true)\n\n  //The port option below express following usage. Note that \"preferred\"\" port values are not configured as default\n  // on purpose\n  // - no config - Attempt to use default port. For e.g. 9092 for Kafka\n  // - non config if default preferred port is busy then select a random port\n  // - port config provided - Then this port would be used. If port is already busy\n  //   then that service would not start. This is mostly meant to be used for test setups\n  //   where test logic would determine a port and needs the service to start on that port only\n  val kafkaPort = opt[Int](\n    descr =\n      s\"Kafka port. If not specified then $preferredKafkaPort or some random free port (if $preferredKafkaPort is busy) would be used\",\n    noshort = true,\n    required = false)\n\n  val kafkaDockerPort = opt[Int](\n    descr = s\"Kafka port for use by docker based services. If not specified then $preferredKafkaDockerPort or some random free port \" +\n      s\"(if $preferredKafkaDockerPort is busy) would be used\",\n    noshort = true,\n    required = false)\n\n  val zkPort = opt[Int](\n    descr =\n      s\"Zookeeper port. If not specified then $preferredZkPort or some random free port (if $preferredZkPort is busy) would be used\",\n    noshort = true,\n    required = false)\n\n  val userEvents = opt[Boolean](descr = \"Enable User Events along with Prometheus and Grafana\", noshort = true)\n\n  val all = opt[Boolean](\n    descr = \"Enables all the optional services supported by Standalone OpenWhisk like CouchDB, Kafka etc\",\n    noshort = true)\n\n  val devKcf = opt[Boolean](descr = \"Enables KubernetesContainerFactory for local development\")\n\n  val noUi = opt[Boolean](descr = \"Disable Playground UI\", noshort = true)\n\n  val uiPort = opt[Int](\n    descr = s\"Playground UI server port. If not specified then $preferredPgPort or some random free port \" +\n      s\"(if $StandaloneOpenWhisk is busy) would be used\",\n    noshort = true)\n\n  val noBrowser = opt[Boolean](descr = \"Disable Launching Browser\", noshort = true)\n\n  val devUserEventsPort = opt[Int](\n    descr = \"Specify the port for the user-event service. This mode can be used for local \" +\n      \"development of user-event service by configuring Prometheus to connect to existing running service instance\")\n\n  val enableBootstrap = opt[Boolean](\n    descr =\n      \"Enable bootstrap of default users and actions like those needed for Api Gateway or Playground UI. \" +\n        \"By default bootstrap is done by default when using Memory store or default CouchDB support. \" +\n        \"When using other stores enable this flag to get bootstrap done\",\n    noshort = true)\n\n  mainOptions = Seq(manifest, configFile, apiGw, couchdb, userEvents, kafka, kafkaUi)\n\n  verify()\n\n  val colorEnabled = !disableColorLogging()\n\n  def serverUrl: Uri = Uri(s\"http://${StandaloneDockerSupport.getLocalHostName()}:${port()}\")\n}\n\nobject Conf {\n  def expandAllMode(args: Seq[String]): Seq[String] = {\n    if (args.contains(\"--all\")) {\n      val svcs = Seq(\"api-gw\", \"couchdb\", \"user-events\", \"kafka\", \"kafka-ui\")\n      val buf = args.toBuffer\n      svcs.foreach { s =>\n        val arg = \"--\" + s\n        if (!buf.contains(arg)) {\n          buf += arg\n        }\n      }\n      buf.toList\n    } else {\n      args\n    }\n  }\n}\n\ncase class GitInfo(commitId: String, commitTime: String)\n\nobject StandaloneConfigKeys {\n  val usersConfigKey = \"whisk.users\"\n  val redisConfigKey = \"whisk.standalone.redis\"\n  val apiGwConfigKey = \"whisk.standalone.api-gateway\"\n  val couchDBConfigKey = \"whisk.standalone.couchdb\"\n  val userEventConfigKey = \"whisk.standalone.user-events\"\n}\n\nobject StandaloneOpenWhisk extends SLF4JLogging {\n\n  val banner =\n    \"\"\"\n      |        ____      ___                   _    _ _     _     _\n      |       /\\   \\    / _ \\ _ __   ___ _ __ | |  | | |__ (_)___| | __\n      |  /\\  /__\\   \\  | | | | '_ \\ / _ \\ '_ \\| |  | | '_ \\| / __| |/ /\n      | /  \\____ \\  /  | |_| | |_) |  __/ | | | |/\\| | | | | \\__ \\   <\n      | \\   \\  /  \\/    \\___/| .__/ \\___|_| |_|__/\\__|_| |_|_|___/_|\\_\\\n      |  \\___\\/ tm           |_|\n    \"\"\".stripMargin\n\n  val defaultRuntime = \"\"\"{\n     |  \"runtimes\": {\n     |    \"nodejs\": [\n     |      {\n     |        \"kind\": \"nodejs:20\",\n     |        \"default\": true,\n     |        \"image\": {\n     |          \"prefix\": \"openwhisk\",\n     |          \"name\": \"action-nodejs-v20\",\n     |          \"tag\": \"latest\"\n     |        },\n     |        \"deprecated\": false,\n     |        \"attached\": {\n     |          \"attachmentName\": \"codefile\",\n     |          \"attachmentType\": \"text/plain\"\n     |        },\n     |        \"stemCells\": [\n     |          {\n     |            \"count\": 1,\n     |            \"memory\": \"256 MB\"\n     |          }\n     |        ]\n     |      }\n     |    ]\n     |  }\n     |}\n     |\"\"\".stripMargin\n\n  val gitInfo: Option[GitInfo] = loadGitInfo()\n\n  val defaultWorkDir = new File(FilenameUtils.concat(FileUtils.getUserDirectoryPath, \".openwhisk/standalone\"))\n\n  val wskPath = System.getProperty(\"whisk.standalone.wsk\", \"wsk\")\n\n  val preferredPgPort = 3232\n\n  private val systemUser = \"whisk.system\"\n\n  def main(args: Array[String]): Unit = {\n    val conf = new Conf(args)\n\n    printBanner(conf)\n    if (!conf.devMode()) {\n      PreFlightChecks(conf).run()\n    }\n\n    configureLogging(conf)\n    initialize(conf)\n    //Create actor system only after initializing the config\n    implicit val actorSystem: ActorSystem = ActorSystem(\"standalone-actor-system\")\n    implicit val logger: Logging = createLogging(actorSystem, conf)\n    implicit val ec: ExecutionContext = actorSystem.dispatcher\n\n    val owPort = conf.port()\n    val (dataDir, workDir) = initializeDirs(conf)\n    val (dockerClient, dockerSupport) = prepareDocker(conf)\n\n    val defaultSvcs = Seq(\n      ServiceContainer(owPort, s\"http://${StandaloneDockerSupport.getLocalHostName()}:$owPort\", \"Controller\"))\n\n    val (apiGwApiPort, apiGwSvcs) = if (conf.apiGw()) {\n      startApiGateway(conf, dockerClient, dockerSupport)\n    } else (-1, Seq.empty)\n\n    val (kafkaDockerPort, kafkaSvcs) = if (conf.kafka() || conf.userEvents()) {\n      startKafka(workDir, dockerClient, conf, conf.kafkaUi())\n    } else (-1, Seq.empty)\n\n    val couchSvcs = if (conf.couchdb()) Some(startCouchDb(dataDir, dockerClient)) else None\n    val userEventSvcs =\n      if (conf.userEvents() || conf.devUserEventsPort.isSupplied)\n        startUserEvents(conf.port(), kafkaDockerPort, conf.devUserEventsPort.toOption, workDir, dataDir, dockerClient)\n      else Seq.empty\n\n    val pgLauncher = if (conf.noUi()) None else Some(createPgLauncher(owPort, conf))\n    val pgSvc = pgLauncher.map(pg => Seq(pg.run())).getOrElse(Seq.empty)\n\n    val svcs = Seq(defaultSvcs, apiGwSvcs, couchSvcs.toList, kafkaSvcs, userEventSvcs, pgSvc).flatten\n    new ServiceInfoLogger(conf, svcs, dataDir).run()\n\n    startServer(conf)\n    new ServerStartupCheck(conf.serverUrl, \"OpenWhisk\").waitForServerToStart()\n\n    if (canInstallUserAndActions(conf)) {\n      if (conf.apiGw()) {\n        installRouteMgmt(conf, workDir, apiGwApiPort)\n      }\n      pgLauncher.foreach(_.install())\n    }\n  }\n\n  def initialize(conf: Conf): Unit = {\n    configureBuildInfo()\n    configureServerPort(conf)\n    configureOSSpecificOpts()\n    initConfigLocation(conf)\n    configureRuntimeManifest(conf)\n    loadWhiskConfig()\n\n    if (conf.devMode()) {\n      configureDevMode()\n    }\n  }\n\n  def startServer(conf: Conf)(implicit actorSystem: ActorSystem, logging: Logging): Unit = {\n    if (canInstallUserAndActions(conf)) {\n      bootstrapUsers()\n    }\n    startController()\n  }\n\n  private def configureServerPort(conf: Conf) = {\n    val port = conf.port()\n    log.info(s\"Starting OpenWhisk standalone on port $port\")\n    System.setProperty(WhiskConfig.disableWhiskPropsFileRead, \"true\")\n    setConfigProp(WhiskConfig.servicePort, port.toString)\n    setConfigProp(WhiskConfig.wskApiPort, port.toString)\n    setConfigProp(WhiskConfig.wskApiProtocol, \"http\")\n\n    //Using hostInternalName instead of getLocalHostIp as using docker alpine way to\n    //determine the ip is seen to be failing with older version of Docker\n    //So to keep main flow which does not use api gw working fine play safe\n    setConfigProp(WhiskConfig.wskApiHostname, StandaloneDockerSupport.getLocalHostInternalName())\n  }\n\n  private def initConfigLocation(conf: Conf): Unit = {\n    conf.configFile.toOption match {\n      case Some(f) =>\n        require(f.exists(), s\"Config file $f does not exist\")\n        System.setProperty(\"config.file\", f.getAbsolutePath)\n      case None =>\n        val config = if (conf.devKcf()) {\n          \"standalone-kcf.conf\"\n        } else \"standalone.conf\"\n        System.setProperty(\"config.resource\", config)\n\n    }\n  }\n\n  private def configKey(k: String): String = Config.prefix + k.replace('-', '.')\n\n  private def loadWhiskConfig(): Unit = {\n    val config = loadConfigOrThrow[Map[String, String]](ConfigKeys.whiskConfig)\n    config.foreach { case (k, v) => setConfigProp(k, v) }\n  }\n\n  private def configureRuntimeManifest(conf: Conf): Unit = {\n    val manifest = conf.manifest.toOption match {\n      case Some(file) =>\n        FileUtils.readFileToString(file, UTF_8)\n      case None => {\n        //Fallback to a default runtime in case resource not found. Say while running from IDE\n        Try(IOUtils.resourceToString(\"/runtimes.json\", UTF_8)).getOrElse(defaultRuntime)\n      }\n    }\n    setConfigProp(WhiskConfig.runtimesManifest, manifest)\n  }\n\n  private def setConfigProp(key: String, value: String): Unit = {\n    setSysProp(configKey(key), value)\n  }\n\n  private def startController()(implicit actorSystem: ActorSystem, logger: Logging): Unit = {\n    Controller.start(Array(\"standalone\"))\n  }\n\n  private def bootstrapUsers()(implicit actorSystem: ActorSystem, logging: Logging): Unit = {\n    implicit val userTid: TransactionId = TransactionId(systemPrefix + \"userBootstrap\")\n    getUsers().foreach {\n      case (subject, key) =>\n        val conf = new org.apache.openwhisk.core.cli.Conf(Seq(\"user\", \"create\", \"--auth\", key, subject))\n        val admin = WhiskAdmin(conf)\n        Await.ready(admin.executeCommand(), 60.seconds)\n        logging.info(this, s\"Created user [$subject]\")\n    }\n  }\n\n  private def configureOSSpecificOpts(): Unit = {\n    //Set the interface based on OS\n    setSysProp(\"whisk.controller.interface\", StandaloneDockerSupport.getLocalHostName())\n  }\n\n  private def loadGitInfo() = {\n    val info = loadPropResource(\"git.properties\")\n    for {\n      commit <- info.get(\"git.commit.id.abbrev\")\n      time <- info.get(\"git.commit.time\")\n    } yield GitInfo(commit, time)\n  }\n\n  private def printBanner(conf: Conf): Unit = {\n    val bannerTxt = clr(banner, AnsiColor.CYAN, conf.colorEnabled)\n    println(bannerTxt)\n    gitInfo.foreach(g => println(s\"Git Commit: ${g.commitId}, Build Date: ${g.commitTime}\"))\n  }\n\n  private def configureBuildInfo(): Unit = {\n    gitInfo.foreach { g =>\n      setSysProp(\"whisk.info.build-no\", g.commitId)\n      setSysProp(\"whisk.info.date\", g.commitTime)\n    }\n  }\n\n  private def setSysProp(key: String, value: String): Unit = {\n    Option(System.getProperty(key)) match {\n      case Some(x) if x != value =>\n        log.info(s\"Founding existing value for system property '$key'- Going to set '$value' , found '$x'\")\n      case _ =>\n        System.setProperty(key, value)\n    }\n  }\n\n  private def loadPropResource(name: String): Map[String, String] = {\n    Try {\n      val propString = IOUtils.resourceToString(\"/\" + name, UTF_8)\n      val props = new Properties()\n      props.load(new ByteArrayInputStream(propString.getBytes(UTF_8)))\n      props.asScala.toMap\n    }.getOrElse(Map.empty)\n  }\n\n  private def configureLogging(conf: Conf): Unit = {\n    if (System.getProperty(\"logback.configurationFile\") == null && !conf.disableColorLogging()) {\n      LogbackConfigurator.configureLogbackFromResource(\"logback-standalone.xml\")\n    }\n    LogbackConfigurator.initLogging(conf)\n  }\n\n  private def createLogging(actorSystem: ActorSystem, conf: Conf): Logging = {\n    val adapter = org.apache.pekko.event.Logging.getLogger(actorSystem, this)\n    if (conf.disableColorLogging())\n      new PekkoLogging(adapter)\n    else\n      new ColoredPekkoLogging(adapter)\n  }\n\n  private def prepareDocker(conf: Conf)(implicit logging: Logging,\n                                        as: ActorSystem,\n                                        ec: ExecutionContext): (StandaloneDockerClient, StandaloneDockerSupport) = {\n    //In dev mode disable the pull\n    val pullDisabled = conf.devMode()\n    val dockerClient = new StandaloneDockerClient(pullDisabled)\n    val dockerSupport = new StandaloneDockerSupport(dockerClient)\n\n    //Remove any existing launched containers\n    dockerSupport.cleanup()\n    (dockerClient, dockerSupport)\n  }\n\n  private def startApiGateway(conf: Conf, dockerClient: StandaloneDockerClient, dockerSupport: StandaloneDockerSupport)(\n    implicit logging: Logging,\n    as: ActorSystem,\n    ec: ExecutionContext): (Int, Seq[ServiceContainer]) = {\n    implicit val tid: TransactionId = TransactionId(systemPrefix + \"apiMgmt\")\n\n    // api port is the port used by rout management actions to configure the api gw upon wsk api commands\n    // mgmt port is the port used by end user while making actual use of api gw\n    val apiGwApiPort = StandaloneDockerSupport.checkOrAllocatePort(9000)\n    val apiGwMgmtPort = conf.apiGwPort()\n\n    val gw = new ApiGwLauncher(dockerClient, apiGwApiPort, apiGwMgmtPort, conf.port())\n    val f = gw.run()\n    val g = f.andThen {\n      case Success(_) =>\n        logging.info(\n          this,\n          s\"Api Gateway started successfully at http://${StandaloneDockerSupport.getLocalHostName()}:$apiGwMgmtPort\")\n      case Failure(t) =>\n        logging.error(this, \"Error starting Api Gateway\" + t)\n    }\n    val services = Await.result(g, 5.minutes)\n    (apiGwApiPort, services)\n  }\n\n  private def installRouteMgmt(conf: Conf, workDir: File, apiGwApiPort: Int)(implicit logging: Logging): Unit = {\n    val apiGwHostv2 = s\"http://${StandaloneDockerSupport.getLocalHostIp()}:$apiGwApiPort/v2\"\n    val authKey = systemAuthKey\n    val installer = InstallRouteMgmt(workDir, authKey, conf.serverUrl, \"/\" + systemUser, Uri(apiGwHostv2), wskPath)\n    installer.run()\n  }\n\n  private def initializeDirs(conf: Conf)(implicit logging: Logging): (File, File) = {\n    val baseDir = conf.dataDir()\n    val thisServerDir = s\"server-${conf.port()}\"\n    val dataDir = new File(baseDir, thisServerDir)\n    if (conf.clean() && dataDir.exists()) {\n      FileUtils.deleteDirectory(dataDir)\n      logging.info(this, s\"Cleaned existing directory ${dataDir.getAbsolutePath}\")\n    }\n    FileUtils.forceMkdir(dataDir)\n    log.info(s\"Using [${dataDir.getAbsolutePath}] as data directory\")\n\n    val workDir = new File(dataDir, \"tmp\")\n    FileUtils.deleteDirectory(workDir)\n    FileUtils.forceMkdir(workDir)\n    (dataDir, workDir)\n  }\n\n  private def getUsers(): Map[String, String] = {\n    val m = loadConfigOrThrow[Map[String, String]](StandaloneConfigKeys.usersConfigKey)\n    m.map { case (name, key) => (name.replace('-', '.'), key) }\n  }\n\n  private def startCouchDb(dataDir: File, dockerClient: StandaloneDockerClient)(\n    implicit logging: Logging,\n    as: ActorSystem,\n    ec: ExecutionContext): ServiceContainer = {\n    implicit val tid: TransactionId = TransactionId(systemPrefix + \"couchDB\")\n    val port = checkOrAllocatePort(5984)\n    val dbDataDir = new File(dataDir, \"couchdb\")\n    FileUtils.forceMkdir(dbDataDir)\n    val db = new CouchDBLauncher(dockerClient, port, dbDataDir)\n    val f = db.run()\n    val g = f.andThen {\n      case Success(_) =>\n      case Failure(t) =>\n        logging.error(this, \"Error starting CouchDB\" + t)\n    }\n    Await.result(g, 5.minutes)\n  }\n\n  private def startKafka(workDir: File, dockerClient: StandaloneDockerClient, conf: Conf, kafkaUi: Boolean)(\n    implicit logging: Logging,\n    as: ActorSystem,\n    ec: ExecutionContext): (Int, Seq[ServiceContainer]) = {\n    implicit val tid: TransactionId = TransactionId(systemPrefix + \"kafka\")\n    val kafkaPort = getPort(conf.kafkaPort.toOption, preferredKafkaPort)\n    val kafkaDockerPort = getPort(conf.kafkaDockerPort.toOption, preferredKafkaDockerPort)\n    val k = new KafkaLauncher(\n      dockerClient,\n      kafkaPort,\n      kafkaDockerPort,\n      getPort(conf.zkPort.toOption, preferredZkPort),\n      workDir,\n      kafkaUi)\n\n    val f = k.run()\n    val g = f.andThen {\n      case Success(_) =>\n        logging.info(\n          this,\n          s\"Kafka started successfully at http://${StandaloneDockerSupport.getLocalHostName()}:$kafkaPort\")\n      case Failure(t) =>\n        logging.error(this, \"Error starting Kafka\" + t)\n    }\n    val services = Await.result(g, 5.minutes)\n\n    setConfigProp(WhiskConfig.kafkaHostList, s\"localhost:$kafkaPort\")\n    setSysProp(\"whisk.spi.MessagingProvider\", \"org.apache.openwhisk.connector.kafka.KafkaMessagingProvider\")\n    setSysProp(\"whisk.spi.LoadBalancerProvider\", \"org.apache.openwhisk.standalone.KafkaAwareLeanBalancer\")\n    (kafkaDockerPort, services)\n  }\n\n  private def startUserEvents(owPort: Int,\n                              kafkaDockerPort: Int,\n                              existingUserEventSvcPort: Option[Int],\n                              workDir: File,\n                              dataDir: File,\n                              dockerClient: StandaloneDockerClient)(implicit logging: Logging,\n                                                                    as: ActorSystem,\n                                                                    ec: ExecutionContext): Seq[ServiceContainer] = {\n    implicit val tid: TransactionId = TransactionId(systemPrefix + \"userevents\")\n    val k = new UserEventLauncher(dockerClient, owPort, kafkaDockerPort, existingUserEventSvcPort, workDir, dataDir)\n\n    val f = k.run()\n    val g = f.andThen {\n      case Success(_) =>\n        logging.info(this, s\"User events started successfully\")\n      case Failure(t) =>\n        logging.error(this, \"Error starting Kafka\" + t)\n    }\n    Await.result(g, 5.minutes)\n  }\n\n  private def getPort(configured: Option[Int], preferred: Int): Int = {\n    configured.getOrElse(checkOrAllocatePort(preferred))\n  }\n\n  private def configureDevMode(): Unit = {\n    setSysProp(\"whisk.docker.standalone.container-factory.pull-standard-images\", \"false\")\n  }\n\n  private def createPgLauncher(owPort: Int,\n                               conf: Conf)(implicit logging: Logging, as: ActorSystem, ec: ExecutionContext) = {\n    implicit val tid: TransactionId = TransactionId(systemPrefix + \"playground\")\n    val pgPort = getPort(conf.uiPort.toOption, preferredPgPort)\n    new PlaygroundLauncher(\n      StandaloneDockerSupport.getLocalHostName(),\n      StandaloneDockerSupport.getExternalHostName(),\n      owPort,\n      pgPort,\n      systemAuthKey,\n      conf.devMode(),\n      conf.noBrowser())\n  }\n\n  private def systemAuthKey: String = {\n    getUsers().getOrElse(systemUser, throw new Exception(s\"Did not found auth key for $systemUser\"))\n  }\n\n  private def canInstallUserAndActions(conf: Conf)(implicit logging: Logging, actorSystem: ActorSystem): Boolean = {\n    val config = actorSystem.settings.config\n    val artifactStore = config.getString(\"whisk.spi.ArtifactStoreProvider\")\n    if (conf.couchdb() || artifactStore == \"org.apache.openwhisk.core.database.memory.MemoryArtifactStoreProvider\") {\n      true\n    } else if (conf.enableBootstrap()) {\n      logging.info(this, \"Bootstrap is enabled for external ArtifactStore\")\n      true\n    } else {\n      logging.info(\n        this,\n        s\"Bootstrap is not enabled as connecting to external ArtifactStore. \" +\n          s\"Start with ${conf.enableBootstrap.name} to bootstrap default users and action\")\n      false\n    }\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/Unzip.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.{File, FileOutputStream, InputStream}\nimport java.util.zip.ZipInputStream\n\nobject Unzip {\n\n  def apply(is: InputStream, dir: File): Unit = {\n    //Based on https://stackoverflow.com/a/40547896/1035417\n    val zis = new ZipInputStream((is))\n    val dest = dir.toPath\n    Stream.continually(zis.getNextEntry).takeWhile(_ != null).foreach { zipEntry =>\n      if (!zipEntry.isDirectory) {\n        val outPath = dest.resolve(zipEntry.getName)\n        val outPathParent = outPath.getParent\n        if (!outPathParent.toFile.exists()) {\n          outPathParent.toFile.mkdirs()\n        }\n\n        val outFile = outPath.toFile\n        val out = new FileOutputStream(outFile)\n        val buffer = new Array[Byte](4096)\n        Stream.continually(zis.read(buffer)).takeWhile(_ != -1).foreach(out.write(buffer, 0, _))\n        out.close()\n      }\n    }\n    zis.close()\n  }\n\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/UserEventLauncher.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.File\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.commons.io.{FileUtils, IOUtils}\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.standalone.StandaloneDockerSupport.{checkOrAllocatePort, containerName, createRunCmd}\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass UserEventLauncher(\n  docker: StandaloneDockerClient,\n  owPort: Int,\n  kafkaDockerPort: Int,\n  existingUserEventSvcPort: Option[Int],\n  workDir: File,\n  dataDir: File)(implicit logging: Logging, ec: ExecutionContext, actorSystem: ActorSystem, tid: TransactionId) {\n\n  //owPort+1 is used by Api Gateway\n  private val userEventPort = existingUserEventSvcPort.getOrElse(checkOrAllocatePort(owPort + 2))\n  private val prometheusPort = checkOrAllocatePort(9090)\n  private val grafanaPort = checkOrAllocatePort(3000)\n\n  case class UserEventConfig(image: String, prometheusImage: String, grafanaImage: String)\n\n  private val userEventConfig = loadConfigOrThrow[UserEventConfig](StandaloneConfigKeys.userEventConfigKey)\n\n  private val hostIp = StandaloneDockerSupport.getLocalHostIp()\n\n  def run(): Future[Seq[ServiceContainer]] = {\n    for {\n      userEvent <- runUserEvents()\n      (promContainer, promSvc) <- runPrometheus()\n      grafanaSvc <- runGrafana(promContainer)\n    } yield {\n      logging.info(this, \"Enabled the user-event config\")\n      System.setProperty(\"whisk.user-events.enabled\", \"true\")\n      Seq(userEvent, promSvc, grafanaSvc)\n    }\n  }\n\n  def runUserEvents(): Future[ServiceContainer] = {\n    existingUserEventSvcPort match {\n      case Some(_) =>\n        logging.info(this, s\"Connecting to pre existing user-event service at $userEventPort\")\n        Future.successful(ServiceContainer(userEventPort, s\"http://localhost:$userEventPort\", \"Existing user-event\"))\n      case None =>\n        val env = Map(\"KAFKA_HOSTS\" -> s\"$hostIp:$kafkaDockerPort\")\n\n        logging.info(this, s\"Starting User Events: $userEventPort\")\n        val name = containerName(\"user-events\")\n        val params = Map(\"-p\" -> Set(s\"$userEventPort:9095\"))\n        val args = createRunCmd(name, env, params)\n\n        val f = docker.runDetached(userEventConfig.image, args, true)\n        f.map(_ => ServiceContainer(userEventPort, s\"http://localhost:$userEventPort\", name))\n    }\n  }\n\n  def runPrometheus(): Future[(StandaloneDockerContainer, ServiceContainer)] = {\n    logging.info(this, s\"Starting Prometheus at $prometheusPort\")\n    val baseParams = Map(\"-p\" -> Set(s\"$prometheusPort:9090\"))\n    val promConfigDir = newDir(workDir, \"prometheus\")\n    val promDataDir = newDir(dataDir, \"prometheus\")\n\n    val configFile = new File(promConfigDir, \"prometheus.yml\")\n    FileUtils.write(configFile, prometheusConfig, UTF_8)\n\n    val volParams = Map(\n      \"-v\" -> Set(s\"${promDataDir.getAbsolutePath}:/prometheus\", s\"${promConfigDir.getAbsolutePath}:/etc/prometheus/\"))\n    val name = containerName(\"prometheus\")\n    val args = createRunCmd(name, Map.empty, baseParams ++ volParams)\n    val f = docker.runDetached(userEventConfig.prometheusImage, args, shouldPull = true)\n    val sc = ServiceContainer(prometheusPort, s\"http://localhost:$prometheusPort\", name)\n    f.map(c => (c, sc))\n  }\n\n  def runGrafana(promContainer: StandaloneDockerContainer): Future[ServiceContainer] = {\n    logging.info(this, s\"Starting Grafana at $grafanaPort\")\n    val baseParams = Map(\"-p\" -> Set(s\"$grafanaPort:3000\"))\n    val grafanaConfigDir = newDir(workDir, \"grafana\")\n    val grafanaDataDir = newDir(dataDir, \"grafana\")\n\n    val promUrl = s\"http://$hostIp:$prometheusPort\"\n    unzipGrafanaConfig(grafanaConfigDir, promUrl)\n\n    val env = Map(\n      \"GF_PATHS_PROVISIONING\" -> \"/etc/grafana/provisioning\",\n      \"GF_USERS_ALLOW_SIGN_UP\" -> \"false\",\n      \"GF_AUTH_ANONYMOUS_ENABLED\" -> \"true\",\n      \"GF_AUTH_ANONYMOUS_ORG_NAME\" -> \"Main Org.\",\n      \"GF_AUTH_ANONYMOUS_ORG_ROLE\" -> \"Admin\")\n\n    val volParams = Map(\n      \"-v\" -> Set(\n        s\"${grafanaDataDir.getAbsolutePath}:/var/lib/grafana\",\n        s\"${grafanaConfigDir.getAbsolutePath}/provisioning/:/etc/grafana/provisioning/\",\n        s\"${grafanaConfigDir.getAbsolutePath}/dashboards/:/var/lib/grafana/dashboards/\"))\n    val name = containerName(\"grafana\")\n    val args = createRunCmd(name, env, baseParams ++ volParams)\n    val f = docker.runDetached(userEventConfig.grafanaImage, args, shouldPull = true)\n    val sc = ServiceContainer(grafanaPort, s\"http://localhost:$grafanaPort\", name)\n    f.map(_ => sc)\n  }\n\n  private def prometheusConfig = {\n    val config = IOUtils.resourceToString(\"/prometheus.yml\", UTF_8)\n    val pattern = \"'user-events:9095'\"\n    require(config.contains(pattern), s\"Did not found expected pattern $pattern in prometheus config $config\")\n\n    val targets = s\"'$hostIp:$userEventPort', '$hostIp:$owPort'\"\n    config.replace(pattern, targets)\n  }\n\n  private def unzipGrafanaConfig(configDir: File, promUrl: String): Unit = {\n    val is = getClass.getResourceAsStream(\"/grafana-config.zip\")\n    if (is != null) {\n      Unzip(is, configDir)\n      val configFile = new File(configDir, \"provisioning/datasources/datasource.yml\")\n      val config = FileUtils.readFileToString(configFile, UTF_8)\n      val updatedConfig = config.replace(\"http://prometheus:9090\", promUrl)\n      FileUtils.write(configFile, updatedConfig, UTF_8)\n    } else {\n      logging.warn(\n        this,\n        \"Did not found the grafana-config.zip in classpath. Make sure its packaged and present in classpath\")\n    }\n  }\n\n  private def newDir(baseDir: File, name: String) = {\n    val dir = new File(baseDir, name)\n    FileUtils.forceMkdir(dir)\n    dir\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/main/scala/org/apache/openwhisk/standalone/Wsk.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.Uri.Query\nimport org.apache.pekko.http.scaladsl.model.headers.{Accept, Authorization, BasicHttpCredentials}\nimport org.apache.pekko.http.scaladsl.model.{HttpHeader, HttpMethods, MediaTypes, Uri}\nimport org.apache.openwhisk.core.database.PutException\nimport org.apache.openwhisk.http.PoolingRestClient\nimport spray.json._\n\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass Wsk(host: String, port: Int, authKey: String)(implicit system: ActorSystem) extends DefaultJsonProtocol {\n  import PoolingRestClient._\n  private implicit val ec: ExecutionContext = system.dispatcher\n  private val client = new PoolingRestClient(\"http\", host, port, 10)\n  private val baseHeaders: List[HttpHeader] = {\n    val Array(username, password) = authKey.split(':')\n    List(Authorization(BasicHttpCredentials(username, password)), Accept(MediaTypes.`application/json`))\n  }\n\n  def updatePgAction(name: String, content: String): Future[Done] = {\n    val js = actionJson(name, content)\n    val params = Map(\"overwrite\" -> \"true\")\n    val uri = Uri(s\"/api/v1/namespaces/_/actions/$name\").withQuery(Query(params))\n    client.requestJson[JsObject](mkJsonRequest(HttpMethods.PUT, uri, js, baseHeaders)).map {\n      case Right(_)     => Done\n      case Left(status) => throw PutException(s\"Error creating action $name \" + status)\n    }\n  }\n\n  private def actionJson(name: String, code: String) = {\n    s\"\"\"{\n      |    \"namespace\": \"_\",\n      |    \"name\": \"$name\",\n      |    \"exec\": {\n      |        \"kind\": \"nodejs:default\",\n      |        \"code\": ${quote(code)}\n      |    },\n      |    \"annotations\": [{\n      |        \"key\": \"provide-api-key\",\n      |        \"value\": true\n      |    }, {\n      |        \"key\": \"web-export\",\n      |        \"value\": true\n      |    }, {\n      |        \"key\": \"raw-http\",\n      |        \"value\": false\n      |    }, {\n      |        \"key\": \"final\",\n      |        \"value\": true\n      |    }],\n      |    \"parameters\": [{\n      |        \"key\": \"__ignore_certs\",\n      |        \"value\": true\n      |    }]\n      |}\"\"\".stripMargin.parseJson.asJsObject\n  }\n\n  private def quote(code: String) = {\n    JsString(code).compactPrint\n  }\n}\n"
  },
  {
    "path": "core/standalone/src/test/scala/org/apache/openwhisk/standalone/DockerVersionTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass DockerVersionTests extends AnyFlatSpec with Matchers {\n  behavior of \"DockerVersion\"\n\n  it should \"parse docker version\" in {\n    val v = DockerVersion(\"18.09.2\")\n    v shouldBe DockerVersion(18, 9, 2)\n  }\n\n  it should \"parse docker version from command output\" in {\n    val v = DockerVersion.fromVersionCommand(\"Docker version 18.09.2, build 624796\")\n    v shouldBe DockerVersion(18, 9, 2)\n  }\n\n  it should \"compare 2 versions semantically\" in {\n    DockerVersion(\"17.09.2\") should be < DockerVersion(\"18.09.2\")\n    DockerVersion(\"17.09.2\") should be < DockerVersion(\"18.03.2\")\n    DockerVersion(\"17.09\") should be < DockerVersion(\"18.03.2\")\n  }\n}\n"
  },
  {
    "path": "core/standalone/start.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nIMAGE=\"${1:-openwhisk/standalone:nightly}\"\nshift\ndocker run --rm -d \\\n  -h openwhisk --name openwhisk \\\n  -p 3233:3233 -p 3232:3232 \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n \"$IMAGE\" \"$@\"\ndocker exec openwhisk waitready\ncase \"$(uname)\" in\n (Linux) xdg-open http://localhost:3232 ;;\n (Darwin) open http://localhost:3232 ;;\n (MINGW*) start http://localhost:3232 ;;\n (*) echo Please use http://localhost:3232 for playground ;;\nesac\n"
  },
  {
    "path": "core/standalone/stop.sh",
    "content": "#!/bin/sh\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\ndocker exec openwhisk stop\n"
  },
  {
    "path": "docs/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Getting started with OpenWhisk\n\nOpenWhisk is an [Apache Software Foundation](https://www.apache.org/) (ASF) project. It is an open source implementation of a distributed, event-driven compute service. You can run it on your own hardware on-prem, or in the cloud. When running in the cloud you could use a Platform as a Service (PaaS) version of the OpenWhisk provided by IBM Cloud Functions, or you can provision it yourself into Infrastructure as a Service (IaaS) clouds, such as IBM Cloud, Amazon EC2, Microsoft Azure, Google GCP, etc.\n\nOpenWhisk runs application logic in response to events or direct invocations from web or mobile apps over HTTP. Events can be provided from IBM Cloud services like Cloudant and from external sources. Developers can focus on writing application logic, and creating actions that are executed on demand. The benefits of this new paradigm are that you do not explicitly provision servers and worry about auto-scaling, or worry about high availability, updates, maintenance and pay for hours of processor time when your server is running but not serving requests. Your code executes whenever there is an HTTP call, database state change, or other type of event that triggers the execution of your code. You get billed by millisecond of execution time (rounded up to the nearest 100ms in case of OpenWhisk) or on some platforms per request (not supported on OpenWhisk yet), not per hour of JVM regardless whether that VM was doing useful work or not.\n\nThis programming model is a perfect match for microservices, mobile, IoT and many other apps – you get inherent auto-scaling and load balancing out of the box without having to manually configure clusters, load balancers, http plugins, etc. All you need to do is to provide the code you want to execute and give it to your cloud vendor. The rest is “magic”. A good introduction into the serverless programming model is available on [Martin Fowler's blog](https://martinfowler.com/articles/serverless.html).\n\n## Overview\n- [How OpenWhisk works](./about.md)\n- [Common uses cases for Serverless applications](./use_cases.md)\n- [Sample applications](./samples.md)\n\n<!--\n- [Is serverless a good fit for my application?](./goodfit.md)\n-->\n\n<!-- TODO - need to add the following items and pages in the future:\n- Concurrency\n- Error processing\n- Security\n- Scalability\n- Logging\n- Pricing (for IBM Cloud only)\n-->\n\n## Implementation guide\n- [Setting up and using OpenWhisk CLI](./cli.md)\n- [Using REST APIs with OpenWhisk](./rest_api.md)\n\n<!-- TODO - need to add the following items and pages in the future:\n- Development workflow\n- IDE and toolchain\n- When OpenWhisk is a good choice and when it is not\n- Building applications from scratch\n- Extending existing applications\n- Using OpenWhisk as the integration tool\n- Integrating with API Management\n- Running OpenWhisk (PaaS versus RYO, private, public, dedicated)\n- Best practices for rules\n- Performance considerations\n- Deployment of applications\n- Debugging\n- Monitoring of applications\n- Writing applications with maximum portability\n- Publishing events\n- Out of the box services and triggers\n- CDN integration\n- Security\n- Limitations\n-->\n\n## Programming model\n- [System details](./reference.md)\n- [Component clustering](./deploy.md)\n- [Catalog of OpenWhisk provided services](./catalog.md)\n- [Actions](./actions.md)\n- [Triggers and Rules](./triggers_rules.md)\n- [Feeds](./feeds.md)\n- [Packages](./packages.md)\n- [Annotations](./annotations.md)\n- [Web actions](./webactions.md)\n- [API Gateway](./apigateway.md)\n\n<!-- TODO - need to add the following items and pages in the future:\n- Concurrency\n- Error processing\n- Integration with Serverless Framework\n-->\n\nOfficial OpenWhisk project website [http://OpenWhisk.org](http://openwhisk.org).\n\n<!-- ## Setting up the OpenWhisk CLI - moved to cli.md -->\n\n<!-- ## Using REST APIs with OpenWhisk - moved to rest_api.md -->\n\n<!-- ## OpenWhisk Hello World example - moved to samples.md -->\n"
  },
  {
    "path": "docs/about.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# System overview\n\nOpenWhisk is an event-driven compute platform also referred to as Serverless computing or as Function as a Service (FaaS) that runs code in response to events or direct invocations. The following figure shows the high-level OpenWhisk architecture.\n\n![OpenWhisk architecture](images/OpenWhisk.png)\n\nExamples of events include changes to database records, IoT sensor readings that exceed a certain temperature, new code commits to a GitHub repository, or simple HTTP requests from web or mobile apps. Events from external and internal event sources are channeled through a trigger, and rules allow actions to react to these events.\n\nActions can be small snippets of code (JavaScript, Swift and many other languages are supported), or custom binary code embedded in a Docker container. Actions in OpenWhisk are instantly deployed and executed whenever a trigger fires. The more triggers fire, the more actions get invoked. If no trigger fires, no action code is running, so there is no cost.\n\nIn addition to associating actions with triggers, it is possible to directly invoke an action by using the OpenWhisk API, CLI, or iOS SDK. A set of actions can also be chained without having to write any code. Each action in the chain is invoked in sequence with the output of one action passed as input to the next in the sequence.\n\nWith traditional long-running virtual machines or containers, it is common practice to deploy multiple VMs or containers to be resilient against outages of a single instance. However, OpenWhisk offers an alternative model with no resiliency-related cost overhead. The on-demand execution of actions provides inherent scalability and optimal utilization as the number of running actions always matches the trigger rate. Additionally, the developer now only focuses on code and does not worry about monitoring, patching, and securing the underlying server, storage, network, and operating system infrastructure.\n\nIntegrations with additional services and event providers can be added with packages. A package is a bundle of feeds and actions. A feed is a piece of code that configures an external event source to fire trigger events. For example, a trigger that is created with a Cloudant change feed will configure a service to fire the trigger every time a document is modified or added to a Cloudant database. Actions in packages represent reusable logic that a service provider can make available so that developers not only can use the service as an event source, but also can invoke APIs of that service.\n\nAn existing catalog of packages offers a quick way to enhance applications with useful capabilities, and to access external services in the ecosystem. Examples of external services that are OpenWhisk-enabled include Cloudant, The Weather Company, Slack, and GitHub.\n\n# How OpenWhisk works\n\nBeing an open-source project, OpenWhisk stands on the shoulders of giants, including Nginx, Kafka, Docker, CouchDB. All of these components come together to form a “serverless event-based programming service”. To explain all the components in more detail, lets trace an invocation of an action through the system as it happens. An invocation in OpenWhisk is the core thing a serverless-engine does: Execute the code the user has fed into the system and return the results of that execution.\n\n## Creating the action\n\nTo give the explanation a little bit of context, let’s create an action in the system first. We will use that action to explain the concepts later on while tracing through the system. The following commands assume that the [OpenWhisk CLI is setup properly](https://github.com/apache/openwhisk/blob/master/docs/cli.md).\n\nFirst, we’ll create a file *action.js* containing the following code which will print “Hello World” to stdout and return a JSON object containing “world” under the key “hello”.\n```\nfunction main() {\n    console.log('Hello World');\n    return { hello: 'world' };\n}\n```\nWe create that action using.\n```\nwsk action create myAction action.js\n```\nDone. Now we actually want to invoke that action:\n```\nwsk action invoke myAction --result\n```\n\n## The internal flow of processing\nWhat actually happens behind the scenes in OpenWhisk?\n\n![OpenWhisk flow of processing](images/OpenWhisk_flow_of_processing.png)\n\n### Entering the system: nginx\n\nFirst: OpenWhisk’s user-facing API is completely HTTP based and follows a RESTful design. As a consequence, the command sent via the `wsk` CLI is essentially an HTTP request against the OpenWhisk system. The specific command above translates roughly to:\n```\nPOST /api/v1/namespaces/$userNamespace/actions/myAction\nHost: $openwhiskEndpoint\n```\n\nNote the *$userNamespace* variable here. A user has access to at least one namespace. For simplicity, let’s assume that the user owns the namespace where *myAction* is put into.\n\nThe first entry point into the system is through **nginx**, “an HTTP and reverse proxy server”. It is mainly used for SSL termination and forwarding appropriate HTTP calls to the next component.\n\n### Entering the system: Controller\n\nNot having done much to our HTTP request, nginx forwards it to the **Controller**, the next component on our trip through OpenWhisk. It is a Scala-based implementation of the actual REST API (based on **Pekko** and **Spray**) and thus serves as the interface for everything a user can do, including [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) requests for your entities in OpenWhisk and invocation of actions (which is what we’re doing right now).\n\nThe Controller first disambiguates what the user is trying to do. It does so based on the HTTP method you use in your HTTP request. As per translation above, the user is issuing a POST request to an existing action, which the Controller translates to an **invocation of an action**.\n\nGiven the central role of the Controller (hence the name), the following steps will all involve it to a certain extent.\n\n### Authentication and Authorization: CouchDB\n\nNow the Controller verifies who you are (*Authentication*) and if you have the privilege to do what you want to do with that entity (*Authorization*). The credentials included in the request are verified against the so-called **subjects** database in a **CouchDB** instance.\n\nIn this case, it is checked that the user exists in OpenWhisk’s database and that it has the privilege to invoke the action myAction, which we assumed is an action in a namespace the user owns. The latter effectively gives the user the privilege to invoke the action, which is what he wishes to do.\n\nAs everything is sound, the gate opens for the next stage of processing.\n\n### Getting the action: CouchDB… again\n\nAs the Controller is now sure the user is allowed in and has the privileges to invoke his action, it actually loads this action (in this case *myAction*) from the **whisks** database in CouchDB.\n\nThe record of the action contains mainly the code to execute (shown above) and default parameters that you want to pass to your action, merged with the parameters you included in the actual invoke request. It also contains the resource restrictions imposed on it in execution, such as the memory it is allowed to consume.\n\nIn this particular case, our action doesn’t take any parameters (the function’s parameter definition is an empty list), thus we assume we haven’t set any default parameters and haven’t sent any specific parameters to the action, making for the most trivial case from this point-of-view.\n\n### Who’s there to invoke the action: Load Balancer\n\nThe Load Balancer, which is part of the Controller, has a global view of the executors available in the system by checking their health status continuously. Those executors are called **Invokers**. The Load Balancer, knowing which Invokers are available, chooses one of them to invoke the action requested.\n\n### Please form a line: Kafka\n\nFrom now on, mainly two bad things can happen to the invocation request you sent in:\n\n1. The system can crash, losing your invocation.\n2. The system can be under such a heavy load, that the invocation needs to wait for other invocations to finish first.\n\nThe answer to both is **Kafka**, “a high-throughput, distributed, publish-subscribe messaging system”. Controller and Invoker solely communicate through messages buffered and persisted by Kafka. That lifts the burden of buffering in memory, risking an *OutOfMemoryException*, off of both the Controller and the Invoker while also making sure that messages are not lost in case the system crashes.\n\nTo get the action invoked then, the Controller publishes a message to Kafka, which contains the action to invoke and the parameters to pass to that action (in this case none). This message is addressed to the Invoker which the Controller chose above from the list of available invokers.\n\nOnce Kafka has confirmed that it got the message, the HTTP request to the user is responded to with an **ActivationId**. The user will use that later on, to get access to the results of this specific invocation. Note that this is an asynchronous invocation model, where the HTTP request terminates once the system has accepted the request to invoke an action. A synchronous model (called blocking invocation) is available, but not covered by this article.\n\n### Actually invoking the code already: Invoker\n\nThe **Invoker** is the heart of OpenWhisk. The Invoker’s duty is to invoke an action. It is also implemented in Scala. But there’s much more to it. To execute actions in an isolated and safe way it uses **Docker**.\n\nDocker is used to setup a new self-encapsulated environment (called *container*) for each action that we invoke in a fast, isolated and controlled way. In a nutshell, for each action invocation a Docker container is spawned, the action code gets injected, it gets executed using the parameters passed to it, the result is obtained, the container gets destroyed. This is also the place where a lot of performance optimization is done to reduce overhead and make low response times possible.\n\nIn our specific case, as we’re having a *Node.js* based action at hand, the Invoker will start a Node.js container, inject the code from *myAction*, run it with no parameters, extract the result, save the logs and destroy the Node.js container again.\n\n### Storing the results: CouchDB again\n\nAs the result is obtained by the Invoker, it is stored into the **activations** database as an activation under the ActivationId mentioned further above. The **activations** database lives in **CouchDB**.\n\nIn our specific case, the Invoker gets the resulting JSON object back from the action, grabs the log written by Docker, puts them all into the activation record and stores it into the database. It will look roughly like this:\n\n```\n{\n   \"activationId\": \"31809ddca6f64cfc9de2937ebd44fbb9\",\n   \"response\": {\n       \"statusCode\": 0,\n       \"result\": {\n           \"hello\": \"world\"\n       }\n   },\n   \"end\": 1474459415621,\n   \"logs\": [\n       \"2016-09-21T12:03:35.619234386Z stdout: Hello World\"\n   ],\n   \"start\": 1474459415595,\n}\n```\n\nNote how the record contains both the returned result and the logs written. It also contains the start and end time of the invocation of the action. There are more fields in an activation record, this is a stripped down version for simplicity.\n\nNow you can use the REST API again (start from step 1 again) to obtain your activation and thus the result of your action. To do so you’d use:\n\n```\nwsk activation get 31809ddca6f64cfc9de2937ebd44fbb9\n```\n\n## Summary\n\nWe’ve seen how a simple **wsk action invoke myAction** passes through different stages of the OpenWhisk system. The system itself mainly consists of only two custom components, the **Controller** and the **Invoker**. Everything else is already there, developed by so many people out there in the open-source community.\n\nYou can find additional information about OpenWhisk in the following topics:\n\n* [Entity names](./reference.md#openwhisk-entities)\n* [Action semantics](./reference.md#action-semantics)\n* [Limits](./reference.md#system-limits)\n* [REST API](./reference.md#rest-api)\n"
  },
  {
    "path": "docs/actions-actionloop.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Developing a new Runtime with the ActionLoop proxy\n\nThe [OpenWhisk runtime specification](actions-new.md) defines the expected behavior of an OpenWhisk runtime; you can choose to implement a new runtime from scratch by just following this specification. However, the fastest way to develop a new, compliant runtime is by reusing the *[ActionLoop proxy](https://github.com/apache/openwhisk-runtime-go#actionloop-runtime)* which already implements most of the specification and requires you to write code for just a few hooks to get a fully functional (and *fast*) runtime in a few hours or less.\n\n## What is the ActionLoop proxy\n\nThe `ActionLoop proxy` is a runtime \"engine\", written in the [Go programming language](https://golang.org/), originally developed specifically to support the [OpenWhisk Go language runtime](https://github.com/apache/openwhisk-runtime-go). However, it was written in a  generic way such that it has since been adopted to implement OpenWhisk runtimes for Swift, PHP, Python, Rust, Java, Ruby and Crystal. Even though it was developed with compiled languages in mind it works equally well with scripting languages.\n\nUsing it, you can develop a new runtime in a fraction of the time needed for authoring a full-fledged runtime from scratch. This is due to the fact that you have only to write a command line protocol and not a fully-featured web server (with a small amount of corner cases to consider). The results should also produce a runtime that is fairly fast and responsive.  In fact, the ActionLoop proxy has also been adopted to improve the performance of existing runtimes like Python, Ruby, PHP, and Java where performance has improved by a factor between 2x to 20x.\n\n### Precompilation of OpenWhisk Actions\n\nIn addition to being the basis for new runtime development,  ActionLoop runtimes can also support offline \"precompilation\" of OpenWhisk Action source files into a ZIP file that contains only the compiled binaries which are very fast to start once deployed. More information on this approach can be found here: [Precompiling Go Sources Offline](https://github.com/apache/openwhisk-runtime-go/blob/master/docs/DEPLOY.md#precompile) which describes how to do this for the Go language, but the approach applies to any language supported by ActionLoop.\n\n# Tutorial - How to write a new runtime with the ActionLoop Proxy\n\nThis section contains a stepwise tutorial which will take you through the process of developing a new ActionLoop runtime using the Ruby language as the example.\n\n## General development process\n\nThe general procedure for authoring a runtime with the `ActionLoop proxy` requires the following steps:\n\n* building a docker image containing your target language compiler and the ActionLoop runtime.\n* writing a simple line-oriented protocol in your target language.\n* writing a compilation script for your target language.\n* writing some mandatory tests for your language.\n\n## ActionLoop Starter Kit\n\nTo facilitate the process, there is an `actionloop-starter-kit` in the [openwhisk-devtools](https://github.com/apache/openwhisk-devtools/tree/master/actionloop-starter-kit) GitHub repository, that implements a fully working runtime for Python.  It contains a stripped-down version of the real Python runtime (with some advanced features removed) along with guided, step-by-step instructions on how to translate it to a different target runtime language using Ruby as an example.\n\nIn short, the starter kit provides templates you can adapt in creating an ActionLoop runtime for each of the steps listed above, these include :\n\n-checking out  the `actionloop-starter-kit` from the `openwhisk-devtools` repository\n-editing the `Dockerfile` to create the target environment for your target language.\n-converting (rewrite) the `launcher.py` script to an equivalent for script for your target language.\n-editing the `compile` script to compile your action in your target language.\n-writing the mandatory tests for your target language, by adapting the `ActionLoopPythonBasicTests.scala` file.\n\nAs a starting language, we chose Python since it is one of the more human-readable languages (can be  treated as `pseudo-code`). Do not worry, you should only need just enough Python knowledge to be able to rewrite `launcher.py` and edit the `compile` script for your target language.\n\nFinally, you will need to update the `ActionLoopPythonBasicTests.scala` test file which, although written in the Scala language, only serves as a wrapper that you will use to embed your target language tests into.\n\n## Notation\n\nIn each step of this tutorial, we typically show snippets of either terminal transcripts (i.e., commands and results) or \"diffs\" of changes to existing code files.\n\nWithin terminal transcript snippets, comments are prefixed with `#` character and commands are prefixed by the `$` character. Lines that follow commands may include sample output (from their execution) which can be used to verify against results in your local environment.\n\nWhen snippets show changes to existing source files, lines without a prefix should be left \"as is\", lines with `-` should be removed and lines with  `+` should be added.\n\n## Prerequisites\n\n* Docker engine - please have a valid [docker engine installed](https://docs.docker.com/install/) that supports [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/) (i.e., Docker 17.05 or higher) and assure the Docker daemon is running.\n\n```bash\n# Verify docker version\n$ docker --version\nDocker version 18.09.3\n\n# Verify docker is running\n$ docker ps\n\n# The result should be a valid response listing running processes\n```\n\n## Setup the development directory\n\nSo let's start to create our own `actionloop-demo-ruby-2.6` runtime. First, check out the `devtools` repository to access the starter kit, then move it in your home directory to work on it.\n\n```bash\n$ git clone https://github.com/apache/openwhisk-devtools\n$ mv openwhisk-devtools/actionloop-starter-kit ~/actionloop-demo-ruby-v2.6\n```\n\nNow, take the directory `python3.7` and rename it to `ruby2.6` and use `sed` to fix the directory name references in the Gradle build files.\n\n```bash\n$ cd ~/actionloop-demo-ruby-v2.6\n$ mv python3.7 ruby2.6\n$ sed -i.bak -e 's/python3.7/ruby2.6/' settings.gradle\n$ sed -i.bak -e 's/actionloop-demo-python-v3.7/actionloop-demo-ruby-v2.6/' ruby2.6/build.gradle\n```\n\nLet's check everything is fine building the image.\n\n```bash\n# building the image\n$ ./gradlew distDocker\n# ... intermediate output omitted ...\nBUILD SUCCESSFUL in 1s\n2 actionable tasks: 2 executed\n# checking the image is available\n$ docker images actionloop-demo-ruby-v2.6\nREPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE\nactionloop-demo-ruby-v2.6   latest              df3e77c9cd8f        2 minutes ago          94.3MB\n```\n\nAt this point, we have built a new image named `actionloop-demo-ruby-v2.6`. However, despite having `Ruby` in the name, internally it still is a `Python` language runtime which we will need to change to one supporting `Ruby` as we continue in this tutorial.\n\n## Preparing the Docker environment\n\nOur language runtime's `Dockerfile` has the task of preparing an environment for executing OpenWhisk Actions.\nUsing the ActionLoop approach, we use a multistage Docker build to\n\n1. derive our OpenWhisk language runtime from an existing Docker image that has all the target language's tools and libraries for running functions authored in that language.\n    * In our case, we will reference the `ruby:2.6.2-alpine3.9` image from the [Official Docker Images for Ruby](https://hub.docker.com/_/ruby) on Docker Hub.\n1. leverage the existing `openwhisk/actionlooop-v2` image on Docker Hub from which we will \"extract\" the *ActionLoop* proxy (i.e. copy `/bin/proxy` binary) our runtime will use to process Activation requests from the OpenWhisk platform and execute Actions by using the language's tools and libraries from step #1.\n\n## Repurpose the renamed Python Dockerfile for Ruby builds\n\nLet's edit the `ruby2.6/Dockerfile` to use the official Ruby image on Docker Hub as our base image, instead of a Python image, and add our our Ruby launcher script:\n\n```dockerfile\n FROM openwhisk/actionloop-v2:latest as builder\n-FROM python:3.7-alpine\n+FROM ruby:2.6.2-alpine3.9\n RUN mkdir -p /proxy/bin /proxy/lib /proxy/action\n WORKDIR /proxy\n COPY --from=builder /bin/proxy /bin/proxy\n-ADD lib/launcher.py /proxy/lib/launcher.py\n+ADD lib/launcher.rb /proxy/lib/launcher.rb\n ADD bin/compile /proxy/bin/compile\n+RUN apk update && apk add python3\n ENV OW_COMPILER=/proxy/bin/compile\n ENTRYPOINT [\"/bin/proxy\"]\n```\n\nNext, let's rename the `launcher.py` (a Python script) to one that indicates it is a Ruby script named `launcher.rb`.\n\n```bash\n$ mv ruby2.6/lib/launcher.py ruby2.6/lib/launcher.rb\n```\n\nNote that:\n\n1. You changed the base Docker image to use a `Ruby` language image.\n1. You changed the launcher script from `Python` to `Ruby`.\n1. We had to add a `python3` package to our Ruby image since our `compile` script will be written in Python for this tutorial. Of course, you may choose to rewrite the `compile` script in `Ruby` if you wish to as your own exercise.\n\n## Implementing the ActionLoop protocol\n\nThis section will take you through how to convert the contents of `launcher.rb` (formerly `launcher.py`) to the target Ruby programming language and implement the `ActionLoop protocol`.\n\n### What the launcher needs to do\n\nLet's recap the steps the launcher must accomplish to implement the `ActionLoop protocol` :\n\n1. import the Action function's `main` method for execution.\n    * Note: the `compile` script will make the function available to the launcher.\n1. open the system's `file descriptor 3` which will be used to output the functions response.\n1. read the system's standard input, `stdin`, line-by-line. Each line is parsed as a JSON string and produces a JSON object (not an array nor a scalar) to be passed as the input `arg` to the function.\n    * Note: within the JSON object, the `value` key contains the user parameter data to be passed to your functions. All the other keys are made available as process environment variables to the function; these need to be uppercased and prefixed with `\"__OW_\"`.\n1. invoke the `main` function with the JSON object payload.\n1. encode the result of the function in JSON (ensuring it is only one line and it is terminated with one newline) and write it to `file descriptor 3`.\n1. Once the function returns the result, flush the contents of `stdout`, `stderr` and `file descriptor 3` (FD 3).\n1. Finally, include the above steps in a loop so that it continually looks for Activations. That's it.\n\n### Converting launcher script to Ruby\n\nNow, let's look at the protocol described above, codified within the launcher script `launcher.rb`, and work to convert its contents from Python to Ruby.\n\n#### Import the function code\n\nSkipping the first few library import statements within  `launcer.rb`, which we will have to resolve later after we determine which ones Ruby may need, we see the first significant line of code importing the actual Action function.\n\n```python\n# now import the action as process input/output\nfrom main__ import main as main\n```\n\nIn Ruby, this can be rewritten as:\n\n```ruby\n# requiring user's action code\nrequire \"./main__\"\n```\n\n*Note that you are free to decide the path and filename for the function's source code. In our examples, we chose a base filename that includes the word `\"main\"` (since it is OpenWhisk's default function name) and append two underscores to better assure uniqueness.*\n\n#### Open File Descriptor (FD) 3 for function results output\n\nThe `ActionLoop` proxy expects to read the results of invoking the Action function from File Descriptor (FD) 3.\n\nThe existing Python:\n\n```python\nout = fdopen(3, \"wb\")\n```\n\nwould be rewritten in Ruby as:\n\n```ruby\nout = IO.new(3)\n```\n\n#### Process Action's arguments from STDIN\n\nEach time the function is invoked via an HTTP request, the `ActionLoop` proxy passes the message contents to the launcher via STDIN. The launcher must read STDIN line-by-line and parse it as JSON.\n\nThe `launcher`'s existing Python code reads STDIN line-by-line as follows:\n\n```python\nwhile True:\n  line = stdin.readline()\n  if not line: break\n  # ...continue...\n```\n\nwould be translated to Ruby as follows:\n\n```ruby\nwhile true\n  # JSON arguments get passed via STDIN\n  line = STDIN.gets()\n  break unless line\n  # ...continue...\nend\n```\n\nEach line is parsed in JSON, where the `payload` is extracted from contents of the `\"value\"` key. Other keys and their values are as uppercased, `\"__OW_\"` prefixed environment variables:\n\nThe existing Python code for this is:\n\n```python\n  # ... continuing ...\n  args = json.loads(line)\n  payload = {}\n  for key in args:\n    if key == \"value\":\n      payload = args[\"value\"]\n    else:\n      os.environ[\"__OW_%s\" % key.upper()]= args[key]\n  # ... continue ...\n```\n\nwould be translated to Ruby:\n\n```ruby\n  # ... continuing ...\n  args = JSON.parse(line)\n  payload = {}\n  args.each do |key, value|\n    if key == \"value\"\n      payload = value\n    else\n      # set environment variables for other keys\n      ENV[\"__OW_#{key.upcase}\"] = value\n    end\n  end\n  # ... continue ...\n```\n\n#### Invoking the Action function\n\nWe are now at the point of invoking the Action function and producing its result. *Note we **must** also capture exceptions and produce an `{\"error\": <result> }` if anything goes wrong during execution.*\n\nThe existing Python code for this is:\n\n```python\n  # ... continuing ...\n  res = {}\n  try:\n    res = main(payload)\n  except Exception as ex:\n    print(traceback.format_exc(), file=stderr)\n    res = {\"error\": str(ex)}\n  # ... continue ...\n```\n\nwould be translated to Ruby:\n\n```ruby\n  # ... continuing ...\n  res = {}\n  begin\n    res = main(payload)\n  rescue Exception => e\n    puts \"exception: #{e}\"\n    res [\"error\"] = \"#{e}\"\n  end\n  # ... continue ...\n```\n\n#### Finalize File Descriptor (FD) 3, STDOUT and STDERR\n\nFinally, we need to write the function's result to File Descriptor (FD) 3 and \"flush\" standard out (stdout), standard error (stderr) and FD 3.\n\nThe existing Python code for this is:\n\n```python\n  out.write(json.dumps(res, ensure_ascii=False).encode('utf-8'))\n  out.write(b'\\n')\n  stdout.flush()\n  stderr.flush()\n  out.flush()\n```\n\nwould be translated to Ruby:\n\n```ruby\n  STDOUT.flush()\n  STDERR.flush()\n  out.puts(res.to_json)\n  out.flush()\n```\n\nCongratulations! You just completed your `ActionLoop` request handler.\n\n## Writing the compilation script\n\nNow, we need to write the `compilation script`. It is basically a script that will prepare the uploaded sources for execution, adding the `launcher` code and generate the final executable.\n\nFor interpreted languages, the compilation script will only \"prepare\" the sources for execution. The executable is simply a shell script to invoke the interpreter.\n\nFor compiled languages, like Go it will actually invoke a compiler in order to produce the final executable. There are also cases like Java where we still need to execute the compilation step that produces intermediate code, but the executable is just a shell script that will launch the Java runtime.\n\n### How the ActionLoop proxy handles action uploads\n\nThe OpenWhisk user can upload actions with the `wsk` Command Line Interface (CLI) tool as a single file.\n\nThis single file can be:\n\n- a source file\n- an executable file\n- a ZIP file containing sources\n- a ZIP file containing an executable and other support files\n\n*Important*:  an executable for ActionLoop is either a Linux binary (an ELF executable) or a script. A script is, using Linux conventions, is anything starting with `#!`. The first line is interpreted as the command to use to launch the script: `#!/bin/bash`, `#!/usr/bin/python` etc.\n\nThe ActionLoop proxy accepts any file, prepares a work folder, with two folders in it named `\"src\"` and `\"bin\"`. Then it detects the format of the uploaded file.  For each case, the behavior is different.\n\n- If the uploaded file is an executable, it is stored as `bin/exec` and executed.\n- If the uploaded file is not an executable and not a zip file, it is stored as `src/exec` then the compilation script is invoked.\n- If the uploaded file is a zip file, it is unzipped in the `src` folder, then the `src/exec` file is checked.\n- If it exists and it is an executable, the folder `src` is renamed to `bin` and then again the `bin/exec` is executed.\n- If the `src/exec` is missing or is not an executable, then the compiler script is invoked.\n\n### Compiling an action in source format\n\nThe compilation script is invoked only when the upload contains sources. According to the description in the past paragraph, if the upload is a single file, we can expect the file is in `src/exec`, without any prefix. Otherwise, sources are spread the `src` folder and it is the task of the compiler script to find the sources. A runtime may impose that when a zip file is uploaded, then there should be a fixed file with the main function. For example, the Python runtime expects the file `__main__.py`. However, it is not a rule: the Go runtime does not require any specific file as it compiles everything. It only requires a function with the name specified.\n\nThe compiler script goal is ultimately to leave in `bin/exec` an executable (implementing the ActionLoop protocol) that the proxy can launch. Also, if the executable is not standalone, other files must be stored in this folder, since the proxy can also zip all of them and send to the user when using the pre-compilation feature.\n\nThe compilation script is a script pointed by the `OW_COMPILER` environment variable (you may have noticed it in the Dockerfile) that will be invoked with 3 parameters:\n\n1. `<main>` is the name of the main function specified by the user on the `wsk` command line\n1. `<src>` is the absolute directory with the sources already unzipped\n1.  an empty `<bin>` directory where we are expected to place our final executables\n\nNote that both the `<src>` and `<bin>` are disposable, so we can do things like removing the `<bin>` folder and rename the `<src>`.\n\nSince the user generally only sends a function specified by the `<main>` parameter, we have to add the launcher we wrote and adapt it to execute the function.\n\n### Implementing the `compile` for Ruby\n\nThis is the algorithm that the `compile` script in the kit follows for Python:\n\n1. if there is a `<src>/exec` it must rename to the main file; I use the name `main__.py`\n1. if there is a `<src>/__main__.py` it will rename to the main file `main__.py`\n1. copy the `launcher.py` to `exec__.py`, replacing the `main(arg)` with `<main>(arg)`;  this file imports the `main__.py` and invokes the function `<main>`\n1. add a launcher script `<src>/exec`\n1. finally it removes the `<bin>` folder and rename `<src>` to `<bin>`\n\nWe can adapt this algorithm easily to Ruby with just a few changes.\n\nThe script defines the functions `sources` and `build` then starts the execution, at the end of the script.\n\nStart from the end of the script, where the script collect parameters from the command line. Instead of `launcher.py`, use `launcher.rb`:\n\n```\n- launcher = \"%s/lib/launcher.py\" % dirname(dirname(sys.argv[0]))\n+ launcher = \"%s/lib/launcher.rb\" % dirname(dirname(sys.argv[0]))\n```\n\nThen the script invokes the `source` function. This function renames the `exec` file to `main__.py`, you will rename it instead to `main__.rb`:\n\n```ruby\n- copy_replace(src_file, \"%s/main__.py\" % src_dir)\n+ copy_replace(src_file, \"%s/main__.rb\" % src_dir)\n```\n\nIf instead there is a `__main__.py` the function will rename to `main__.py` (the launcher invokes this file always). The Ruby runtime will use a `main.rb` as starting point. So the next change is:\n\n```ruby\n- # move __main__ in the right place if it exists\n- src_file = \"%s/__main__.py\" % src_dir\n+ # move main.rb in the right place if it exists\n+ src_file = \"%s/main.rb\" % src_dir\n```\n\nNow, the `source` function  copies the launcher as `exec__.py`, replacing the line `from main__ import main as main` (invoking the main function) with `from main__ import <main> as main`. In Ruby you may want to replace the line `res = main(payload)` with `res = <main>(payload)`. In code it is:\n\n```ruby\n- copy_replace(launcher, \"%s/exec__.py\" % src_dir,\n-   \"from main__ import main as main\",\n-    \"from main__ import %s as main\" % main )\n+ copy_replace(launcher, \"%s/exec__.rb\" % src_dir,\n+    \"res = main(payload)\",\n+     \"res = %s(payload)\" % main )\n```\n\nWe are almost done. We just need the startup script that instead of invoking python will invoke Ruby. So in the `build` function do this change:\n\n```ruby\n write_file(\"%s/exec\" % tgt_dir, \"\"\"#!/bin/sh\n cd \"$(dirname $0)\"\n-exec /usr/local/bin/python exec__.py\n+exec ruby exec__.rb\n \"\"\")\n```\n\nFor an interpreted language that is all. We move the `src` folder in the `bin`. For a compiled language instead, we may want to actually invoke the compiler to produce the executable.\n\n## Debugging\n\nNow that we have completed both the `launcher` and `compile` scripts, it is time to test them.\n\nHere we will learn how to:\n\n1. enter in a test environment\n1. simple smoke tests to check things work\n1. writing the validation tests\n1. testing the image in an actual OpenWhisk environment\n\n\n### Entering in the test environment\n\nIn the starter kit, there is a `Makefile` that can help with our development efforts.\n\nWe can build the Dockerfile using the provided Makefile. Since it has a reference to the image we are building, let's change it:\n\n```bash\nsed -i.bak -e 's/actionloop-demo-python-v3.7/actionloop-demo-ruby-v2.6/' ruby2.6/Makefile\n```\n\nWe should be now able to build the image and enter in it with `make debug`. It will rebuild the image for us and put us into a shell so we can enter access the image environment for testing and debugging:\n\n```bash\n$ cd ruby2.6\n$ make debug\n# results omitted for brevity ...\n```\n\nLet's start with a couple of notes about this test environment.\n\nFirst, use `--entrypoint=/bin/sh` when starting the image to have a shell available at our image entrypoint. Generally, this is true by default; however, in some stripped down base images a shell may not be available.\n\nSecond, the `/proxy` folder is mounted in our local directory, so that we can edit the `bin/compile` and the `lib/launcher.rb` using our editor outside the Docker image\n\n*NOTE* It is not necessary to rebuild the Docker image with every change when using `make debug` since directories and environment variables used by the proxy indicate where the code outside the Docker container is located.\n\nOnce at the shell prompt that we will use for development,  we will have to start and stop the proxy. The shell will help us to inspect what happened inside the container.\n\n### A simple smoke test\n\nIt is time to test. Let's write a very simple test first, converting the `example\\hello.py` in `example\\hello.rb` to appear as follows:\n\n```ruby\ndef hello(args)\n  name = args[\"name\"] || \"stranger\"\n  greeting = \"Hello #{name}!\"\n  puts greeting\n  { \"greeting\" => greeting }\nend\n```\n\nNow change into the `ruby2.6` subdirectory of our runtime project and in one terminal type:\n\n```bash\n$ cd <projectdir>/ruby2.6\n$ make debug\n# results omitted for brevity ...\n# (you should see a shell prompt of your image)\n$ /bin/proxy -debug\n2019/04/08 07:47:36 OpenWhisk ActionLoop Proxy 2: starting\n```\n\nNow the runtime is started in debug mode, listening on port 8080, and ready to accept Action deployments.\n\nOpen another terminal (while leaving the first one running the proxy) and go *into the top-level directory of our project* to test the Action by executing an `init` and then a couple of `run` requests using the `tools/invoke.py` test script.\n\nThese steps should look something like this in the second terminal:\n\n```bash\n$ cd <projectdir>\n$ python tools/invoke.py init hello example/hello.rb\n{\"ok\":true}\n$ python tools/invoke.py run '{}'\n{\"greeting\":\"Hello stranger!\"}\n$ python tools/invoke.py run  '{\"name\":\"Mike\"}'\n{\"greeting\":\"Hello Mike!\"}\n```\n\nWe should also see debug output from the first terminal running the proxy (with the `debug` flag) which should have successfully processed the `init` and `run` requests above.\n\nThe proxy's debug output should appear something like:\n\n```bin\n/proxy # /bin/proxy -debug\n2019/04/08 07:54:57 OpenWhisk ActionLoop Proxy 2: starting\n2019/04/08 07:58:00 compiler: /proxy/bin/compile\n2019/04/08 07:58:00 it is source code\n2019/04/08 07:58:00 compiling: ./action/16/src/exec main: hello\n2019/04/08 07:58:00 compiling: /proxy/bin/compile hello action/16/src action/16/bin\n2019/04/08 07:58:00 compiler out: , <nil>\n2019/04/08 07:58:00 env: [__OW_API_HOST=]\n2019/04/08 07:58:00 starting ./action/16/bin/exec\n2019/04/08 07:58:00 Start:\n2019/04/08 07:58:00 pid: 13\n2019/04/08 07:58:24 done reading 13 bytes\nHello stranger!\nXXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\nXXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n2019/04/08 07:58:24 received::{\"greeting\":\"Hello stranger!\"}\n2019/04/08 07:58:54 done reading 27 bytes\nHello Mike!\nXXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\nXXX_THE_END_OF_A_WHISK_ACTIVATION_XXX\n2019/04/08 07:58:54 received::{\"greeting\":\"Hello Mike!\"}\n```\n\n### Hints and tips for debugging\n\nOf course, it is very possible something went wrong. Here a few debugging suggestions:\n\nThe ActionLoop runtime (proxy) can only be initialized once using the `init` command from the `invoke.py` script. If we need to re-initialize the runtime, we need to stop the runtime (i.e., with Control-C)  and restart it.\n\nWe can also check what is in the action folder. The proxy creates a numbered folder under `action` and then a `src` and `bin` folder.\n\nFor example, using a terminal window, we would would see a directory and file structure created by a single action:\n\n```bash\n$ find\naction/\naction/1\naction/1/bin\naction/1/bin/exec__.rb\naction/1/bin/exec\naction/1/bin/main__.rb\n```\n\nNote that the `exec` starter, `exec__.rb` launcher and `main__.rb` action code are have all been copied under a directory numbered`1`.\n\nIn addition, we can try to run the action directly and see if it behaves properly:\n\n```bash\n$ cd action/1/bin\n$ ./exec 3>&1\n$ {\"value\":{\"name\":\"Mike\"}}\nHello Mike!\n{\"greeting\":\"Hello Mike!\"}\n```\n\nNote we redirected the file descriptor 3 in stdout to check what is happening, and note that logs appear in stdout too.\n\nAlso, we can test the compiler invoking it directly.\n\nFirst let's prepare the environment as it appears when we just uploaded the action:\n\n```bash\n$ cd /proxy\n$ mkdir -p action/2/src action/2/bin\n$ cp action/1/bin/main__.rb action/2/src/exec\n$ find action/2\naction/2\naction/2/bin\naction/2/src\naction/2/src/exec\n```\n\nNow compile and examine the results again:\n\n```bash\n$ /proxy/bin/compile main action/2/src action/2/bin\n$ find action/2\naction/2/\naction/2/bin\naction/2/bin/exec__.rb\naction/2/bin/exec\naction/2/bin/main__.rb\n```\n\n## Testing\n\nIf we have reached this point in the tutorial, the runtime is able to run and execute a simple test action. Now we need to validate the runtime against a set of mandatory tests both locally and within an OpenWhisk staging environment. Additionally, we should author and automate additional tests for language specific features and styles.\n\nThe `starter kit` includes two handy `makefiles` that we can leverage for some additional tests. In the next sections, we will show how to update them for testing our Ruby runtime.\n\n### Testing multi-file Actions\n\nSo far we tested a only an Action comprised of a single file. We should also test multi-file Actions (i.e., those with relative imports) sent to the runtime in both source and binary formats.\n\nFirst, let's try a multi-file Action by creating a Ruby Action script named `example/main.rb` that invokes our `hello.rb` as follows:\n\n```ruby\nrequire \"./hello\"\ndef main(args)\n    hello(args)\nend\n```\n\nWithin the `example/Makefile` makefile:\n\n- update the name of the image to `ruby-v2.6\"` as well as the name of the `main` action.\n- update the PREFIX with your DockerHub username.\n\n```makefile\n-IMG=actionloop-demo-python-v3.7:latest\n-ACT=hello-demo-python\n-PREFIX=docker.io/openwhisk\n+IMG=actionloop-demo-ruby-v2.6:latest\n+ACT=hello-demo-ruby\n+PREFIX=docker.io/<docker username>\n```\n\nNow, we are ready to test the various cases. Again, start the runtime proxy in debug mode:\n\n```bash\n$ cd ruby2.6\n$ make debug\n$ /bin/proxy -debug\n```\n\nOn another terminal, try to deploy a single file:\n\n```bash\n$ make test-single\npython ../tools/invoke.py init hello ../example/hello.rb\n{\"ok\":true}\npython ../tools/invoke.py run '{}'\n{\"greeting\":\"Hello stranger!\"}\npython ../tools/invoke.py run '{\"name\":\"Mike\"}'\n{\"greeting\":\"Hello Mike!\"}\n```\n\nNow, *stop and restart the proxy* and try to send a ZIP file with the sources:\n\n```\n$ make test-src-zip\nzip src.zip main.rb hello.rb\n  adding: main.rb (deflated 42%)\n  adding: hello.rb (deflated 42%)\npython ../tools/invoke.py init ../example/src.zip\n{\"ok\":true}\npython ../tools/invoke.py run '{}'\n{\"greeting\":\"Hello stranger!\"}\npython ../tools/invoke.py run '{\"name\":\"Mike\"}'\n{\"greeting\":\"Hello Mike!\"}\n```\n\nFinally, test the pre-compilation: the runtime builds a zip file with the sources ready to be deployed. Again, *stop and restart the proxy* then:\n\n```\n$ make test-bin-zip\ndocker run -i actionloop-demo-ruby-v2.6:latest -compile main <src.zip >bin.zip\npython ../tools/invoke.py init ../example/bin.zip\n{\"ok\":true}\n\npython ../tools/invoke.py run '{}'\n{\"greeting\":\"Hello stranger!\"}\n\npython ../tools/invoke.py run '{\"name\":\"Mike\"}'\n{\"greeting\":\"Hello Mike!\"}\n```\n\nCongratulations! The runtime works locally! Time to test it on the public cloud. So as the last step before moving forward, let's push the image to Docker Hub with `make push`.\n\n### Testing on OpenWhisk\n\nTo run this test you need to configure access to OpenWhisk with `wsk`. A simple way is to get access is to register a free account in the IBM Cloud but this works also with our own deployment of OpenWhisk.\n\nEdit the Makefile as we did previously:\n\n```makefile\nIMG=actionloop-demo-ruby-v2.6:latest\nACT=hello-demo-ruby\nPREFIX=docker.io/<docker username>\n```\n\nAlso, change any reference to `hello.py` and `main.py` to `hello.rb` and `main.rb`.\n\nOnce this is done, we can re-run the tests we executed locally on \"the real thing\".\n\nTest single:\n\n```bash\n$ make test-single\nwsk action update hello-demo-ruby hello.rb --docker docker.io/linus/actionloop-demo-ruby-v2.6:latest --main hello\nok: updated action hello-demo-ruby\nwsk action invoke hello-demo-ruby -r\n{\n    \"greeting\": \"Hello stranger!\"\n}\nwsk action invoke hello-demo-ruby -p name Mike -r\n{\n    \"greeting\": \"Hello Mike!\"\n}\n```\n\nTest source zip:\n\n```bash\n$ make test-src-zip\nzip src.zip main.rb hello.rb\n  adding: main.rb (deflated 42%)\n  adding: hello.rb (deflated 42%)\nwsk action update hello-demo-ruby src.zip --docker docker.io/linus/actionloop-demo-ruby-v2.6:latest\nok: updated action hello-demo-ruby\nwsk action invoke hello-demo-ruby -r\n{\n    \"greeting\": \"Hello stranger!\"\n}\nwsk action invoke hello-demo-ruby -p name Mike -r\n{\n    \"greeting\": \"Hello Mike!\"\n}\n```\n\nTest binary ZIP:\n\n```bash\n$ make test-bin-zip\ndocker run -i actionloop-demo-ruby-v2.6:latest -compile main <src.zip >bin.zip\nwsk action update hello-demo-ruby bin.zip --docker docker.io/actionloop/actionloop-demo-ruby-v2.6:latest\nok: updated action hello-demo-ruby\nwsk action invoke hello-demo-ruby -r\n{\n    \"greeting\": \"Hello stranger!\"\n}\nwsk action invoke hello-demo-ruby -p name Mike -r\n{\n    \"greeting\": \"Hello Mike!\"\n}\n```\n\nCongratulations! Your runtime works also in the real world.\n\n### Writing the validation tests\n\nBefore you can submit your runtime you should ensure your runtime pass the validation tests.\n\nUnder `tests/src/test/scala/runtime/actionContainers/ActionLoopPythonBasicTests.scala` there is the template for the test.\n\nRename to `tests/src/test/scala/runtime/actionContainers/ActionLoopRubyBasicTests.scala`, change internally the class name to `class ActionLoopRubyBasicTests` and implement the following test cases:\n\n- `testNotReturningJson`\n- `testUnicode`\n- `testEnv`\n- `testInitCannotBeCalledMoreThanOnce`\n- `testEntryPointOtherThanMain`\n- `testLargeInput`\n\nYou should convert Python code to Ruby code. We do not do go into the details of each test, as they are pretty simple and obvious. You can check the source code for the real test [here](https://github.com/apache/openwhisk-runtime-ruby/blob/master/tests/src/test/scala/actionContainers/Ruby26ActionLoopContainerTests.scala).\n\nYou can verify tests are running properly with:\n\n```bash\n$ ./gradlew test\n\nStarting a Gradle Daemon, 1 busy Daemon could not be reused, use --status for details\n\n> Task :tests:test\n\nruntime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should handle initialization with no code PASSED\n\nruntime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should handle initialization with no content PASSED\n\nruntime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should run and report an error for function not returning a json object PASSED\n\nruntime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should fail to initialize a second time PASSED\n\nruntime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should invoke non-standard entry point PASSED\n\nruntime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should echo arguments and print message to stdout/stderr PASSED\n\nruntime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should handle unicode in source, input params, logs, and result PASSED\n\nruntime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should confirm expected environment variables PASSED\n\nruntime.actionContainers.ActionLoopPythoRubyTests > runtime proxy should echo a large input PASSED\n\nBUILD SUCCESSFUL in 55s\n```\n\nBig congratulations are in order having reached this point successfully. At this point, our runtime should be ready to run on any OpenWhisk platform and also can be submitted for consideration to be included in the Apache OpenWhisk project.\n"
  },
  {
    "path": "docs/actions-docker.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Creating and invoking Docker actions\n\nApache OpenWhisk supports using custom Docker images as the action runtime. Custom runtimes images can either have the action source files built-in or injected dynamically by the platform during initialisation.\n\nBuilding custom runtime images is a common solution to the issue of having external application dependencies too large to deploy, due to the action size limit (48MB), e.g. machine learning libraries.\n\n### Usage\n\nThe [Apache OpenWhisk CLI](https://github.com/apache/openwhisk-cli) has a `--docker` configuration parameter to set a custom runtime for an action.\n\n```\nwsk action create <ACTION_NAME> --docker <IMAGE> source.js\n```\n\n*`<IMAGE>` must be an image name for a public Docker image on [Docker Hub](https://hub.docker.com/search?q=&type=image).*\n\nThe `--docker` flag can also be used without providing additional source or archive files for an action.\n\n```\nwsk action create <ACTION_NAME> --docker <IMAGE>\n```\n\nIn this scenario, action source code will not be injected into the runtime during cold-start initialisation. The runtime container must handle the platform invocation requests directly.\n\n### Restrictions\n\n- Custom runtime images must implement the [Action interface](https://github.com/apache/openwhisk/blob/master/docs/actions-new.md#runtime-general-requirements). This is the [protocol used by the platform](https://github.com/apache/openwhisk/blob/master/docs/actions-new.md#action-interface) to pass invocation requests to the runtime containers. Containers are expected to expose a HTTP server (running on port 8080) with `/init` and `/run` endpoints.\n- Custom runtime images must be available on [Docker Hub](https://hub.docker.com/search?q=&type=image). Docker Hub is the only container registry currently supported. This means all custom runtime images will need to be publicly available.\n- Custom runtime images will be pulled from Docker Hub into the local platform registry upon the first invocation. This can lead to longer cold-start times on the first invocation with a new or updated image. Once images have been pulled down, they are cached locally.\n\n### Image Refresh Behaviour\n\nCustom runtimes images should be versioned using explicit [image tags](https://docs.docker.com/engine/reference/commandline/tag/) where possible.\n\nWhen an image identifier has the `latest` tag (or has no explicit tag), creating new runtime containers from a custom image will always result in an image refresh check against the registry. Pulling an image may fail due to a network interruption or Docker Hub outage. Explicitly tagged images allow the system to gracefully recover by using locally cached images, making it much more resilient against external issues.\n\nThe \"latest\" tag should only be used for rapid prototyping, where guaranteeing the latest code state is more important than runtime stability at scale.\n\n### Existing Runtime Images\n\nApache OpenWhisk publishes all the [existing runtime images](https://hub.docker.com/u/openwhisk) on Docker Hub. This makes it simple to extend an existing runtimes with additional libraries or native dependencies. Public runtimes images can be used as base images in new runtime images.\n\nHere are some of the more common runtime images...\n\n- `openwhisk/action-nodejs-v10` - [Node.js 10](https://hub.docker.com/r/openwhisk/action-nodejs-v10) ([Source](https://github.com/apache/openwhisk-runtime-nodejs/blob/master/core/nodejs10Action/Dockerfile))\n- `openwhisk/python3action` - [Python 3](https://hub.docker.com/r/openwhisk/python3action) ([Source](https://github.com/apache/openwhisk-runtime-python/blob/master/core/pythonAction/Dockerfile))\n- `openwhisk/java8action` - [Java 8](https://hub.docker.com/r/openwhisk/java8action) ([Source](https://github.com/apache/openwhisk-runtime-java/blob/master/core/java8/Dockerfile))\n- `openwhisk/action-swift-v4.2` - [Swift 4.2](https://hub.docker.com/r/openwhisk/action-swift-v4.2) ([Source](https://github.com/apache/openwhisk-runtime-swift/blob/master/core/swift42Action/Dockerfile))\n- `openwhisk/action-php-v7.4` - [PHP 7.4](https://hub.docker.com/r/openwhisk/action-php-v7.4) ([Source](https://github.com/apache/openwhisk-runtime-php/blob/master/core/php7.4Action/Dockerfile))\n- `openwhisk/action-ruby-v2.5` - [Ruby 2.5](https://hub.docker.com/r/openwhisk/action-ruby-v2.5) ([Source](https://github.com/apache/openwhisk-runtime-ruby/blob/master/core/ruby2.5Action/Dockerfile))\n\n## Extending Existing Runtimes\n\nIf you want to use extra libraries in an action that can't be deployed (due to the action size limit), building a custom runtime with a project image runtime base image is an easy way to handle this.\n\nBy using an existing language runtime image as the base image, the container will already be set up to handle platform invocation requests for that language. This means the image build file only needs to contain commands to install those extra libraries or dependencies\n\nHere are examples for Node.js and Python using this approach to provide large libraries in the runtime.\n\n### Node.js\n\n[Tensorflow.js](https://www.tensorflow.org/js/) is a JavaScript implementation of [TensorFlow](https://www.tensorflow.org/), the open-source Machine Learning library from Google. This project also comes with a [Node.js backend driver](https://github.com/tensorflow/tfjs-node) to run the project on CPU or GPU devices in the Node.js runtime.\n\nBoth the core library and CPU backend driver for Node.js (`tfjs` and `tfjs-node`) can be installed as normal NPM packages. Unfortunately, it is not possible to deploy these libraries in a zip file to the Node.js runtime in Apache OpenWhisk due to the size of the library and its dependencies. The `tfjs-node` library has a native dependency which is over 170MB.\n\nInstead, we can build a custom runtime which extends the project's Node.js runtime image and runs `npm install` during the container build process. These libraries will then be pre-installed into the runtime and can be excluded from the deployment archive.\n\n- Create a `Dockerfile` with the following contents:\n\n```\nFROM openwhisk/action-nodejs-v10:latest\n\nRUN npm install @tensorflow/tfjs-node\n```\n\n- Build the Docker image.\n\n```\ndocker build -t action-nodejs-v10:tf-js .\n```\n\n- Tag the Docker image with your Docker Hub username.\n\n```\ndocker tag action-nodejs-v10:tf-js <USER_NAME>/action-nodejs-v10:tf-js\n```\n\n- Push the Docker image to Docker Hub.\n\n```\ndocker push <USER_NAME>/action-nodejs-v10:tf-js\n```\n\n- Create a new Apache OpenWhisk action with the following source code:\n\n```javascript\nconst tf = require('@tensorflow/tfjs-node')\n\nconst main = () => {\n  return { tf: tf.version }\n}\n```\n\n```\nwsk action create tfjs --docker <USER_NAME>/action-nodejs-v10:tf-js action.js\n```\n\n- Invoking the action should return the TensorFlow.js libraries versions available in the runtime.\n\n```\nwsk action invoke tfjs --result\n```\n\n```\n{\n    \"tf\": {\n        \"tfjs\": \"1.0.4\",\n        \"tfjs-converter\": \"1.0.4\",\n        \"tfjs-core\": \"1.0.4\",\n        \"tfjs-data\": \"1.0.4\",\n        \"tfjs-layers\": \"1.0.4\",\n        \"tfjs-node\": \"1.0.3\"\n    }\n}\n```\n\n### Python\n\nPython is a popular language for machine learning and data science due to availability of libraries like [numpy](http://www.numpy.org/).\n\nPython libraries can be [imported into the Python runtime](https://github.com/apache/openwhisk/blob/master/docs/actions-python.md#packaging-python-actions-with-a-virtual-environment-in-zip-files) in Apache OpenWhisk by including a `virtualenv` folder in the deployment archive. This approach does not work when the deployment archive would be larger than the action size limit (48MB).\n\nInstead, we can build a custom runtime which extends the project's Python runtime image and runs `pip install` during the container build process. These libraries will then be pre-installed into the runtime and can be excluded from the deployment archive.\n\n- Create a `Dockerfile` with the following contents:\n\n```\nFROM openwhisk/python3action:latest\n\nRUN apk add --update py-pip\nRUN pip install numpy\n```\n\n- Build the Docker image.\n\n```\ndocker build -t python3action:ml-libs .\n```\n\n- Tag the Docker image with your Docker Hub username.\n\n```\ndocker tag python3action:ml-libs <USER_NAME>/python3action:ml-libs\n```\n\n- Push the Docker image to Docker Hub.\n\n```\ndocker push <USER_NAME>/python3action:ml-libs\n```\n\n- Create a new Apache OpenWhisk action with the following source code:\n\n```python\nimport numpy\n\ndef main(params):\n    return {\n        \"numpy\": numpy.__version__\n    }\n```\n\n```\nwsk action create ml-libs --docker <USER_NAME>/python3action:ml-libs action.py\n```\n\n- Invoking the action should return the TensorFlow.js library versions available in the runtime.\n\n```\nwsk action invoke ml-libs --result\n```\n\n```\n{\n    \"numpy\": \"1.16.2\"\n}\n```\n\n## Creating native actions\n\nDocker support can also be used to run any executable file (from static binaries to shell scripts) on the platform. Executable files need to use the `openwhisk/dockerskeleton` [runtime image](https://github.com/apache/openwhisk-runtime-docker). Native actions can be created using the `--native` CLI flag, rather than explicitly specifying `dockerskeleton` as the runtime image name.\n\n### Usage\n\n```\nwsk action create my-action --native source.sh\nwsk action create my-action --native archive.zip\n```\n\nExecutables can either be text or binary files. Text-based executable files (e.g. shell scripts) are passed directly as the action source files. Binary files (e.g. C programs) must be named `exec` and packaged into a zip archive.\n\nNative action source files must be executable within the  `openwhisk/dockerskeleton` [runtime image](https://github.com/apache/openwhisk-runtime-docker). This means being compiled for the correct platform architecture, linking to the correct dynamic libraries  and using pre-installed external dependencies.\n\nWhen an invocation request is received by the runtime container, the native action file will be executed until the process exits. Action invocation parameters will be passed as a JSON string to `stdin`.\n\nWhen the process ends, the last line of text output to `stdout` will be parsed as the action result. This must contain a text string with a JSON object. All other text lines written to `stdout` will be treated as logging output and returned into the response logs for the activation.\n\n### Example (Shell Script)\n\n- Create a shell script called `script.sh` with the following contents.\n\n```\n#!/bin/bash\nARGS=$@\nNAME=`echo \"$ARGS\" | jq -r '.\"name\"'`\nDATE=`date`\necho \"{ \\\"message\\\": \\\"Hello $NAME! It is $DATE.\\\" }\"\n```\n\nAn action supports not only a JSON object but also a JSON array as a return value.\n\nIt would be a simple example that uses an array as a return value:\n\n```\n#!/bin/bash\necho '[\"a\", \"b\"]''\n```\n\nYou can also create a sequence action with actions accepting an array param and returning an array result.\n\nYou can easily figure out the parameters with the following example:\n\n```\n#!/bin/bash\necho $1\n```\n\n- Create an action from this shell script.\n\n```\n wsk action create bash script.sh --native\n```\n\n- Invoke the action with the `name` parameter.\n\n```\nwsk action invoke bash --result --param name James\n```\n\n```\n{\n    \"message\": \"Hello James! It is Thu Apr 18 15:24:23 UTC 2019.\"\n}\n```\n\n### Example (Static C Binary)\n\n- Create a C source file called `main.c` with the following contents:\n\n```c\n#include <stdio.h>\nint main(int argc, char *argv[]) {\n    printf(\"This is an example log message from an arbitrary C program!\\n\");\n    printf(\"{ \\\"msg\\\": \\\"Hello from arbitrary C program!\\\", \\\"args\\\": %s }\",\n           (argc == 1) ? \"undefined\" : argv[1]);\n}\n```\n\n- Create a shell script (`build.sh`) with the following contents:\n\n```\n#!/bin/bash\napk add gcc libc-dev\n\ngcc main.c -o exec\n```\n\n- Make the build script executable.\n\n```\nchmod 755 build.sh\n```\n\n- Compile the C binary using the `dockerskeleton` image and the build script.\n\n```\ndocker run -it -v $PWD:/action/ -w /action/ openwhisk/dockerskeleton ./build.sh\n```\n\n- Add the binary to a zip file.\n\n```\nzip -r action.zip exec\n```\n\n- Create an action from the zip file containing the binary.\n\n```\n wsk action create c-binary action.zip --native\n```\n\n- Invoke the action with the `name` parameter.\n\n```\nwsk action invoke c-binary --result --param name James\n```\n\n```\n{\n    \"args\": {\n        \"name\": \"James\"\n    },\n    \"msg\": \"Hello from arbitrary C program!\"\n}\n```\n"
  },
  {
    "path": "docs/actions-dotnet.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Creating and invoking .NET Core actions\n\nThe following sections guide you through creating and invoking a single .NET Core action.\n\nIn order to compile, test and archive .NET Core projects, you must have the [.NET Core SDK](https://www.microsoft.com/net/download) installed locally and the environment variable `DOTNET_HOME` set to the location where the `dotnet` executable can be found.\n\nA .NET Core action is a .NET Core class library with a method called `Main` that has the exact signature as follows:\n\n```csharp\npublic Newtonsoft.Json.Linq.JObject Main(Newtonsoft.Json.Linq.JObject);\n```\n\nFor example, create a C# project called `Apache.OpenWhisk.Example.Dotnet`:\n\n```bash\ndotnet new classlib -n Apache.OpenWhisk.Example.Dotnet -lang \"C#\"\ncd Apache.OpenWhisk.Example.Dotnet\n```\n\nInstall the [Newtonsoft.Json](https://www.newtonsoft.com/json) NuGet package as follows:\n\n```bash\ndotnet add package Newtonsoft.Json -v 12.0.1\n```\n\nNow create a file called `Hello.cs` with the following content:\n\n```csharp\nusing System;\nusing Newtonsoft.Json.Linq;\n\nnamespace Apache.OpenWhisk.Example.Dotnet\n{\n    public class Hello\n    {\n        public JObject Main(JObject args)\n        {\n            string name = \"stranger\";\n            if (args.ContainsKey(\"name\")) {\n                name = args[\"name\"].ToString();\n            }\n            JObject message = new JObject();\n            message.Add(\"greeting\", new JValue($\"Hello, {name}!\"));\n            return (message);\n        }\n    }\n}\n```\n\nPublish the project as follows:\n\n```bash\ndotnet publish -c Release -o out\n```\n\nZip the published files as follows:\n\n```bash\ncd out\nzip -r -0 helloDotNet.zip *\n```\n\nAn action supports not only a JSON object but also a JSON array as a return value.\n\nIt would be a simple example that uses an array as a return value:\n\n```csharp\nusing System;\nusing Newtonsoft.Json.Linq;\nnamespace Apache.OpenWhisk.Tests.Dotnet\n{\n    public class HelloArray\n    {\n        public JArray Main(JObject args)\n        {\n            JArray jarray = new JArray();\n            jarray.Add(\"a\");\n            jarray.Add(\"b\");\n            return (jarray);\n        }\n    }\n}\n```\nYou can also create a sequence action with actions accepting an array param and returning an array result.\n\nYou can easily figure out the parameters with the following example:\n\n```csharp\nusing System;\nusing Newtonsoft.Json.Linq;\nnamespace Apache.OpenWhisk.Tests.Dotnet\n{\n    public class HelloPassArrayParam\n    {\n        public JArray Main(JArray args)\n        {\n            return (args);\n        }\n    }\n}\n```\n\n### Create the .NET Core Action\n\nYou need to specify the name of the function handler using `--main` argument.\nThe value for `main` needs to be in the following format:\n`{Assembly}::{Class Full Name}::{Method}`, e.q.,\n`Apache.OpenWhisk.Example.Dotnet::Apache.OpenWhisk.Example.Dotnet.Hello::Main`\n\nTo use on a deployment of OpenWhisk that contains the runtime as a kind:\n\n```bash\nwsk action update helloDotNet helloDotNet.zip --main Apache.OpenWhisk.Example.Dotnet::Apache.OpenWhisk.Example.Dotnet.Hello::Main --kind dotnet:2.2\n```\n\n### Invoke the .NET Core Action\n\nAction invocation is the same for .NET Core actions as it is for Swift and JavaScript actions:\n\n```bash\nwsk action invoke --result helloDotNet --param name World\n```\n\n```json\n  {\n      \"greeting\": \"Hello World!\"\n  }\n```\n"
  },
  {
    "path": "docs/actions-go.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n<a name=\"golang\"/>\n\n# Creating and Invoking Go Actions\n\nThe `action-golang-v1.15` runtime can execute actions written in the Go programming language in OpenWhisk, either as precompiled binary or compiling sources on the fly.\n\n## Entry Point\n\nThe source code of an action is one or more Go source files. The entry point of the action is a function, placed in the `main` package. The default name for the main function is `Main`, but you can change it to any name you want using the `--main` switch in `wsk`. The name is however always capitalized. The function must have a specific signature, as described next.\n\n*NOTE* The runtime does *not* support different packages from `main` for the entry point. If you specify `hello.main` the runtime will try to use `Hello.main`, that will be almost certainly incorrect. You can however have other packages in your sources, as described below.\n\n## Signature\n\nThe expected signature for a `main` function is:\n\n`func Main(event map[string]interface{}) map[string]interface{}`\n\nSo a very simple single file `hello.go` action would be:\n\n```go\npackage main\n\nimport \"log\"\n\n// Main is the function implementing the action\nfunc Main(obj map[string]interface{}) map[string]interface{} {\n  // do your work\n  name, ok := obj[\"name\"].(string)\n  if !ok {\n    name = \"world\"\n  }\n  msg := make(map[string]interface{})\n  msg[\"message\"] = \"Hello, \" + name + \"!\"\n  // log in stdout or in stderr\n  log.Printf(\"name=%s\\n\", name)\n  // encode the result back in json\n  return msg\n}\n```\n\nAn action supports not only a JSON object but also a JSON array as a return value.\n\nIt would be a simple example that uses an array as a return value:\n\n```go\npackage main\n// Main is the function implementing the action\nfunc Main(event map[string]interface{}) []interface{} {\n        result := []interface{}{\"a\", \"b\"}\n        return result\n}\n```\n\nyou can also create a sequence action with actions accepting an array param and returning an array result.\n\nYou can easily figure out the parameters with the following example:\n\n```go\npackage main\n// Main is the function implementing the action\nfunc Main(obj []interface{}) []interface{} {\n        return obj\n}\n```\n\nYou can deploy it with just:\n\n```\nwsk action create hello-go hello.go\n```\n\nYou can also have multiple source files in an action, packages and vendor folders.\n\n## Deployment\n\nThe runtime `action-golang-v1.15` accepts:\n\n- executable binaries in Linux ELF executable compiled for the AMD64 architecture\n- zip files containing a binary executable named `exec` at the top level, again a Linux ELF executable compiled for the AMD64 architecture\n- a single source file in Go, that will be compiled\n- a zip file not containing in the top level a binary file `exec`, it will be interpreted as a collection of source files in Go, and compiled\n\nYou can create a binary in the correct format on any Go platform cross-compiling with `GOOS=Linux` and `GOARCH=amd64`. However it is recommended you use the compiler embedded in the Docker image for this purpose using the precompilation feature, as described below.\n\n## Using packages and vendor folder\n\nWhen you deploy a zip file, you can:\n\n- have all your functions in the `main` package\n- have some functions placed in some packages, like `hello`\n- have some third party dependencies you want to include in your sources\n\nIf all your functions are in the main package, just place all your sources in the top level of your zip file.\n\n### Use a package folder\n\nIf some functions belongs to a package, like `hello/`, you need to be careful with the layout of your sources, especially if you use editors like [VcCode](#vscode), and make. The layout recommended is the following:\n\n```\ngolang-main-package/\n- Makefile\n- src/\n   - main.go\n   - main_test.go\n   - hello/\n       - hello.go\n       - hello_test.go\n```\n\nFor running tests, editing without errors with package resolution, you need to use a `src` folder, place the sources that belongs to the main package in the `src` and place sources of your package in the `src/hello` folder.\n\nYou should import it your subpackage with `import \"hello\"`.\nNote this means if you want to compile locally you have to set your `GOPATH` to parent of your `src` directory. If you use VSCode, you need to enable the `go.inferGopath` option.\n\nWhen you send the sources, you will have to zip the content of the `src` folder, *not* the main directory. For example:\n\n```\ncd src\nzip -r ../hello.zip *\ncd ..\nwsk action create hellozip hello.zip --kind go:1.15\n```\n\nCheck the example [golang-main-package](https://github.com/apache/openwhisk-runtime-go/tree/master/examples/golang-main-package) and the associated `Makefile`.\n\n### Using vendor folders\n\nWhen you need to use third party libraries, the runtime does not download them from Internet when compiling. You have to provide them,  downloading and placing them using the `vendor` folder mechanism. We are going to show here how to use the vendor folder with the `dep` tool.\n\n*NOTE* the `vendor` folder does not work at the top level, you have to use a `src` folder and a package folder to have also the `vendor` folder. If you want use the vendor folder for the `main` package, you can do it but instead of placing files that belongs to the `main` package in the top-level, you have to place in a subfolder named `main`.\n\nFor example consider you have in the file `src/hello/hello.go` the import:\n\n```\nimport \"github.com/sirupsen/logrus\"\n```\n\nTo create a vendor folder, you need to\n\n- install the [dep](https://github.com/golang/dep) tool\n- cd to the `src/hello` folder (*not* the `src` folder)\n- run `DEPPROJECTROOT=$(realpath $PWD/../..) dep init` the first time\n\nThe tool will detect the used libraries and create 2 manifest files `Gopkg.lock` and `Gopkg.toml`. If already have the manifest files, you just need `dep ensure` to create and populate the `vendor` folder.\n\nThe layout will be something like this:\n\n```\ngolang-hello-vendor\n- Makefile\n- src/\n    - hello.go\n    - hello/\n      - Gopkg.lock\n      - Gopkg.toml\n         - hello.go\n         - hello_test.go\n         - vendor/\n            - github.com/...\n            - golang.org/...\n```\n\nCheck the example [golang-hello-vendor](https://github.com/apache/openwhisk-runtime-go/tree/master/examples/golang-hello-vendor)\n\nNote you do not need to store the `vendor` folder in the version control system as it can be regenerated, you only the manifest files. However, you need to include the entire vendor folder when you deploy the action in source format for compilation by the runtime.\n\nIf you need to use vendor folder in the main package, you need to create a directory `main` and place all the source code that would normally go in the top level, in the `main` folder instead.  A vendor folder in the top level *does not work*.\n\n\n<a name=\"precompile\"/>\n\n## Precompiling Go Sources Offline\n\nCompiling sources on the image can take some time when the images is initialized. You can speed up precompiling the sources using the image `action-golang-v1.15` as an offline compiler. You need `docker` for doing that.\n\nThe images accepts a `-compile <main>` flag, and expects you provide sources in standard input. It will then compile them, emit the binary in standard output and errors in stderr. The output is always a zip file containing an executable.\n\nIf you have a single source maybe in file `main.go`, with a function named `Main` just do this:\n\n`docker run openwhisk/action-golang-v1.15 -compile main <main.go >main.zip`\n\nIf you have multiple sources in current directory, even with a subfolder with sources, you can compile it all with:\n\n```\ncd src\nzip -r ../src.zip *\ncd ..\ndocker -i run openwhisk/action-golang-v1.15 -compile main <src.zip >exec.zip\n```\n\nNote that the output is always a zip file in  Linux AMD64 format so the executable can be run only inside a Docker Linux container.\n\nHere a `Makefile` is helpful. Check the [examples](https://github.com/apache/openwhisk-runtime-go/tree/master/examples) for a collection of tested Makefiles. The  generated executable is suitable to be deployed in OpenWhisk, so you can do:\n\n`wsk action create my-action exec.zip --kind go:1.15`\n\nYou can also use just the `openwhisk/actionloop` as runtime, it is smaller.\n\n<a name=\"vscode\"/>\n\n## Using VsCode\n\nIf you are using [VsCode](https://code.visualstudio.com/) as your Go development environment with the [VsCode Go](https://marketplace.visualstudio.com/items?itemName=ms-vscode.Go) support, without errors and with completion working you need to:\n\n- enable the option `go.inferGopath`\n- place all your sources in a `src` folder\n- either to open the `src` folder as the top level source or add it as a folder in the workspace (it is not enough just have it as a subfolder)\n- create a `dummy.go` an empty main - it will not be used but it will shut up \"`main.main` missing error detection\"\n"
  },
  {
    "path": "docs/actions-java.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Creating and invoking Java actions\n\nThe process of creating Java actions is similar to that of [other actions](actions.md#the-basics).\nThe following sections guide you through creating and invoking a single Java action,\nand demonstrate how to bundle multiple files and third party dependencies.\n\nIn order to compile, test and archive Java files, you must have a\n[JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/index.html) installed locally.\n\nA Java action is a Java program with a method called `main` that has the exact signature as follows:\n```java\npublic static com.google.gson.JsonObject main(com.google.gson.JsonObject);\n```\n\nFor example, create a Java file called `Hello.java` with the following content:\n\n```java\nimport com.google.gson.JsonObject;\n\npublic class Hello {\n    public static JsonObject main(JsonObject args) {\n        String name = \"stranger\";\n        if (args.has(\"name\"))\n            name = args.getAsJsonPrimitive(\"name\").getAsString();\n        JsonObject response = new JsonObject();\n        response.addProperty(\"greeting\", \"Hello \" + name + \"!\");\n        return response;\n    }\n}\n```\n\nAn action supports not only a JSON object but also a JSON array as a return value.\n\nIt would be a simple example that uses an array as a return value:\n\n```java\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\npublic class HelloArray {\n    public static JsonArray main(JsonObject args) {\n        JsonArray jsonArray = new JsonArray();\n        jsonArray.add(\"a\");\n        jsonArray.add(\"b\");\n        return jsonArray;\n    }\n}\n```\n\nYou can also create a sequence action with actions accepting an array param and returning an array result.\n\nYou can easily figure out the parameters with the following example:\n\n```java\nimport com.google.gson.JsonArray;\npublic class Sort {\n    public static JsonArray main(JsonArray args) {\n        return args;\n    }\n}\n```\n\nThen, compile `Hello.java` into a JAR file `hello.jar` as follows:\n```\njavac Hello.java\n```\n```\njar cvf hello.jar Hello.class\n```\n\n**Note:** [google-gson](https://github.com/google/gson) must exist in your Java CLASSPATH when compiling the Java file.\n\nYou can create a OpenWhisk action called `helloJava` from this JAR file as\nfollows:\n\n```\nwsk action create helloJava hello.jar --main Hello\n```\n\nWhen you use the command line and a `.jar` source file, you do not need to\nspecify that you are creating a Java action;\nthe tool determines that from the file extension.\n\nYou need to specify the name of the main class using `--main`. An eligible main\nclass is one that implements a static `main` method as described above. If the\nclass is not in the default package, use the Java fully-qualified class name,\ne.g., `--main com.example.MyMain`.\n\nIf needed you can also customize the method name of your Java action. This\ncan be done by specifying the Java fully-qualified method name of your action,\ne.q., `--main com.example.MyMain#methodName`\n\nAction invocation is the same for Java actions as it is for Swift and JavaScript actions:\n\n```\nwsk action invoke --result helloJava --param name World\n```\n\n```json\n  {\n      \"greeting\": \"Hello World!\"\n  }\n```\n\nFind out more about parameters in the [Working with parameters](./parameters.md) section.\n"
  },
  {
    "path": "docs/actions-new.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n## Adding Action Language Runtimes\n\nOpenWhisk supports [several languages and runtimes](actions.md#languages-and-runtimes) but\nthere may be other languages or runtimes that are important for your organization, and for\nwhich you want tighter integration with the platform. The OpenWhisk platform is extensible\nand you can add new languages or runtimes (with custom packages and third-party dependencies)\nfollowing the guide described here.\n\n### Runtime general requirements\n\nThe unit of execution for all functions is a [Docker container](https://docs.docker.com) which\nmust implement a specific [Action interface](#action-interface) that, in general performs:\n\n1. **[Initialization](#initialization)** - accepts an initialization payload (the code) and prepared for execution,\n2. **[Activation](#activation)** - accepts a runtime payload (the input parameters) and\n   - prepares the activation context,\n   - runs the function,\n   - returns the function result,\n3. **[Logging](#logging)** - flushes all `stdout` and `stderr` logs and adds a frame marker at the end of the activation.\n\nThe specifics of the [Action interface](#action-interface) and its functions are shown below.\n\n### Platform requirements\n\nIn order for your language runtime to be properly recognized by the OpenWhisk platform,\nand officially recognized by the Apache OpenWhisk project, please follow these\nrequirements and best practices:\n\n1. Implement the runtime in its own repository to permit a management lifecycle independent of the rest of the OpenWhisk platform.\n2. Introduce the runtime specification into the [runtimes manifest](../ansible/files/runtimes.json),\n3. Add the runtime to the [Swagger file](../core/controller/src/main/resources/apiv1swagger.json)\n4. Add a new `actions-<your runtime>.md` file to the [docs](.) directory,\n5. Add a link to your new runtime doc to the [top level actions index](actions.md#languages-and-runtimes).\n6. Add a standard [test action](#the-test-action) to the [tests artifacts directory](../tests/dat/actions/unicode.tests) (as shown below),\n\nThe new runtime repository should conform to the [Canonical runtime repository](#canonical-runtime-repository) layout (as shown below).\nFurther, you should automate and pass the following test suites:\n- [Action Interface tests](#action-interface-tests)\n- [Runtime proxy tests](#runtime-proxy-tests)\n\n### The runtimes manifest\n\nActions when created specify the desired runtime for the function via a property called \"kind\".\nWhen using the `wsk` CLI, this is specified as `--kind <runtime-kind>`. The value is typically\na string describing the language (e.g., `nodejs`) followed by a colon and the version for the runtime\nas in `nodejs:20` or `php:8.1`.\n\nThe manifest is a map of runtime family names to an array of specific kinds. The details of the\nschema are found in the [Exec Manifest](../common/scala/src/main/scala/org/apache/openwhisk/core/entity/ExecManifest.scala).\nAs an example, the following entry add a new runtime family called `nodejs` with a single kind\n`nodejs:20`.\n\n```json\n{\n  \"nodejs\": [{\n    \"kind\": \"nodejs:20\",\n    \"default\": true,\n    \"image\": {\n      \"prefix\": \"openwhisk\",\n      \"name\": \"action-nodejs-v20\",\n      \"tag\": \"latest\"\n    }\n  }]\n}\n```\n\nThe `default` property indicates if the corresponding kind should be treated as the\ndefault for the runtime family. The JSON `image` structure defines the Docker image name\nthat is used for actions of this kind (e.g., `openwhisk/nodejs10action:latest` for the\nJSON example above).\n\n### Canonical runtime repository\n\nThe runtime repository should follow the canonical structure used by other runtimes.\n\n```\n/path/to/runtime\n├── build.gradle         # Gradle build file to integrate with rest of build and test framework\n├── core\n│   └── <runtime name and version>\n│       ├── Dockerfile   # builds the runtime's Docker image\n│       └── ...          # your runtimes files which implement the action proxy\n└── tests\n    └── src              # tests suits...\n        └── ...          # ... which extend canonical interface plus additional runtime specific tests\n```\n\nThe [Docker skeleton repository](https://github.com/apache/openwhisk-runtime-docker)\nis an example starting point to fork and modify for your new runtime.\n\n### The test action\n\nThe standard test action is shown below in JavaScript. It should be adapted for the\nnew language and added to the [test artifacts directory](../tests/dat/actions/unicode.tests)\nwith the name `<runtime-kind>.txt` for plain text file or `<runtime-kind>.bin` for\na binary file. The `<runtime-kind>` must match the value used for `kind` in the corresponding\nruntime manifest entry, replacing `:` in the kind with a `-`.\nFor example, a plain text function for `nodejs:20` becomes `nodejs-20.txt`.\n\n```js\nfunction main(args) {\n    var str = args.delimiter + \" ☃ \" + args.delimiter;\n    console.log(str);\n    return { \"winter\": str };\n}\n```\n\n### Action Interface\n\nAn action consists of the user function (and its dependencies) along with a _proxy_ that implements a\ncanonical protocol to integrate with the OpenWhisk platform.\n\nThe proxy is a web server with two endpoints.\n* It listens on port `8080`.\n* It implements `/init` to initialize the container.\n* It also implements `/run` to activate the function.\n\nThe proxy also prepares the\n[execution context](actions.md#accessing-action-metadata-within-the-action-body),\nand flushes the logs produced by the function to stdout and stderr.\n\n#### Initialization\n\nThe initialization route is `/init`. It must accept a `POST` request with a JSON object as follows:\n```\n{\n  \"value\": {\n    \"name\" : String,\n    \"main\" : String,\n    \"code\" : String,\n    \"binary\": Boolean,\n    \"env\": Map[String, String]\n  }\n}\n```\n\n* `name` is the name of the action.\n* `main` is the name of the function to execute.\n* `code` is either plain text or a base64 encoded string for binary functions (i.e., a compiled executable).\n* `binary` is false if `code` is in plain text, and true if `code` is base64 encoded.\n* `env` is a map of key-value pairs of properties to export to the environment. And contains several properties starting with the `__OW_` prefix that are specific to the running action.\n  * `__OW_API_KEY` the API key for the subject invoking the action, this key may be a restricted API key. This property is absent unless explicitly [requested](./annotations.md#annotations-for-all-actions).\n  * `__OW_NAMESPACE` the namespace for the _activation_ (this may not be the same as the namespace for the action).\n  * `__OW_ACTION_NAME` the fully qualified name of the running action.\n  * `__OW_ACTION_VERSION` the internal version number of the running action.\n  * `__OW_ACTIVATION_ID` the activation id for this running action instance.\n  * `__OW_DEADLINE` the approximate time when this initializer will have consumed its entire duration quota (measured in epoch milliseconds).\n\n\nThe initialization route is called exactly once by the OpenWhisk platform, before executing a function.\nThe route should report an error if called more than once. It is possible however that a single initialization\nwill be followed by many activations (via `/run`). If an `env` property is provided, the corresponding environment\nvariables should be defined before the action code is initialized.\n\n**Successful initialization:** The route should respond with `200 OK` if the initialization is successful and\nthe function is ready to execute. Any content provided in the response is ignored.\n\n**Failures to initialize:** Any response other than `200 OK` is treated as an error to initialize. The response\nfrom the handler if provided must be a JSON object with a single field called `error` describing the failure.\nThe value of the error field may be any valid JSON value. The proxy should make sure to generate meaningful log\nmessage on failure to aid the end user in understanding the failure.\n\n**Time limit:** Every action in OpenWhisk has a defined time limit (e.g., 60 seconds). The initialization\nmust complete within the allowed duration. Failure to complete initialization within the allowed time frame\nwill destroy the container.\n\n**Limitation:** The proxy does not currently receive any of the activation context at initialization time.\nThere are scenarios where the context is convenient if present during initialization. This will require a\nchange in the OpenWhisk platform itself. Note that even if the context is available during initialization,\nit must be reset with every new activation since the information will change with every execution.\n\n#### Activation\n\nThe proxy is ready to execute a function once it has successfully completed initialization. The OpenWhisk\nplatform will invoke the function by posting an HTTP request to `/run` with a JSON object providing a new\nactivation context and the input parameters for the function. There may be many activations of the same\nfunction against the same proxy (viz. container). Currently, the activations are guaranteed not to overlap\n— that is, at any given time, there is at most one request to `/run` from the OpenWhisk platform.\n\nThe route must accept a JSON object and respond with a JSON object, otherwise the OpenWhisk platform will\ntreat the activation as a failure and proceed to destroy the container. The JSON object provided by the\nplatform follows the following schema:\n```\n{\n  \"value\": JSON,\n  \"namespace\": String,\n  \"action_name\": String,\n  \"api_host\": String,\n  \"api_key\": String,\n  \"activation_id\": String,\n  \"transaction_id\": String,\n  \"deadline\": Number\n}\n```\n\n* `value` is a JSON object and contains all the parameters for the function activation.\n* `namespace` is the OpenWhisk namespace for the action (e.g., `whisk.system`).\n* `action_name` is the [fully qualified name](reference.md#fully-qualified-names) of the action.\n* `activation_id` is a unique ID for this activation.\n* `transaction_id` is a unique ID for the request of which this activation is part of.\n* `deadline` is the deadline for the function.\n* `api_key` is the API key used to invoke the action.\n\nThe `value` is the function parameters. The rest of the properties become part of the activation context\nwhich is a set of environment variables constructed by capitalizing each of the property names, and prefixing\nthe result with `__OW_`. Additionally, the context must define `__OW_API_HOST` whose value\nis the OpenWhisk API host. This value is currently provided as an environment variable defined at container\nstartup time and hence already available in the context.\n\n**Successful activation:** The route must respond with `200 OK` if the activation is successful and\nthe function has produced a JSON object as its result. The response body is recorded as the [result\nof the activation](actions.md#understanding-the-activation-record).\n\n**Failed activation:** Any response other than `200 OK` is treated as an activation error. The response\nfrom the handler must be a JSON object with a single field called `error` describing the failure.\nThe value of the error field may be any valid JSON value. Should the proxy fail to respond with a JSON\nobject, the OpenWhisk platform will treat the failure as an uncaught exception. These two failures modes are\ndistinguished by the value of the `response.status` in the [activation record](actions.md#understanding-the-activation-record)\nwhich is \"application error\" if the proxy returned an \"error\" object, and \"action developer error\" otherwise.\n\n**Time limit:** Every action in OpenWhisk has a defined time limit (e.g., 60 seconds). The activation\nmust complete within the allowed duration. Failure to complete activation within the allowed time frame\nwill destroy the container.\n\n#### Logging\n\nThe proxy must flush all the logs produced during initialization and execution and add a frame marker\nto denote the end of the log stream for an activation. This is done by emitting the token\n[`XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX`](https://github.com/apache/openwhisk/blob/59abfccf91b58ee39f184030374203f1bf372f2d/core/invoker/src/main/scala/whisk/core/containerpool/docker/DockerContainer.scala#L51)\nas the last log line for the `stdout` _and_ `stderr` streams. Failure to emit this marker will cause delayed\nor truncated activation logs.\n\n### Testing\n\n#### Action Interface tests\n\nThe [Action interface](#action-interface) is enforced via a [canonical test suite](../tests/src/test/scala/actionContainers/BasicActionRunnerTests.scala) which validates the initialization protocol, the runtime protocol,\nensures the activation context is correctly prepared, and that the logs are properly framed. Your\nruntime should extend this test suite, and of course include additional tests as needed.\n\n#### Runtime proxy tests\n\nThere is a [canonical test harness](../tests/src/test/scala/actionContainers/BasicActionRunnerTests.scala)\nfor validating a new runtime.\n\nThe tests verify that the proxy can handle the following scenarios:\n* Test the proxy can handle the identity functions (initialize and run).\n* Test the proxy can handle pre-defined environment variables as well as initialization parameters.\n* Test the proxy properly constructs the activation context.\n* Test the proxy can properly handle functions with Unicode characters.\n* Test the proxy can handle large payloads (more than 1MB).\n* Test the proxy can handle an entry point other than \"main\".\n* Test the proxy does not permit re-initialization.\n* Test the error handling for an action returning an invalid response.\n* Test the proxy when initialized with no content.\n\nThe canonical test suite should be extended by the new runtime tests. Additional\ntests will be required depending on the feature set provided by the runtime.\n\nSince the OpenWhisk platform is language and runtime agnostic, it is generally not\nnecessary to add integration tests. That is the unit tests verifying the protocol are\nsufficient. However, it may be necessary in some cases to modify the `wsk` CLI or\nother OpenWhisk clients. In which case, appropriate tests should be added as necessary.\nThe OpenWhisk platform will perform a generic integration test as part of its basic\nsystem tests. This integration test will require a [test function](#the-test-action) to\nbe available so that the test harness can create, invoke, and delete the action.\n\n### Supporting Additional Execution Environments\n\nThere are now several runtimes that support execution environments in addition to OpenWhisk. Currently only an interface for single entrypoint execution environments has been defined, but more could be defined in the future.\n\n#### Action Proxy Single Entrypoint Interface\n\nSingle entrypoint proxies are proxies that have only one addressable HTTP endpoint. They do not use `/init` and `/run` endpoints utilized by standard OpenWhisk runtime environments; instead both the initialization and activation are handled through one endpoint. The first example of such a proxy was implemented for Knative Serving, but the same interface can be used for any single entrypoint execution environment. In an effort to standardize how the various action proxy implementation containers are able to handle single entrypoint execution environments (such as Knative Serving), there is a description of the contract and example cases outlining how a container should respond with a given input. The descriptions and example cases are documented in [Single Entrypoint Proxy Contract](single_entrypoint_proxy_contract.md).\n"
  },
  {
    "path": "docs/actions-nodejs.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Creating and invoking JavaScript actions\n\nThe process of creating JavaScript actions is similar to that of [other actions](actions.md#the-basics).\nThe following sections guide you through creating and invoking a single JavaScript action,\nand demonstrate how to bundle multiple JavaScript files and third party dependencies.\n\n1. Create a JavaScript file with the following content. For this example, the file name is `hello.js`.\n\n  ```javascript\n  function main() {\n      return { msg: 'Hello world' };\n  }\n  ```\n\n  An action supports not only a JSON object but also a JSON array as a return value.\n\n  It would be a simple example that uses an array as a return value:\n\n  ```javascript\n  function main(params) {\n    return [\"a\", \"b\"];\n  }\n  ```\n\n  You can also create a sequence action with actions accepting an array param and returning an array result.\n\n  You can easily figure out the parameters with the following example:\n\n  ```javascript\n  /**\n   * Sort a set of lines.\n   * @param lines An array of strings to sort.\n   */\n  function main(msg) {\n      var lines = msg || [];\n      lines.sort();\n      return lines;\n  }\n  ```\n\n  The JavaScript file might contain additional functions.\n  However, by convention, a function called `main` must exist to provide the entry point for the action.\n\n2. Create an action from the following JavaScript function. For this example, the action is called `hello`.\n\n  ```\n  wsk action create hello hello.js\n  ok: created action hello\n  ```\n\n  The CLI automatically infers the type of the action by using the source file extension.\n  For `.js` source files, the action runs by using a Node.js runtime. You may specify\n  the Node.js runtime to use by explicitly specifying the parameter `--kind nodejs:18`, or `--kind nodejs:20`.\n\n\n## Creating asynchronous actions\n\nJavaScript functions that run asynchronously may need to return the activation result after the `main` function has returned. You can accomplish this by returning a Promise in your action.\n\n1. Save the following content in a file called `asyncAction.js`.\n\n  ```javascript\n  function main(args) {\n       return new Promise(function(resolve, reject) {\n         setTimeout(function() {\n           resolve({ done: true });\n         }, 2000);\n      })\n   }\n  ```\n\n  Notice that the `main` function returns a Promise, which indicates that the activation hasn't completed yet, but is expected to in the future.\n\n  The `setTimeout()` JavaScript function in this case waits for two seconds before calling the callback function.  This represents the asynchronous code and goes inside the Promise's callback function.\n\n  The Promise's callback takes two arguments, resolve and reject, which are both functions.  The call to `resolve()` fulfills the Promise and indicates that the activation has completed normally.\n\n  A call to `reject()` can be used to reject the Promise and signal that the activation has completed abnormally.\n\n2. Run the following commands to create the action and invoke it:\n\n  ```\n  wsk action create asyncAction asyncAction.js\n  ```\n  ```\n  wsk action invoke --result asyncAction\n  ```\n  ```json\n  {\n      \"done\": true\n  }\n  ```\n\n  Notice that you performed a blocking invocation of an asynchronous action.\n\n3. Fetch the activation log to see how long the activation took to complete:\n\n  ```\n  wsk activation list --limit 1 asyncAction\n  ```\n<pre>\nDatetime            Activation ID                    Kind      Start Duration   Status  Entity\n2019-03-16 19:46:43 64581426b44e4b3d981426b44e3b3d19 nodejs:6  cold  2.033s     success guest/asyncAction:0.0.1\n</pre>\n  ```\n  wsk activation get 64581426b44e4b3d981426b44e3b3d19\n  ```\n ```json\n  {\n      \"start\": 1552762003015,\n      \"end\":   1552762005048,\n      ...\n  }\n ```\n\n  Comparing the `start` and `end` time stamps in the activation record, you can see that this activation took slightly over two seconds to complete.\n\n## Using actions to call an external API\n\nThe examples so far have been self-contained JavaScript functions. You can also create an action that calls an external API.\n\nThis example invokes a Yahoo Weather service to get the current conditions at a specific location.\n\n1. Save the following content in a file called `weather.js`.\n\n  ```javascript\n  var request = require('request');\n\n  function main(params) {\n      var location = params.location || 'Vermont';\n      var url = 'https://query.yahooapis.com/v1/public/yql?q=select item.condition from weather.forecast where woeid in (select woeid from geo.places(1) where text=\"' + location + '\")&format=json';\n\n      return new Promise(function(resolve, reject) {\n          request.get(url, function(error, response, body) {\n              if (error) {\n                  reject(error);\n              }\n              else {\n                  var condition = JSON.parse(body).query.results.channel.item.condition;\n                  var text = condition.text;\n                  var temperature = condition.temp;\n                  var output = 'It is ' + temperature + ' degrees in ' + location + ' and ' + text;\n                  resolve({msg: output});\n              }\n          });\n      });\n  }\n  ```\n\n  Note that the action in the example uses the JavaScript `request` library to make an HTTP request to the Yahoo Weather API, and extracts fields from the JSON result.\n  See the JavaScript [reference](#reference) for the Node.js packages available in the runtime environment.\n\n  This example also shows the need for asynchronous actions. The action returns a Promise to indicate that the result of this action is not available yet when the function returns. Instead, the result is available in the `request` callback after the HTTP call completes, and is passed as an argument to the `resolve()` function.\n\n2. Create an action from the `weather.js` file:\n\n  ```\n  wsk action create weather weather.js\n  ```\n\n3. Use the following command to run the action, and observe the output:\n  ```\n  wsk action invoke --result weather --param location \"Brooklyn, NY\"\n  ```\n\n  Using the `--result` flag means that the value returned from the action is shown as output on the command-line:\n\n  ```json\n  {\n      \"msg\": \"It is 28 degrees in Brooklyn, NY and Cloudy\"\n  }\n  ```\n\nThis example also passed a parameter to the action by using the `--param` flag and a value that can be changed each time the action is invoked. Find out more about parameters in the [Working with parameters](./parameters.md) section.\n\n## Packaging actions as Node.js modules with NPM libraries\n\nInstead of writing all your action code in a single JavaScript source file, actions can be deployed from a zip file containing a [Node.js module](https://nodejs.org/docs/latest-v10.x/api/modules.html#modules_modules).\n\nArchive zip files are extracted into the runtime environment and dynamically imported using `require()` during initialisation. **Actions packaged as a zip file MUST contain a valid `package.json` with a `main` field used to denote the [module index file](https://nodejs.org/docs/latest-v10.x/api/modules.html#modules_folders_as_modules) to return.**\n\nIncluding a `node_modules` folder in the zip file means external NPM libraries can be used on the platform.\n\n### Simple Example\n\n- Create the following `package.json` file:\n\n```json\n{\n  \"name\": \"my-action\",\n  \"main\": \"index.js\",\n  \"dependencies\" : {\n    \"left-pad\" : \"1.1.3\"\n  }\n}\n```\n\n- Create the following `index.js` file:\n\n```javascript\nfunction myAction(args) {\n    const leftPad = require(\"left-pad\")\n    const lines = args.lines || [];\n    return { padded: lines.map(l => leftPad(l, 30, \".\")) }\n}\n\nexports.main = myAction;\n```\n\nFunctions are exported from a module by setting properties on the `exports` object. The `--main` property on the action can be used to configure the module function invoked by the platform (this defaults to `main`).\n\n- Install module dependencies using NPM.\n\n```\nnpm install\n```\n\n- Create a `.zip` archive containing all files (including all dependencies).\n\n```\nzip -r action.zip *\n```\n\n> Please note: Using the Windows Explorer action for creating the zip file will result in an incorrect structure. OpenWhisk zip actions must have `package.json` at the root of the zip, while Windows Explorer will put it inside a nested folder. The safest option is to use the command line `zip` command as shown above.\n\n- Create the action from the zip file.\n\n```\nwsk action create packageAction --kind nodejs:20 action.zip\n```\n\nWhen creating an action from a `.zip` archive with the CLI tool, you must explicitly provide a value for the `--kind` flag by using `nodejs:18`, or `nodejs:20`.\n\n- Invoke the action as normal.\n\n```\nwsk action invoke --result packageAction --param lines \"[\\\"and now\\\", \\\"for something completely\\\", \\\"different\\\" ]\"\n```\n```json\n{\n    \"padded\": [\n        \".......................and now\",\n        \"......for something completely\",\n        \".....................different\"\n    ]\n}\n```\n\n### Handling NPM Libraries with Native Dependencies\n\nNode.js libraries can import native dependencies needed by the modules. These native dependencies are compiled upon installation to ensure they work in the local runtime. Native dependencies for NPM libraries must be compiled for the correct platform architecture to work in Apache OpenWhisk.\n\nThere are two approaches to using libraries with native dependencies...\n\n1. Run `npm install` inside a Docker container from the platform images.\n2. Building custom runtime image with libraries pre-installed.\n\n**The first approach is easiest but can only be used when a zip file containing all source files and libraries is less than the action size limit (48MB).**\n\n#### Running `npm install` inside runtime container\n\n - Run the following command to bind the local directory into the runtime container and run `npm install`.\n\n```\ndocker run -it -v $PWD:/nodejsAction openwhisk/action-nodejs-v10 \"npm install\"\n```\n This will leave a `node_modules` folder with native dependencies compiled for correct runtime.\n\n - Zip up the action source files including `node_modules` directory.\n\n```\nzip -r action.zip *\n```\n\n- Create new action with action archive.\n\n```\nwsk action create my-action --kind nodejs:20 action.zip\n```\n\n#### Building custom runtime image\n\n- Create a `Dockerfile` with the `npm install` command run during build.\n\n```\nFROM openwhisk/action-nodejs-v10\n\nRUN npm install <LIB_WITH_NATIVE_DEPS>\n```\n\n- Build and push the image to Docker Hub.\n\n```\n$ docker build -t <USERNAME>/custom-runtime .\n$ docker push <USERNAME>/custom-runtime\n```\n\n- Create new action using custom runtime image.\n\n```\nwsk action create my-action --docker <USERNAME>/custom-runtime action.zip\n```\n\n**Make sure the `node_modules` included in the `action.zip` does not include the same libraries folders.**\n\n## Using JavaScript Bundlers to package action source files\n\nUsing a JavaScript module bundler can transform application source files (with external dependencies) into a single compressed JavaScript file. This can lead to faster deployments, lower cold-starts and allow you to deploy large applications where individual sources files in a zip archive are larger than the action size limit.\n\nHere are the instructions for how to use three popular module bundlers with the Node.js runtime. The \"left pad\" action example will be used as the source file for bundling along with the external library.\n\n### Using rollup.js ([https://rollupjs.org](https://rollupjs.org))\n\n- Re-write the `index.js` to use ES6 Modules, rather than CommonJS module format.\n\n```javascript\nimport leftPad from 'left-pad';\n\nfunction myAction(args) {\n  const lines = args.lines || [];\n  return { padded: lines.map(l => leftPad(l, 30, \".\")) }\n}\n\nexport const main = myAction\n```\n\n*Make sure you export the function using the `const main = ...` pattern. Using `export {myAction as main}` does not work due to tree-shaking. See this [blog post](https://boneskull.com/rollup-for-javascript-actions-on-openwhisk/) for full details on why this is necessary.*\n\n- Create the Rollup.js configuration file in `rollup.config.js` with the following contents.\n\n```javascript\nimport commonjs from 'rollup-plugin-commonjs';\nimport resolve from 'rollup-plugin-node-resolve';\n\nexport default {\n  input: 'index.js',\n  output: {\n    file: 'bundle.js',\n    format: 'cjs'\n  },\n  plugins: [\n    resolve(),\n    commonjs()\n  ]\n};\n```\n\n- Install the Rollup.js library and plugins using NPM.\n\n```\nnpm install rollup rollup-plugin-commonjs rollup-plugin-node-resolve --save-dev\n```\n\n- Run the Rollup.js tool using the configuration file.\n\n```\nnpx rollup --config\n```\n\n- Create an action using the bundle source file.\n\n```\nwsk action create my-action bundle.js --kind nodejs:20\n```\n\n- Invoke the action as normal. Results should be the same as the example above.\n\n```\nwsk action invoke my-action --result --param lines \"[\\\"and now\\\", \\\"for something completely\\\", \\\"different\\\" ]\"\n```\n\n### Using webpack ([https://webpack.js.org/](https://webpack.js.org/))\n\n- Change `index.js` to export the `main` function using as a global reference.\n\n```javascript\nconst leftPad = require('left-pad');\n\nfunction myAction(args) {\n  const lines = args.lines || [];\n  return { padded: lines.map(l => leftPad(l, 30, \".\")) }\n}\n\nglobal.main = myAction\n```\n\nThis allows the bundle source to \"break out\" of the closures Webpack uses when defining the modules.\n\n- Create the Webpack configuration file in `webpack.config.js` with the following contents.\n\n```javascript\nmodule.exports = {\n  entry: './index.js',\n  target: 'node',\n  output: {\n    filename: 'bundle.js'\n  }\n};\n```\n\n- Install the Webpack library and CLI using NPM.\n\n```\nnpm install webpack-cli --save-dev\n```\n\n- Run the Webpack tool using the configuration file.\n\n```\nnpx webpack --config webpack.config.js\n```\n\n- Create an action using the bundle source file.\n\n```\nwsk action create my-action dist/bundle.js --kind nodejs:20\n```\n\n- Invoke the action as normal. Results should be the same as the example above.\n\n```\nwsk action invoke my-action --result --param lines \"[\\\"and now\\\", \\\"for something completely\\\", \\\"different\\\" ]\"\n```\n\n### Using parcel ([https://parceljs.org/](https://parceljs.org/))\n\n- Change `index.js` to export the `main` function using as a global reference.\n\n```javascript\nconst leftPad = require('left-pad');\n\nfunction myAction(args) {\n  const lines = args.lines || [];\n  return { padded: lines.map(l => leftPad(l, 30, \".\")) }\n}\n\nglobal.main = myAction\n```\n\nThis allows the bundle source to \"break out\" of the closures Parcel uses when defining the modules.\n\n- Install the Parcel library using NPM.\n\n```\nnpm install parcel-bundler --save-dev\n```\n\n- Run the Parcel tool using the configuration file.\n\n```\n npx parcel index.js\n```\n\n- Create an action using the bundle source file.\n\n```\nwsk action create my-action dist/index.js --kind nodejs:20\n```\n\n- Invoke the action as normal. Results should be the same as the example above.\n\n```\nwsk action invoke my-action --result --param lines \"[\\\"and now\\\", \\\"for something completely\\\", \\\"different\\\" ]\"\n```\n\n\n## Reference\n\nJavaScript actions can be executed in Node.js version 18 or 20.\nCurrently actions are executed by default in a Node.js version 20 environment.\n\n### Node.js version 18 environment\nThe Node.js version 18 environment is used if the `--kind` flag is explicitly specified with a value of 'nodejs:18' when creating or updating an Action.\n\nThe following packages are pre-installed in the Node.js version 18 environment:\n\n- [openwhisk](https://www.npmjs.com/package/openwhisk) - JavaScript client library for the OpenWhisk platform. Provides a wrapper around the OpenWhisk APIs.\n\n### Node.js version 20 environment\nThe Node.js version 20 environment is used if the `--kind` flag is explicitly specified with a value of 'nodejs:20' when creating or updating an Action.\n\nThe following packages are pre-installed in the Node.js version 20 environment:\n\n- [openwhisk](https://www.npmjs.com/package/openwhisk) - JavaScript client library for the OpenWhisk platform. Provides a wrapper around the OpenWhisk APIs.\n"
  },
  {
    "path": "docs/actions-php.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Creating and invoking PHP actions\n\nThe process of creating PHP actions is similar to that of [other actions](actions.md#the-basics).\nThe following sections guide you through creating and invoking a single PHP action,\nand demonstrate how to bundle multiple PHP files and third party dependencies.\n\nPHP actions are executed using PHP 8.0, 7.4 or 7.3. The specific\nversion of PHP is listed in the CHANGELOG files in the [PHP runtime repository](https://github.com/apache/openwhisk-runtime-php).\n\nTo use a PHP runtime, specify the `wsk` CLI parameter `--kind` when creating or\nupdating an action. The available PHP kinds are:\n\n* PHP 8.0: `--kind php:8.0`\n* PHP 7.4: `--kind php:7.4`\n* PHP 7.3: `--kind php:7.3`\n\nAn action is simply a top-level PHP function. For example, create a file called `hello.php`\nwith the following source code:\n\n```php\n<?php\nfunction main(array $args) : array\n{\n    $name = $args[\"name\"] ?? \"stranger\";\n    $greeting = \"Hello $name!\";\n    echo $greeting;\n    return [\"greeting\" => $greeting];\n}\n```\n\nAn action supports not only a JSON object but also a JSON arary as a return value.\n\nIt would be a simple example that uses an array as a return value:\n\n```php\n<?php\nfunction main(array $args) : array\n{\n    $arr=array(\"a\",\"b\",\"c\");\n    return $arr;\n}\n```\n\nYou can also create a sequence action with actions accepting an array param and returning an array result.\n\nYou can easily figure out the parameters with the following example:\n\n```php\n<?php\nfunction main(array $args) : array\n{\n    $result = array_reverse($args);\n    return $result;\n}\n```\n\nPHP actions always consume an associative array and return an associative array.\nThe entry method for the action is `main` by default but may be specified explicitly when creating\nthe action with the `wsk` CLI using `--main`, as with any other action type.\n\nYou can create an OpenWhisk action called `helloPHP` from this function as follows:\n\n```\nwsk action create helloPHP hello.php\n```\n\nThe CLI automatically infers the type of the action from the source file extension.\nFor `.php` source files, the action runs using a PHP 7.4 runtime.\n\nAction invocation is the same for PHP actions as it is for [any other action](actions.md#the-basics).\n\n```\nwsk action invoke --result helloPHP --param name World\n```\n\n```json\n{\n  \"greeting\": \"Hello World!\"\n}\n```\n\nFind out more about parameters in the [Working with parameters](./parameters.md) section.\n\n## Packaging PHP actions in zip files\n\nYou can package a PHP action along with other files and dependent packages in a zip file.\nThe filename of the source file containing the entry point (e.g., `main`) must be `index.php`.\nFor example, to create an action that includes a second file called `helper.php`,\nfirst create an archive containing your source files:\n\n```bash\nzip -r helloPHP.zip index.php helper.php\n```\n\nand then create the action:\n\n```bash\nwsk action create helloPHP --kind php:7.4 helloPHP.zip\n```\n\n## Including Composer dependencies\n\nIf your PHP action requires [Composer](https://getcomposer.org) dependencies,\nyou can install them as usual using `composer require` which will create a `vendor` directory.\nAdd this directory to your action's zip file and create the action:\n\n```bash\nzip -r helloPHP.zip index.php vendor\nwsk action create helloPHP --kind php:7.4 helloPHP.zip\n```\n\nThe PHP runtime will automatically include Composer's autoloader for you, so you can immediately\nuse the dependencies in your action code. Note that if you don't include your own `vendor` folder,\nthen the runtime will include one for you with the following Composer packages:\n\n- guzzlehttp/guzzle\n- ramsey/uuid\n\nThe specific versions of these packages depends on the PHP runtime in use and is listed in the\nCHANGELOG files in the [PHP runtime repository](https://github.com/apache/openwhisk-runtime-php).\n\n\n## Built-in PHP extensions\n\nThe following PHP extensions are available in addition to the standard ones:\n\n- bcmath\n- curl\n- gd\n- intl\n- mbstring\n- mongodb\n- mysqli\n- pdo_mysql\n- pdo_pgsql\n- pdo_sqlite\n- soap\n- zip\n"
  },
  {
    "path": "docs/actions-python.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Creating and invoking Python actions\n\nThe process of creating Python actions is similar to that of [other actions](actions.md#the-basics).\nThe following sections guide you through creating and invoking a single Python action,\nand demonstrate how to bundle multiple Python files and third party dependencies.\n\nAn example action Python action is simply a top-level function.\nFor example, create a file called `hello.py` with the following source code:\n\n```python\ndef main(args):\n    name = args.get(\"name\", \"stranger\")\n    greeting = \"Hello \" + name + \"!\"\n    print(greeting)\n    return {\"greeting\": greeting}\n```\n\nAn action supports not only a JSON object but also a JSON array as a return value.\n\nIt would be a simple example that uses an array as a return value:\n\n```python\ndef main(args):\n    return [\"a\", \"b\"]\n```\n\nYou can also create a sequence action with actions accepting an array param and returning an array result.\n\nYou can easily figure out the parameters with the following example:\n\n```python\ndef main(args):\n    return args\n```\n\nPython actions always consume a dictionary and produce a dictionary.\nThe entry method for the action is `main` by default but may be specified explicitly when creating\nthe action with the `wsk` CLI using `--main`, as with any other action type.\n\nYou can create an OpenWhisk action called `helloPython` from this function as follows:\n\n```\nwsk action create helloPython hello.py\n```\nThe CLI automatically infers the type of the action from the source file extension.\nFor `.py` source files, the action runs using a Python 3.6 runtime.\n\nAction invocation is the same for Python actions as it is for any other actions:\n\n```\nwsk action invoke --result helloPython --param name World\n```\n\n```json\n  {\n      \"greeting\": \"Hello World!\"\n  }\n```\n\nFind out more about parameters in the [Working with parameters](./parameters.md) section.\n\n## Packaging Python actions in zip files\n\nYou can package a Python action and dependent modules in a zip file.\nThe filename of the source file containing the entry point (e.g., `main`) must be `__main__.py`.\nFor example, to create an action with a helper module called `helper.py`, first create an archive containing your source files:\n\n```bash\nzip -r helloPython.zip __main__.py helper.py\n```\n\nand then create the action:\n\n```bash\nwsk action create helloPython --kind python:3 helloPython.zip\n```\n\n## Packaging Python actions with a virtual environment in zip files\n\nAnother way of packaging Python dependencies is using a virtual environment (`virtualenv`). This allows you to link additional packages\nthat may be installed via [`pip`](https://packaging.python.org/installing/) for example.\nTo ensure compatibility with the OpenWhisk container, package installations inside a virtualenv must be done in the target environment.\nSo the docker image `openwhisk/python3action` should be used to create a virtualenv directory for your action.\n\nAs with basic zip file support, the name of the source file containing the main entry point must be `__main__.py`. In addition, the virtualenv directory must be named `virtualenv`.\nBelow is an example scenario for installing dependencies, packaging them in a virtualenv, and creating a compatible OpenWhisk action.\n\n1. Given a `requirements.txt` file that contains the `pip` modules and versions to install, run the following to install the dependencies and create a virtualenv using a compatible Docker image:\n ```bash\n docker run --rm -v \"$PWD:/tmp\" openwhisk/python3action bash \\\n   -c \"cd tmp && virtualenv virtualenv && source virtualenv/bin/activate && pip install -r requirements.txt\"\n ```\n\n2. Archive the virtualenv directory and any additional Python files:\n ```bash\n zip -r helloPython.zip virtualenv __main__.py\n ```\n\n3. Create the action:\n```bash\nwsk action create helloPython --kind python:3 helloPython.zip\n```\n\n## Python 3 actions\n\nPython 3 actions are executed using Python 3.6.1. This is the default runtime for Python actions, unless you specify the `--kind` flag when creating or updating an action.\nThe following packages are available for use by Python actions, in addition to the Python 3.6 standard libraries.\n\n- aiohttp v1.3.3\n- appdirs v1.4.3\n- asn1crypto v0.21.1\n- async-timeout v1.2.0\n- attrs v16.3.0\n- beautifulsoup4 v4.5.1\n- cffi v1.9.1\n- chardet v2.3.0\n- click v6.7\n- cryptography v1.8.1\n- cssselect v1.0.1\n- Flask v0.12\n- gevent v1.2.1\n- greenlet v0.4.12\n- httplib2 v0.9.2\n- idna v2.5\n- itsdangerous v0.24\n- Jinja2 v2.9.5\n- kafka-python v1.3.1\n- lxml v3.6.4\n- MarkupSafe v1.0\n- multidict v2.1.4\n- packaging v16.8\n- parsel v1.1.0\n- pyasn1 v0.2.3\n- pyasn1-modules v0.0.8\n- pycparser v2.17\n- PyDispatcher v2.0.5\n- pyOpenSSL v16.2.0\n- pyparsing v2.2.0\n- python-dateutil v2.5.3\n- queuelib v1.4.2\n- requests v2.11.1\n- Scrapy v1.1.2\n- service-identity v16.0.0\n- simplejson v3.8.2\n- six v1.10.0\n- Twisted v16.4.0\n- w3lib v1.17.0\n- Werkzeug v0.12\n- yarl v0.9.8\n- zope.interface v4.3.3\n"
  },
  {
    "path": "docs/actions-ruby.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Creating and invoking Ruby actions\n\nThe process of creating Ruby actions is similar to that of [other actions](actions.md#the-basics).\nThe following sections guide you through creating and invoking a single Ruby action,\nand demonstrate how to bundle multiple Ruby files and third party dependencies.\n\nRuby actions are executed using Ruby 2.5. To use this runtime, specify the `wsk` CLI parameter\n`--kind ruby:2.5` when creating or updating an action. This is the default when creating an action\nwith file that has a `.rb` extension.\n\nAn action is simply a top-level Ruby method. For example, create a file called `hello.rb`\nwith the following source code:\n\n```ruby\ndef main(args)\n  name = args[\"name\"] || \"stranger\"\n  greeting = \"Hello #{name}!\"\n  puts greeting\n  { \"greeting\" => greeting }\nend\n```\n\nAn action supports not only a JSON object but also a JSON array as a return value.\n\nIt would be a simple example that uses an array as a return value:\n\n```ruby\ndef main(args)\n  nums = Array[\"a\",\"b\"]\n  nums\nend\n```\n\nYou can also create a sequence action with actions accepting an array param and returning an array result.\n\nYou can easily figure out the parameters with the following example:\n\n```ruby\ndef main(args)\n  args\nend\n```\n\nRuby actions always consume a Hash and return a Hash.\nThe entry method for the action is `main` by default but may be specified explicitly\nwhen creating the action with the `wsk` CLI using `--main`, as with any other action type.\n\nYou can create an OpenWhisk action called `hello_ruby` from this function as follows:\n\n```\nwsk action create hello_ruby hello.rb\n```\n\nThe CLI automatically infers the type of the action from the source file extension.\nFor `.rb` source files, the action runs using a Ruby 2.5 runtime.\n\nAction invocation is the same for Ruby actions as it is for [any other action](actions.md#the-basics).\n\n```\nwsk action invoke --result hello_ruby --param name World\n```\n\n```json\n{\n  \"greeting\": \"Hello World!\"\n}\n```\n\nFind out more about parameters in the [Working with parameters](./parameters.md) section.\n\n## Packaging Ruby actions in zip files\n\nYou can package a Ruby action along with other files and dependent packages in a zip file.\nThe filename of the source file containing the entry point (e.g., `main`) must be `main.rb`.\nFor example, to create an action that includes a second file called `helper.rb`,\nfirst create an archive containing your source files:\n\n```bash\nzip -r hello_ruby.zip main.rb helper.rb\n```\n\nand then create the action:\n\n```bash\nwsk action create hello_ruby --kind ruby:2.5 hello_ruby.zip\n```\n\nA few Ruby gems such as `mechanize` and `jwt` are available in addition to the default and bundled gems.\nYou can use arbitrary gems so long as you use zipped actions to package all the dependencies.\n"
  },
  {
    "path": "docs/actions-rust.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Creating and invoking Rust actions\n\nThe process of creating Rust actions is similar to that of [other actions](actions.md#the-basics).\nThe following sections guide you through creating and invoking a single Rust action,\nand demonstrate how to bundle multiple Rust files and third party dependencies.\n\nAn example action Rust action is simply a top-level function.\nFor example, create a file called `hello.rs` with the following source code:\n\n```Rust\nextern crate serde_json;\n\nuse serde_derive::{Deserialize, Serialize};\nuse serde_json::{Error, Value};\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\nstruct Input {\n    #[serde(default = \"stranger\")]\n    name: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\nstruct Output {\n    greeting: String,\n}\n\nfn stranger() -> String {\n    \"stranger\".to_string()\n}\n\npub fn main(args: Value) -> Result<Value, Error> {\n    let input: Input = serde_json::from_value(args)?;\n    let output = Output {\n        greeting: format!(\"Hello, {}\", input.name),\n    };\n    serde_json::to_value(output)\n}\n```\n\nRust actions are mainly composed by a `main` function that accepts a JSON `serdes Value` as input and returns a `Result` including a JSON `serde Value`.\n\nAn action supports not only a JSON object but also a JSON array as a return value.\n\nIt would be a simple example that uses an array as a return value:\n\n```rust\nextern crate serde_json;\nuse serde_derive::{Deserialize, Serialize};\nuse serde_json::{Error, Value};\npub fn main(args: Value) -> Result<Value, Error> {\n    let output = [\"a\", \"b\"];\n    serde_json::to_value(output)\n}\n```\nYou can also create a sequence action with actions accepting an array param and returning an array result.\n\nYou can easily figure out the parameters with the following example:\n\n```rust\nextern crate serde_json;\nuse serde_derive::{Deserialize, Serialize};\nuse serde_json::{Error, Value};\npub fn main(args: Value) -> Result<Value, Error> {\n    let inputParam = args.as_array();\n    let defaultOutput = [\"c\", \"d\"];\n    match inputParam {\n        None => serde_json::to_value(defaultOutput),\n        Some(x) => serde_json::to_value(x),\n    }\n}\n```\n\nThe entry method for the action is `main` by default but may be specified explicitly when creating\nthe action with the `wsk` CLI using `--main`, as with any other action type.\n\nYou can create an OpenWhisk action called `helloRust` from this function as follows:\n\n```\nwsk action create helloRust --kind rust:1.34 hello.rs\n```\nThe CLI automatically infers the type of the action from the source file extension.\nFor `.rs` source files, the action runs using a Rust v1.34 runtime.\n\nAction invocation is the same for Rust actions as it is for any other actions:\n\n```\nwsk action invoke --result helloRust --param name World\n```\n\n```json\n  {\n      \"greeting\": \"Hello World!\"\n  }\n```\n\nFind out more about parameters in the [Working with parameters](./parameters.md) section.\n\n## Packaging Rust actions in zip files\n\nIf your action needs external dependencies, you need to provide a zip file including your source and your cargo file with all your dependencies.\nThe filename of the source file containing the entry point (e.g., `main`) must be `lib.rs`.\nThe folder structure should be as follows:\n```\n|- Cargo.toml\n|- src\n    |- lib.rs\n```\nHere is an example of a Cargo.toml file\n```\n[package]\nname = \"actions\"\nversion = \"0.1.0\"\nauthors = [\"John Doe <john@doe.com>\"]\nedition = \"2018\"\n\n[dependencies]\nserde_json = \"1.0\"\nserde = \"1.0\"\nserde_derive = \"1.0\"\n```\n\nTo zip your folder:\n\n```bash\nzip -r helloRust.zip Cargo.toml src\n```\n\nand then create the action:\n\n```bash\nwsk action create helloRust --kind rust:1.34 helloRust.zip\n```\n"
  },
  {
    "path": "docs/actions-swift.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Creating and invoking Swift actions\n\nThe process of creating Swift actions is similar to that of [other actions](actions.md#the-basics).\nThe following sections guide you through creating and invoking a single Swift action,\nand demonstrate how to bundle multiple Swift files and third party dependencies.\n\n**Tip:** You can use the [Online Swift Playground](http://online.swiftplayground.run) to test your Swift code without having to install Xcode on your machine.\n\n**Note:** Swift actions run in a Linux environment. Swift on Linux is still in development,\nand OpenWhisk usually uses the latest available release, which is not necessarily stable.\nIn addition, the version of Swift that is used with OpenWhisk might be inconsistent with versions\nof Swift from stable releases of Xcode on MacOS.\n\n### Swift 4\nAn action is simply a top-level Swift function. For example, create a file called\n`hello.swift` with the following content:\n\n```swift\nfunc main(args: Any) -> Any {\n    let dict = args as! [String:Any]\n    if let name = dict[\"name\"] as? String {\n        return [ \"greeting\" : \"Hello \\(name)!\" ]\n    } else {\n        return [ \"greeting\" : \"Hello stranger!\" ]\n    }\n}\n```\nIn this example the Swift action consumes a dictionary and produces a dictionary.\n\nAn action supports not only a JSON object but also a JSON array as a return value.\n\nIt would be a simple example that uses an array as a return value:\n\n```swift\nfunc main(args: Any) -> Any {\n    var arr = [\"a\", \"b\"]\n    return arr\n}\n```\n\nYou can also create a sequence action with actions accepting an array param and returning an array result.\n\nYou can easily figure out the parameters with the following example:\n\n```swift\n func main(args: Any) -> Any {\n     return args\n }\n```\n\nYou can create an OpenWhisk action called `helloSwift` from this function as\nfollows:\n\n```\nwsk action create helloSwift hello.swift\n```\n\n### Swift 4 Codable type\n\nNew in Swift 4 in addition of the above main function signature there are two more signatures out of the box taking advantage of the [Codable](https://developer.apple.com/documentation/swift/codable) type. You can learn more about data types encodable and decodable for compatibility with external representations such as JSON [here](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types).\n\nThe following takes as input parameter a Codable Input with field `name`, and returns a Codable output with a field `greetings`\n```swift\nstruct Input: Codable {\n    let name: String?\n}\nstruct Output: Codable {\n    let greeting: String\n}\nfunc main(param: Input, completion: (Output?, Error?) -> Void) -> Void {\n    let result = Output(greeting: \"Hello \\(param.name ?? \"stranger\")!\")\n    print(\"Log greeting:\\(result.greeting)\")\n    completion(result, nil)\n}\n```\nIn this example the Swift action consumes a Codable and produces a Codable type.\nIf you don't need to handle any input you can use the function signature that doesn't take any input, only Codable output.\n```swift\nstruct Output: Codable {\n    let greeting: String\n}\nfunc main(completion: (Output?, Error?) -> Void) -> Void {\n    let result = Output(greeting: \"Hello OpenWhisk!\")\n    completion(result, nil)\n}\n```\n\n\nYou can create a OpenWhisk action called `helloSwift` from this function as\nfollows:\n\n```\nwsk action create helloSwift hello.swift\n```\n\n\nSee the Swift [reference](#reference.md) for more information about the Swift runtime.\n\nAction invocation is the same for Swift actions as it is for JavaScript actions:\n\n```\nwsk action invoke --result helloSwift --param name World\n```\n\n```json\n{\n  \"greeting\": \"Hello World!\"\n}\n```\n\nFind out more about parameters in the [Working with parameters](./parameters.md) section.\n\n## Packaging an action as a Swift executable\n\nWhen you create an OpenWhisk Swift action with a Swift source file, it has to be compiled into a binary before the action is run. Once done, subsequent calls to the action are much faster until the container holding your action is purged. This delay is known as the cold-start delay.\n\nTo avoid the cold-start delay, you can compile your Swift file into a binary and then upload to OpenWhisk in a zip file. As you need the OpenWhisk scaffolding, the easiest way to create the binary is to build it within the same environment as it will be run in.\n\n## Compiling Swift 4.2 packaged actions\n\nThe docker runtime includes a compiler to help users compile and package Swift 4.2 actions.\n\n### Compiling a single source file for Swift 4.2\n\nTo compile a single source file that doesn't depend on external libraries you can use the following command:\n```bash\ndocker run -i openwhisk/action-swift-v4.2 -compile main <hello.swift >hello.zip\n```\nThe docker container reads from stdin the content of the file, and writes to stdout a zip archive with the compiled swift executable.\nUse the flag `-compile` with the name of the main method.\nThe zip archive is ready for deployment and invocation using the kind `swift:4.2`\n```bash\nwsk action update helloSwiftly hello.zip --kind swift:4.2\nwsk action invoke helloSwiftly -r -p name World\n```\n\n### Compiling dependencies and multi-file projects for Swift 4.2\n\nTo compile multiple files and include external dependencies create the following directory structure.\n```\n.\n├── Package.swift\n└── Sources\n    └── main.swift\n```\nThe directory `Sources/` should contain a file named `main.swift`.\nThe `Package.swift` should start with a comment specifying version `4.2` for the Swift tooling:\n```swift\n// swift-tools-version:4.2\nimport PackageDescription\n\nlet package = Package(\n    name: \"Action\",\n    products: [\n    .executable(\n        name: \"Action\",\n        targets:  [\"Action\"]\n    )\n    ],\n    dependencies: [\n    .package(url: \"https://github.com/IBM-Swift/SwiftyRequest.git\", .upToNextMajor(from: \"1.0.0\"))\n    ],\n    targets: [\n    .target(\n        name: \"Action\",\n        dependencies: [\"SwiftyRequest\"],\n        path: \".\"\n    )\n    ]\n)\n```\n\nCreate a zip archive with the content of the directory:\n```bash\nzip ../action-src.zip -r *\n```\nPass the zip archive to the docker container over stdin, and the stdout will be a new zip archive with the compiled executable.\nThe docker container reads from stdin the content of the zip archive, and writes to stdout a  new zip archive with the compiled swift executable.\n```\ndocker run -i openwhisk/action-swift-v4.2 -compile main <action-src.zip >../action-bin.zip\n```\nIn a Linux based system you can combined the `zip` and `docker run` steps in a single command:\n```\nzip - -r * | docker run -i openwhisk/action-swift-v4.2 -compile main >../action-bin.zip\n```\n\nThe zip `action-bin.zip` archive is ready for deployment and invocation using the kind `swift:4.2`\n```bash\nwsk action update helloSwiftly action-bin.zip --kind swift:4.2\nwsk action invoke helloSwiftly -r\n```\n\n## Error Handling in Swift 4\n\nWith the new Codable completion handler, you can pass an Error to indicate a failure in your Action.\n[Error handling in Swift](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html) resembles exception handling in other languages, with the use of the `try, catch` and `throw` keywords.\nThe following example shows a an example on handling an error\n```swift\nenum VendingMachineError: Error {\n    case invalidSelection\n    case insufficientFunds(coinsNeeded: Int)\n    case outOfStock\n}\nfunc main(param: Input, completion: (Output?, Error?) -> Void) -> Void {\n    // Return real error\n    do {\n        throw VendingMachineError.insufficientFunds(coinsNeeded: 5)\n    } catch {\n        completion(nil, error)\n    }\n}\n```\n\n## Reference\n\n### Swift 4\nSwift 4 actions are executed using Swift 4.2 using `--kind swift:4.2` respectively.\nThe default `--kind swift:default` is Swift 4.2.\n\nSwift 4.x action runtimes don't embed any packages, follow the instructions for [packaged swift actions](./actions.md#packaging-an-action-as-a-swift-executable) to include dependencies using a Package.swift.\n"
  },
  {
    "path": "docs/actions-update.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n## Updating Action Language Runtimes\n\nOpenWhisk supports [several languages and runtimes](actions.md#languages-and-runtimes) that can be made\navailable for usage in an OpenWhisk deployment. This is done via the [runtimes manifest](actions-new.md#the-runtimes-manifest).\n\nOver time, you may need to do one or more of the following:\n\n* Update runtimes to address security issues - for example, the latest code level of Node.js 10.\n* Remove runtime versions that are no longer supported - for example, Node.js 6.\n* Add more languages due to user demand.\n* Remove languages that are no longer needed.\n\nWhile adding and updating languages and runtimes is pretty straightforward, removing or renaming languages and runtimes\nrequires some caution to prevent problems with preexisting actions.\n\n### Updating runtimes\n\nFollow these steps to update a particular runtime kind:\n\n1. Update the runtime's container image.\n2. Update the corresponding `image` property in the [runtimes manifest](actions-new.md#the-runtimes-manifest) to use the new container image.\n3. Restart / re-deploy controllers and invokers such that they pick up the changed runtimes manifest.\n\nAlready existing actions of the changed runtime kind will immediately use the new container image when the invoker creates a new action container.\n\nObviously, this approach should only be used if existing actions do not break with the new container image. If a new container image may break existing actions, consider introducing a new runtime kind instead.\n\n### Removing runtimes\n\nFollow these steps to remove a particular runtime kind under the assumption that actions with the runtime kind exist in the system. Clearly, the steps below should be spaced out in time to give action owners time to react.\n\n1. Deprecate the runtime kind by setting `\"deprecated\": true` in the [runtimes manifest](actions-new.md#the-runtimes-manifest). This setting prevents new actions from being created with the deprecated action kind. In addition, existing actions cannot be changed to the deprecated action kind any more.\n2. Ask owners of existing actions affected by the runtime kind to remove or update their actions to a different action kind.\n3. Create an automated process that identifies all actions with the runtime kind to be removed in the system's action artifact store. Either automatically remove these actions or change to a different runtime kind.\n4. Once the system's action artifact store does not contain actions with the runtime kind to be removed, remove the runtime kind from the [runtimes manifest](actions-new.md#the-runtimes-manifest).\n5. Remove the runtime kind from the list of known kinds in the `ActionExec` object of the [controller API's Swagger definition](../core/controller/src/main/resources/apiv1swagger.json).\n\nIf you remove a runtime kind from the [runtimes manifest](actions-new.md#the-runtimes-manifest), all actions that still use the removed runtime kind can no longer be read, updated, deleted or invoked.\n\nIf the system's action artifact store does not contain any action with the runtime kind to be removed, there is no need for the deprecation and migration steps.\n\n### Renaming runtimes\n\nRenaming a runtime kind actually means removing the runtime kind and adding a different runtime kind. Follow the steps for [Removing runtimes](removing-runtimes) and [Updating runtimes](updating-runtimes).\n\n### Updating languages\n\n* The process of adding a new language is described in [Adding Action Language Runtimes](actions-new.md).\n* Adding new runtime kinds to a language family needs no special considerations - just add it to the [runtimes manifest](actions-new.md#the-runtimes-manifest).\n\n### Removing languages\n\nYou need to follow the steps described in [Removing runtimes](removing-runtimes) for all runtime kinds in the language family.\n\n### Renaming languages\n\nRenaming a language family actually means removing the language family and adding a different family. Follow the steps for [Removing languages](removing-languages) and [Updating languages](updating-languages).\n"
  },
  {
    "path": "docs/actions.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# OpenWhisk Actions\n\nActions are stateless functions that run on the OpenWhisk platform. For example, an action can\nbe used to detect the faces in an image, respond to a database change, respond to an API call,\nor post a Tweet. In general, an action is invoked in response to an event and produces some\nobservable output.\n\nAn action may be created from a function programmed using a number of [supported languages and runtimes](#languages-and-runtimes),\nor from a binary-compatible executable, or even executables packaged as Docker containers.\n\n* The OpenWhisk CLI [`wsk`](https://github.com/apache/openwhisk-cli/releases)\nmakes it easy to create and invoke actions. Instructions for configuring the CLI are available [here](cli.md).\n* You can also use the [REST API](rest_api.md).\n\nWhile the actual function code will be specific to a [language and runtime](#languages-and-runtimes),\nthe OpenWhisk operations to create, invoke and manage an action are the same regardless of the\nimplementation choice. We recommend that you review [the basics](#the-basics) before moving on to\nadvanced topics.\n\n* [The basics of working with actions](#the-basics) (_start here_)\n* Common `wsk` CLI operations and tips\n  * [Watching action output](#watching-action-output)\n  * [Getting actions](#getting-actions)\n  * [Listing actions](#listing-actions)\n  * [Deleting actions](#deleting-actions)\n* [Accessing action metadata within the action body](#accessing-action-metadata-within-the-action-body)\n* [Securing your action](security.md)\n* [Concurrency in actions](intra-concurrency.md)\n\n## Languages and Runtimes\n\nLonger tutorials that are specific to a language of your choice are listed below.\nWe recommend reading the basics in this document first, which are language agnostic, before getting deeper\ninto a language-specific tutorial. If your preferred language isn't supported directly, you may find\nthe [Docker](actions-docker.md) action or [native binary](actions-docker.md#creating-native-actions)\npaths more suitable. Or, you can [create a new runtime](actions-new.md).\n\n* [Go](actions-go.md)\n* [Java](actions-java.md)\n* [JavaScript](actions-nodejs.md)\n* [PHP](actions-php.md)\n* [Python](actions-python.md)\n* [Ruby](actions-ruby.md)\n* [Rust](actions-rust.md)\n* [Swift](actions-swift.md)\n* [.NET Core](actions-dotnet.md)\n* [Docker and native binaries](actions-docker.md)\n\nMultiple actions from different languages may be composed together to create a longer processing\npipeline called a [sequence](#creating-action-sequences). The polyglot nature of the composition is\npowerful in that it affords you the ability to use the right language for the problem you're solving,\nand separates the orchestration of the dataflow between functions from the choice of language.\nA more advanced form of composition is described [here](conductors.md).\n\nIf your runtime is not listed there, you can create a new one for your specific language.\n\nYou can create a new runtime in two ways:\n\n- Implementing the [runtime specification](actions-new.md)\n- Using the [ActionLoop engine](actions-actionloop.md) that provides a simplified path for building a new runtime.\n\nFollow the instructions in [Updating Action Language Runtimes](actions-update.md) for updating, removing or renaming\nruntime kinds or language families.\n\n### How prewarm containers are provisioned without a reactive configuration\n\nPrewarmed containers are created when an invoker starts, they are created according to runtimes.json's stemCells, e.g.\n```\n{\n    \"kind\": \"nodejs:20\",\n    \"default\": true,\n    \"image\": {\n        \"prefix\": \"openwhisk\",\n        \"name\": \"action-nodejs-v20\",\n        \"tag\": \"nightly\"\n    },\n    \"deprecated\": false,\n    \"attached\": {\n        \"attachmentName\": \"codefile\",\n        \"attachmentType\": \"text/plain\"\n     },\n     \"stemCells\": [\n     {\n        \"initialCount\": 2,\n        \"memory\": \"256 MB\"\n     }\n     ]\n}\n```\nIn the above example, there is only one runtime configuration, which is `nodejs:20`.\nIt has a stem cell configuration and 2 containers with 256MB memory for `nodejs:20` will be provisioned when an invoker starts.\nWhen an activation with the `nodejs:20` kind arrives, one of the prewarm containers can be used to alleviate a cold start.\nA prewarm container that is assigned to an action is moved to the busy pool and the invoker creates one more prewarm container to replenish the prewarm pool.\nIn this way, when no reactive configuration is configured, an invoker always maintains the same number of prewarm containers.\n\n### How prewarmed containers are provisioned with a reactive configuration\n\nWith a reactive configuration, the number of prewarm containers is dynamically controlled, e.g.\n```\n{\n    \"kind\": \"nodejs:20\",\n    \"default\": true,\n    \"image\": {\n        \"prefix\": \"openwhisk\",\n        \"name\": \"action-nodejs-v20\",\n        \"tag\": \"nightly\"\n    },\n    \"deprecated\": false,\n    \"attached\": {\n        \"attachmentName\": \"codefile\",\n        \"attachmentType\": \"text/plain\"\n     },\n     \"stemCells\": [\n     {\n        \"initialCount\": 2,\n         \"memory\": \"256 MB\",\n         \"reactive\": {\n             \"minCount\": 1,\n             \"maxCount\": 4,\n             \"ttl\": \"2 minutes\",\n             \"threshold\": 2,\n             \"increment\": 1\n     }\n     ]\n}\n```\nIn the above example, there is a reactive configuration for `nodejs:20` and there are 4 underlying configurations.\n* `minCount`: the minimum number of prewarm containers. The number of prewarm containers can't be fewer than this value\n* `maxCount`: the maximum number of prewarm containers. The number of prewarm containers cannot exceed this value\n* `ttl`: the amount of time that prewarm containers can exist without any activation. If no activation for the prewarm container arrives in the given time, the prewarm container will be removed\n* `threshold` and `increment`: these two configurations control the number of new prewarm containers to be created.\n\nThe number of prewarmed containers is dynamically controlled when:\n* they are expired due to a TTL, some prewarmed containers are removed to save resources.\n* cold starts happen, some prewarm containers are created according to the following calculus.\n  - `# of prewarm containers to be created` = `# of cold starts` / `threshold` * `increment`\n  - ex1) `cold start number(2)` / `threshold(2)` * `increment(1)` = 1\n  - ex2) `cold start number(4)` / `threshold(2)` * `increment(1)` = 2\n  - ex3) `cold start number(8)` / `threshold(2)` * `increment(1)` = 4\n  - ex4) `cold start number(16)` / `threshold(2)` * `increment(1)` = 4 (cannot exceed the maximum number)\n* no activation arrives for long time, the number of prewarm containers will eventually converge to `minCount`.\n\n## The basics\n\nTo use a function as an action, it must conform to the following:\n- The function accepts a dictionary as input and produces a dictionary as output. The input and output dictionaries are\nkey-value pairs, where the key is a string and the value is any valid JSON value. The dictionaries are\ncanonically represented as JSON objects when interfacing to an action via the REST API or the `wsk` CLI.\n- The function must be called `main` or otherwise must be explicitly exported to identify it as the entry point.\nThe mechanics may vary depending on your choice of language, but in general the entry point can be specified using the\n`--main` flag when using the `wsk` CLI.\n\nIn this section, you'll invoke a built-in action using the `wsk` CLI, which you should\n[download and configure](cli.md) first if necessary.\n\n### Invoking a built-in action\n\nActions are identified by [fully qualified names](reference.md#fully-qualified-names) which generally have\nthree parts separated by a forward slash:\n1. a namespace\n2. a package name\n3. the action name\n\nAs an example, we will work with a built-in sample action called `/whisk.system/samples/greeting`.\nThe namespace for this action is `whisk.system`, the package name\nis `samples`, and the action name is `greeting`. There are other sample actions and\nutility actions, and later you'll learn how to explore the platform to discover more actions.\nYou can learn more about [packages](packages.md) after completing the basic tutorial.\n\nLet's take a look at the action body by saving the function locally:\n```\nwsk action get /whisk.system/samples/greeting --save\nok: saved action code to /path/to/openwhisk/greeting.js\n```\n\nThis is a JavaScript function, which is indicated by the `.js` extension.\nIt will run using a [Node.js](http://nodejs.org/) runtime.\nSee [supported languages and runtimes](#languages-and-runtimes) for other languages and runtimes.\n\nThe contents of the file `greeting.js` should match the function below. It is a short function which\naccepts optional parameters and returns a standard greeting.\n\n```js\n/**\n * @params is a JSON object with optional fields \"name\" and \"place\".\n * @return a JSON object containing the message in a field called \"msg\".\n */\nfunction main(params) {\n  // log the parameters to stdout\n  console.log('params:', params);\n\n  // if a value for name is provided, use it else use a default\n  var name = params.name || 'stranger';\n\n  // if a value for place is provided, use it else use a default\n  var place = params.place || 'somewhere';\n\n  // construct the message using the values for name and place\n  return {msg:  'Hello, ' + name + ' from ' + place + '!'};\n}\n```\n\nThe command to invoke an action and get its result is `wsk action invoke <name> --result` as in:\n```\nwsk action invoke /whisk.system/samples/greeting --result\n```\n\nThis command will print the following result to the terminal:\n```json\n{\n  \"msg\": \"Hello, stranger from somewhere!\"\n}\n```\n\n### Passing parameters to actions\n\nActions may receive parameters as input, and the `wsk` CLI makes it convenient to pass parameters to the actions\nfrom the command line. Briefly, this is done with the flag `--param key value` where `key` is the property name and `value` is\nany valid JSON value. There is a longer [tutorial on working with parameters](parameters.md) that you should read after completing\nthis basic walk-through.\n\nThe `/whisk.system/samples/greeting` action accepts two optional input arguments, which are used to tailor the\nresponse. The default greeting as described earlier is \"Hello, stranger from somewhere!\". The words \"stranger\" and\n\"somewhere\" may be replaced by specifying the following parameters respectively:\n- `name` whose value will replace the word \"stranger\",\n- `place` whose value will replace the word \"somewhere\".\n\n```\nwsk action invoke /whisk.system/samples/greeting --result --param name Dorothy --param place Kansas\n{\n  \"msg\": \"Hello, Dorothy from Kansas!\"\n}\n```\n\n### Request-Response vs Fire-and-Forget\n\nThe style of invocation shown above is synchronous in that the request from the CLI _blocks_ until the\nactivation completes and the result is available from the OpenWhisk platform. This is generally useful\nfor rapid iteration and development.\n\nYou can invoke an action asynchronously as well, by dropping the `--result` command line option. In this case\nthe action is invoked, and the OpenWhisk platform returns an activation ID which you can use later to retrieve\nthe activation record.\n\n ```\nwsk action invoke /whisk.system/samples/greeting\nok: invoked /whisk.system/samples/greeting with id 5a64676ec8aa46b5a4676ec8aaf6b5d2\n ```\n\nTo retrieve the activation record, you use the `wsk activation get <id>` command, as in:\n```\nwsk activation get 5a64676ec8aa46b5a4676ec8aaf6b5d2\nok: got activation 5a64676ec8aa46b5a4676ec8aaf6b5d2\n{\n  \"activationId\": \"5a64676ec8aa46b5a4676ec8aaf6b5d2\",\n  \"duration\": 3,\n  \"response\": {\n    \"result\": {\n      \"msg\": \"Hello, stranger from somewhere!\"\n    },\n    \"status\": \"success\",\n    \"success\": true\n  }, ...\n}\n```\n\nSometimes it is helpful to invoke an action in a blocking style and receiving the activation record entirely\ninstead of just the result. This is achieved using the `--blocking` command line parameter.\n\n```\nwsk action invoke /whisk.system/samples/greeting --blocking\nok: invoked /whisk.system/samples/greeting with id 5975c24de0114ef2b5c24de0118ef27e\n{\n  \"activationId\": \"5975c24de0114ef2b5c24de0118ef27e\",\n  \"duration\": 3,\n  \"response\": {\n    \"result\": {\n      \"msg\": \"Hello, stranger from somewhere!\"\n    },\n    \"status\": \"success\",\n    \"success\": true\n  }, ...\n}\n```\n\n### Blocking invocations and timeouts\n\nA blocking invocation request will _wait_ for the activation result to be available. The wait period\nis the lesser of 60 seconds (this is the default for blocking invocations) or the action's configured\n[time limit](reference.md#per-action-timeout-ms-default-60s).\n\nThe result of the activation is returned if it is available within the blocking wait period.\nOtherwise, the activation continues processing in the system and an activation ID is returned\nso that one may check for the result later, as with non-blocking requests\n(see [here](#watching-action-output) for tips on monitoring activations).\nWhen an action exceeds its configured time limit, the activation record will indicate this error.\nSee [understanding the activation record](#understanding-the-activation-record) for more details.\n\n### Working with activations\n\nSome common CLI commands for working with activations are:\n- `wsk activation list`: lists all activations\n- `wsk activation get --last`: retrieves the most recent activation record\n- `wsk activation result <activationId>`: retrieves only the result of the activation (or use `--last` to get the most recent result).\n- `wsk activation logs <activationId>`: retrieves only the logs of the activation.\n- `wsk activation logs <activationId> --strip`: strips metadata from each log line so the logs are easier to read.\n\n#### The `wsk activation list` command\n\nThe `activation list` command lists all activations, or activations filtered by namespace or name. The result set can be limited by using several flags:\n\n```\nFlags:\n  -f, --full          include full activation description\n  -l, --limit LIMIT   only return LIMIT number of activations from the collection with a maximum LIMIT of 200 activations (default 30)\n      --since SINCE   return activations with timestamps later than SINCE; measured in milliseconds since Th, 01, Jan 1970\n  -s, --skip SKIP     exclude the first SKIP number of activations from the result\n      --upto UPTO     return activations with timestamps earlier than UPTO; measured in milliseconds since Th, 01, Jan 1970\n```\n\nFor example, to list the last 6 activations:\n```\nwsk activation list --limit 6\n```\n<pre>\nDatetime            Activation ID                    Kind      Start Duration   Status  Entity\n2019-03-16 20:03:00 8690bc9904794c9390bc9904794c930e nodejs:6  warm  2ms        success guest/tripleAndIncrement:0.0.1\n2019-03-16 20:02:59 7e76452bec32401db6452bec32001d68 nodejs:6  cold  32ms       success guest/increment:0.0.1\n2019-03-16 20:02:59 097250ad10a24e1eb250ad10a23e1e96 nodejs:6  warm  2ms        success guest/tripleAndIncrement:0.0.1\n2019-03-16 20:02:58 4991a50ed9ed4dc091a50ed9edddc0bb nodejs:6  cold  33ms       success guest/triple:0.0.1\n2019-03-16 20:02:57 aee63124f3504aefa63124f3506aef8b nodejs:6  cold  34ms       success guest/tripleAndIncrement:0.0.1\n2019-03-16 20:02:57 22da217c8e3a4b799a217c8e3a0b79c4 sequence  warm  3.46s      success guest/tripleAndIncrement:0.0.1\n</pre>\n\nThe meaning of the different columns in the list are:\n\n| Column          | Description                                                                                                                                          |\n|:----------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------|\n| `Datetime`      | The date and time when the invocation occurred.                                                                                                      |\n| `Activation ID` | An activation ID that can be used to retrieve the result using the `wsk activation get`, `wsk activation result` and `wsk activation logs` commands. |\n| `Kind`          | The runtime or action type                                                                                                                           |\n| `Start`         | An indication of the latency, i.e. if the runtime container was cold or warm started.                                                                |\n| `Duration`      | Time taken to execute the invocation.                                                                                                                |\n| `Status`        | The outcome of the invocation. For an explanation of the various statuses, see the description of the `statusCode` below.                            |\n| `Entity`        | The fully qualified name of entity that was invoked.                                                                                                 |\n\n#### Understanding the activation record\n\nEach action invocation results in an activation record which contains the following fields:\n\n- `activationId`: The activation ID.\n- `namespace` and `name`: The namespace and name of the entity.\n- `start` and `end`: Timestamps recording the start and end of the activation. The values are in [UNIX time format](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_15).\n- `logs`: An array of strings with the logs that are produced by the action during its activation. Each array element corresponds to a line output to `stdout` or `stderr` by the action, and includes the time and stream of the log output. The structure is as follows: `TIMESTAMP` `STREAM:` `LOG LINE`.\n- `annotations`: An array of key-value pairs that record [metadata](annotations.md#annotations-specific-to-activations) about the action activation.\n- `response`: A dictionary that defines the following keys\n  - `status`: The activation result, which might be one of the following values:\n    - *\"success\"*: the action invocation completed successfully.\n    - *\"application error\"*: the action was invoked, but returned an error value on purpose, for instance because a precondition on the arguments was not met.\n    - *\"action developer error\"*: the action was invoked, but it completed abnormally, for instance the action did not detect an exception, or a syntax error existed. This status code is also returned under specific conditions such as:\n      - the action failed to initialize for any reason\n      - the action exceeded its time limit during the init or run phase\n      - the action specified a wrong docker container name\n      - the action did not properly implement the expected [runtime protocol](actions-new.md)\n    - *\"whisk internal error\"*: the system was unable to invoke the action.\n  - `statusCode`: A value between 0 and 3 that maps to the activation result, as described by the *status* field:\n\n    | statusCode | status                 |\n    |:---------- |:---------------------- |\n    | 0          | success                |\n    | 1          | application error      |\n    | 2          | action developer error |\n    | 3          | whisk internal error   |\n  - `success`: Is *true* if and only if the status is *\"success\"*.\n  - `result`: A dictionary as a JSON object which contains the activation result. If the activation was successful, this contains the value that is returned by the action. If the activation was unsuccessful, `result` contains the `error` key, generally with an explanation of the failure.\n\n### Creating and updating your own action\n\nEarlier we saved the code from the `greeting` action locally. We can use it to create our own version of the action\nin our own namespace.\n```\nwsk action create greeting greeting.js\nok: created action greeting\n```\n\nFor convenience, you can omit the namespace when working with actions that belong to you. Also if there\nis no package, then you simply use the action name without a [package](packages.md) name.\nIf you modify the code and want to update the action, you can use `wsk action update` instead of\n`wsk action create`. The two commands are otherwise the same in terms of their command like parameters.\n\n```\nwsk action update greeting greeting.js\nok: updated action greeting\n```\n\n### Binding parameters to actions\n\nSometimes it is necessary or just convenient to provide values for function parameters. These can serve as\ndefaults, or as a way of reusing an action but with different parameters. Parameters can be bound to an action\nand unless overridden later by an invocation, they will provide the specified value to the function.\n\nHere is an example.\n\n```\nwsk action invoke greeting --result\n{\n  \"msg\": \"Hello, stranger from somewhere!\"\n}\n```\n```\nwsk action update greeting --param name Toto\nok: updated action greeting\n```\n```\nwsk action invoke greeting --result\n{\n  \"msg\": \"Hello, Toto from somewhere!\"\n}\n```\n\nYou may still provide additional parameters, as in the `place`:\n```\nwsk action invoke greeting --result --param place Kansas\n{\n  \"msg\": \"Hello, Toto from Kansas!\"\n}\n```\nand even override the `name`:\n```\nwsk action invoke greeting --result --param place Kansas --param name Dorothy\n{\n  \"msg\": \"Hello, Dorothy from Kansas!\"\n}\n```\n\n### Action execution\n\nWhen an invocation request is received, the system records the request and dispatches an activation.\n\nThe system returns an activation ID (in the case of a non-blocking invocation) to confirm that the invocation was received.\nNotice that if there's a network failure or other failure which intervenes before you receive an HTTP response, it is possible\nthat OpenWhisk received and processed the request.\n\nThe system attempts to invoke the action once and records the `status` in the [activation record](#understanding-the-activation-record).\nEvery invocation that is successfully received, and that the user might be billed for, will eventually have an activation record.\n\nNote that in the case of [*action developer error*](#understanding-the-activation-record), the action may\nhave partially run and generated externally visible side effects. It is the user's responsibility to check\nwhether such side effects actually happened, and issue retry logic if desired.\nAlso note that certain [*whisk internal errors*](#understanding-the-activation-record) will indicate that\nan action started running but the system failed before the action registered completion.\n\n### Further considerations\n- Functions should be stateless, or *idempotent*. While the system does not enforce this property,\nthere is no guarantee that any state maintained by an action will be available across invocations. In some cases,\ndeliberately leaking state across invocations may be advantageous for performance, but also exposes some risks.\n- An action executes in a sandboxed environment, namely a container. At any given time, a single activation will\nexecute inside the container. Subsequent invocations of the same action may reuse a previous container,\nand there may exist more than one container at any given time, each having its own state.\n- Invocations of an action are not ordered. If the user invokes an action twice from the command line or the REST API,\nthe second invocation might run before the first. If the actions have side effects, they might be observed in any order.\n- There is no guarantee that actions will execute atomically. Two actions can run concurrently and their side effects\ncan be interleaved. OpenWhisk does not ensure any particular concurrent consistency model for side effects.\nAny concurrency side effects will be implementation-dependent.\n- Actions have two phases: an initialization phase, and a run phase. During initialization, the function is loaded\nand prepared for execution. The run phase receives the action parameters provided at invocation time. Initialization\nis skipped if an action is dispatched to a previously initialized container --- this is referred to as a _warm start_.\nYou can tell if an [invocation was a warm activation or a cold one requiring initialization](annotations.md#annotations-specific-to-activations)\nby inspecting the activation record.\n- An action runs for a bounded amount of time. This limit can be configured per action, and applies to both the\ninitialization and the execution separately. If the action time limit is exceeded during the initialization or run phase, the activation's response status is _action developer error_.\n- Functions should follow best practices to reduce [vulnerabilities](security.md) by treating input as untrusted,\nand be aware of vulnerabilities they may inherit from third-party dependencies.\n\n## Creating action sequences\n\nA powerful feature of the OpenWhisk programming model is the ability to compose actions together. A common\ncomposition is a sequence of actions, where the result of one action becomes the input to the next action in\nthe sequence.\n\nHere we will use several utility actions that are provided in the `/whisk.system/utils`\n[package](packages.md) to create your first sequence.\n\n1. Display the actions in the `/whisk.system/utils` package.\n\n  ```\n  wsk package get --summary /whisk.system/utils\n  ```\n  ```\n  package /whisk.system/utils: Building blocks that format and assemble data\n     (parameters: none defined)\n   action /whisk.system/utils/split: Split a string into an array\n     (parameters: payload, separator)\n   action /whisk.system/utils/sort: Sorts an array\n     (parameters: lines)\n   ...\n  ```\n\n  You will be using the `split` and `sort` actions in this example shown here, although the package contains more actions.\n\n2. Create an action sequence so that the result of one action is passed as an argument to the next action.\n\n  ```\n  wsk action create mySequence --sequence /whisk.system/utils/split,/whisk.system/utils/sort\n  ```\n\n  This action sequence converts some lines of text to an array, and sorts the lines.\n\n3. Invoke the action:\n\n  ```\n  wsk action invoke --result mySequence --param payload \"Over-ripe sushi,\\nThe Master\\nIs full of regret.\"\n  ```\n  ```json\n  {\n      \"length\": 3,\n      \"lines\": [\n          \"Is full of regret.\",\n          \"Over-ripe sushi,\",\n          \"The Master\"\n      ]\n  }\n  ```\n\n  In the result, you see that the lines are sorted.\n\n**Note**: Parameters passed between actions in the sequence are explicit, except for default parameters.\nTherefore parameters that are passed to the sequence action (e.g., `mySequence`) are only available to the first action in the sequence.\nThe result of the first action in the sequence becomes the input JSON object to the second action in the sequence (and so on).\nThis object does not include any of the parameters originally passed to the sequence unless the first action explicitly includes them in its result.\nInput parameters to an action are merged with the action's default parameters, with the former taking precedence and overriding any matching default parameters.\nFor more information about invoking action sequences with multiple named parameters, learn about [setting default parameters](parameters.md#setting-default-parameters).\n\nA more advanced form of composition using *conductor* actions is described [here](conductors.md).\n\n## Watching action output\n\nOpenWhisk actions might be invoked by other users, in response to various events, or as part of an action sequence. In such cases it can be useful to monitor the invocations.\n\nYou can use the OpenWhisk CLI to watch the output of actions as they are invoked.\n\n1. Issue the following command from a shell:\n```\nwsk activation poll\n```\n\nThis command starts a polling loop that continuously checks for logs from activations.\n\n2. Switch to another window and invoke an action:\n\n```\nwsk action invoke /whisk.system/samples/helloWorld --param payload Bob\nok: invoked /whisk.system/samples/helloWorld with id 7331f9b9e2044d85afd219b12c0f1491\n```\n\n3. Observe the activation log in the polling window:\n\n```\nActivation: helloWorld (7331f9b9e2044d85afd219b12c0f1491)\n  2016-02-11T16:46:56.842065025Z stdout: hello bob!\n```\n\nSimilarly, whenever you run the poll utility, you see in real time the logs for any actions running on your behalf in OpenWhisk.\n\n## Getting actions\n\nMetadata that describes existing actions can be retrieved via the `wsk action get` command.\n\n```\nwsk action get hello\nok: got action hello\n{\n    \"namespace\": \"guest\",\n    \"name\": \"hello\",\n    \"version\": \"0.0.1\",\n    \"exec\": {\n        \"kind\": \"nodejs:6\",\n        \"binary\": false\n    },\n    \"annotations\": [\n        {\n            \"key\": \"exec\",\n            \"value\": \"nodejs:6\"\n        }\n    ],\n    \"limits\": {\n        \"timeout\": 60000,\n        \"memory\": 256,\n        \"logs\": 10\n    },\n    \"publish\": false\n}\n```\n\n### Getting the URL for an action\n\nAn action can be invoked through the REST interface via an HTTPS request.\nTo get an action URL, execute the following command:\n\n```\nwsk action get greeting --url\n```\n\nA URL with the following format will be returned for standard actions:\n```\nok: got action actionName\nhttps://${APIHOST}/api/v1/namespaces/${NAMESPACE}/actions/greeting\n```\n\nAuthentication is required when invoking an action via an HTTPS request using this resource path.\nFor more information regarding action invocations using the REST interface, see [Using REST APIs with OpenWhisk](rest_api.md#actions).\n\nAnother way of invoking an action which does not require authentication is via\n[web actions](webactions.md#web-actions).\n\nAny action may be exposed as a web action, using the `--web true` command line option at action\ncreation time (or later when updating the action).\n\n```\nwsk action update greeting --web true\nok: updated action greeting\n```\n\nThe resource URL for a web action is different:\n```\nwsk action get greeting --url\nok: got action greeting\nhttps://${APIHOST}/api/v1/web/${NAMESPACE}/${PACKAGE}/greeting\n```\n\nYou can use `curl` or wget to invoke the action.\n```\ncurl `wsk action get greeting --url | tail -1`.json\n{\n  \"payload\": \"Hello, Toto from somewhere!\"\n}\n```\n\n### Saving action code\n\nCode associated with an existing action may be retrieved and saved locally. Saving can be performed on all actions except sequences and docker actions.\n\n1. Save action code to a filename that corresponds with an existing action name in the current working directory. A file extension that corresponds to the action kind is used, or an extension of `.zip` will be used for action code that is a zip file.\n  ```\n  wsk action get /whisk.system/samples/greeting --save\n  ok: saved action code to /path/to/openwhisk/greeting.js\n  ```\n\n2. You may provide your own file name and extension as well using the `--save-as` flag.\n  ```\n  wsk action get /whisk.system/samples/greeting --save-as hello.js\n  ok: saved action code to /path/to/openwhisk/hello.js\n  ```\n\n## Listing actions\n\nYou can list all the actions that you have created using `wsk action list`:\n\n```\nwsk action list\n```\n```\nactions\n/guest/mySequence                  private sequence\n/guest/greeting                    private nodejs:6\n```\n\nHere, we see actions listed in order from most to least recently updated. For easier browsing, you can use the flag `--name-sort` or `-n` to sort the list alphabetically:\n\n```\nwsk action list --name-sort\n```\n```\nactions\n/guest/mySequence                  private sequence\n/guest/greeting                    private nodejs:6\n```\n\nNotice that the list is now sorted alphabetically by namespace, then package name if any, and finally action name, with the default package (no specified package) listed at the top.\n\n**Note**: The printed list is sorted alphabetically after it is received from the platform. Other list flags such as `--limit` and `--skip` will be applied to the block of actions before they are received for sorting. To list actions in order by creation time, use the flag `--time`.\n\nAs you write more actions, this list gets longer and it can be helpful to group related actions into [packages](packages.md). To filter your list of actions to just those within a specific package, you can use:\n\n```\nwsk action list /whisk.system/utils\n```\n```\nactions\n/whisk.system/utils/hosturl        private nodejs:6\n/whisk.system/utils/namespace      private nodejs:6\n/whisk.system/utils/cat            private nodejs:6\n/whisk.system/utils/smash          private nodejs:6\n/whisk.system/utils/echo           private nodejs:6\n/whisk.system/utils/split          private nodejs:6\n/whisk.system/utils/date           private nodejs:6\n/whisk.system/utils/head           private nodejs:6\n/whisk.system/utils/sort           private nodejs:6\n```\n\n## Deleting actions\n\nYou can clean up by deleting actions that you do not want to use.\n\n1. Run the following command to delete an action:\n  ```\n  wsk action delete greeting\n  ok: deleted greeting\n  ```\n\n2. Verify that the action no longer appears in the list of actions.\n  ```\n  wsk action list\n  ```\n  ```\n  actions\n  /guest/mySequence                private sequence\n  ```\n\n## Accessing action metadata within the action body\n\nThe action environment contains several properties that are specific to the running action.\nThese allow the action to programmatically work with OpenWhisk assets via the REST API,\nor set an internal alarm when the action is about to use up its allotted time budget.\nThe properties are accessible via the system environment for all supported runtimes:\nNode.js, Python, Swift, Java and Docker actions when using the OpenWhisk Docker skeleton.\n\n* `__OW_API_HOST` the API host for the OpenWhisk deployment running this action.\n* `__OW_API_KEY` the API key for the subject invoking the action, this key may be a restricted API key. This property is absent unless explicitly [requested](./annotations.md#annotations-for-all-actions).\n* `__OW_NAMESPACE` the namespace for the _activation_ (this may not be the same as the namespace for the action).\n* `__OW_ACTION_NAME` the fully qualified name of the running action.\n* `__OW_ACTION_VERSION` the internal version number of the running action.\n* `__OW_ACTIVATION_ID` the activation id for this running action instance.\n* `__OW_DEADLINE` the approximate time when this action will have consumed its entire duration quota (measured in epoch milliseconds).\n"
  },
  {
    "path": "docs/annotations.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Annotations on OpenWhisk assets\n\nOpenWhisk actions, triggers, rules and packages (collectively referred to as assets) may be decorated with `annotations`. Annotations are attached to assets just like parameters with a `key` that defines a name and `value` that defines the value. It is convenient to set them from the command line interface (CLI) via `--annotation` or `-a` for short.\n\nRationale: Annotations were added to OpenWhisk to allow for experimentation without making changes to the underlying asset schema. We had, until the writing of this document, deliberately not defined what `annotations` are permitted. However as we start to use annotations more heavily to impart semantic changes, it's important that we finally start to document them.\n\nThe most prevalent use of annotations to date is to document actions and packages. You'll see many of the packages in the OpenWhisk catalog carry annotations such as a description of the functionality offered by their actions, which parameters are required at package binding time, and which are invoke-time parameters, whether a parameter is a \"secret\" (e.g., password), or not. We have invented these as needed, for example to allow for UI integration.\n\nHere is a sample set of annotations for an `echo` action which returns its input arguments unmodified (e.g., `function main(args) { return args }`). This action may be useful for logging input parameters for example as part of a sequence or rule.\n\n```\nwsk action create echo echo.js \\\n    -a description 'An action which returns its input. Useful for logging input to enable debug/replay.' \\\n    -a parameters  '[{ \"required\":false, \"description\": \"Any JSON entity\" }]' \\\n    -a sampleInput  '{ \"msg\": \"Five fuzzy felines\"}' \\\n    -a sampleOutput '{ \"msg\": \"Five fuzzy felines\"}'\n```\n\nThe annotations we have used for describing packages are:\n\n* `description`: a pithy description of the package\n* `parameters`: an array describing parameters that are scoped to the package (described further below)\n\nSimilarly, for actions:\n\n* `description`: a pithy description of the action\n* `parameters`: an array describing actions that are required to execute the action\n* `sampleInput`: an example showing the input schema with typical values\n* `sampleOutput`: an example showing the output schema, usually for the `sampleInput`\n\nThe annotations we have used for describing parameters include:\n\n* `name`: the name of the parameter\n* `description`: a pithy description of the parameter\n* `doclink`: a link to further documentation for parameter (useful for OAuth tokens for example)\n* `required`: true for required parameters and false for optional ones\n* `bindTime`: true if the parameter should be specified when a package is bound\n* `type`: the type of the parameter, one of `password`, `array` (but may be used more broadly)\n\nThe annotations are _not_ checked. So while it is conceivable to use the annotations to infer if a composition of two actions into a sequence is legal, for example, the system does not yet do that.\n\n# Annotations for all actions\n\nThe following annotations on an action are available.\n\n* `provide-api-key`: This annotation may be attached to actions which require an API key, for example to make REST API calls to the OpenWhisk host. For newly created actions, if not specified, it defaults to a false value. For existing actions, the absence of this annotation, or its presence with a value that is not _falsy_ (i.e., a value that is different from zero, null, false, and the empty string) will cause an API key to be present in the [action execution context](./actions.md#accessing-action-metadata-within-the-action-body).\n\n# Annotations specific to web actions\n\nWeb actions are enabled with explicit annotations which decorate individual actions. The annotations only apply to the [web actions](webactions.md) API,\nand must be present and explicitly set to `true` to have an affect. The annotations have no meaning otherwise in the system. The annotations are:\n\n* `web-export`: Makes its corresponding action accessible to REST calls _without_ authentication. We call these [_web actions_](webactions.md) because they allow one to use OpenWhisk actions from a browser for example. It is important to note that the _owner_ of the web action incurs the cost of running them in the system (i.e., the _owner_ of the action also owns the activations record). The rest of the annotations described below have no effect on the action unless this annotation is also set.\n* `final`: Makes all of the action parameters that are already defined immutable. A parameter of an action carrying the annotation may not be overridden by invoke-time parameters once the parameter has a value defined through its enclosing package or the action definition.\n* `raw-http`: When set, the HTTP request query and body parameters are passed to the action as reserved properties.\n* `web-custom-options`: When set, this annotation enables a web action to respond to OPTIONS requests with customized headers, otherwise a [default CORS response](webactions.md#options-requests) applies.\n* `require-whisk-auth`: This annotation protects the web action so that it is only invoked by requests that provide appropriate authentication credentials. When set to a boolean value, it controls whether or not the request's Basic Authentication value (i.e. Whisk auth key) will be authenticated - a value of `true` will authenticate the credentials, a value of `false` will invoke the action without any authentication. When set to a number or a string, this value must match the request's `X-Require-Whisk-Auth` header value. In both cases, it is important to note that the _owner_ of the web action will still incur the cost of running them in the system (i.e., the _owner_ of the action also owns the activations record).\n\n# Annotations specific to activations\n\nThe system decorates activation records with annotations as well. They are:\n\n* `path`: the fully qualified path name of the action that generated the activation. Note that if this activation was the result of an action in a package binding, the path refers to the parent package.\n* `binding`: the entity path of the package binding. Note that this is only present for actions in a package binding.\n* `kind`: the kind of action executed, and one of the support OpenWhisk runtime kinds.\n* `limits`: the time, memory and log limits that this activation were subject to.\n\nAdditionally for sequence related activations, the system will generate the following annotations:\n\n* `topmost`: this is only present for an outermost sequence action.\n* `causedBy`: this is only present for actions that are contained in a sequence.\n\nLastly, and in order to provide you with some performance transparency, activations also record:\n\n* `waitTime`: the time spent waiting in the internal OpenWhisk system. This is roughly the time spent between the controller receiving the activation request and when the invoker provisioned a container for the action.\n* `initTime`: the time spent initializing the function. If this value is present, the action required initialization and represents a cold start. A warm activation will skip initialization, and in this case, the annotation is not generated.\n\nAn example of these annotations as they would appear in an activation record is shown below.\n\n```javascript\n\"annotations\": [\n  {\n    \"key\": \"path\",\n    \"value\": \"guest/echo\"\n  },\n  {\n    \"key\": \"waitTime\",\n    \"value\": 66\n  },\n  {\n    \"key\": \"kind\",\n    \"value\": \"nodejs:6\"\n  },\n  {\n    \"key\": \"initTime\",\n    \"value\": 50\n  },\n  {\n    \"key\": \"limits\",\n    \"value\": {\n      \"logs\": 10,\n      \"memory\": 256,\n      \"timeout\": 60000\n    }\n  }\n]\n```\n"
  },
  {
    "path": "docs/apigateway.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# API Gateway\n\nOpenWhisk actions can benefit from being managed by API management.\n\nThe API Gateway can act as a proxy to [Web Actions](webactions.md) and provides them with additional features including HTTP method routing , client id/secrets, rate limiting and CORS.\nFor more information on API Gateway feature you can read the [api management documentation](https://github.com/apache/openwhisk-apigateway/blob/master/doc/v2/management_interface_v2.md)\n\n\n## Create APIs from OpenWhisk web actions using the CLI\n\n### OpenWhisk CLI configuration\n\nFollow the instructions in [Configure CLI](./README.md#setting-up-the-openwhisk-cli) on how to set the authentication key for your specific namespace.\n\n### Create your first API using the CLI\n\n1. Create a JavaScript file with the following content. For this example, the file name is 'hello.js'.\n  ```javascript\n  function main({name:name='Serverless API'}) {\n      return {payload: `Hello world ${name}`};\n  }\n  ```\n\n2. Create a web action from the following JavaScript function. For this example, the action is called 'hello'. Make sure to add the flag `--web true`\n\n  ```\n  wsk action create hello hello.js --web true\n  ```\n  ```\n  ok: created action hello\n  ```\n\n3. Create an API with base path `/hello`, path `/world` and method `get` with response type `json`\n\n  ```\n  wsk api create /hello /world get hello --response-type json\n  ```\n  ```\n  ok: created API /hello/world GET for action /_/hello\n  https://${APIHOST}:9001/api/${GENERATED_API_ID}/hello/world\n  ```\n  A new URL is generated exposing the `hello` action via a __GET__ HTTP method.\n\n4. Let's give it a try by sending a HTTP request to the URL.\n\n  ```\n  $ curl https://${APIHOST}:9001/api/${GENERATED_API_ID}/hello/world?name=OpenWhisk\n  ```\n\n  ```json\n  {\n  \"payload\": \"Hello world OpenWhisk\"\n  }\n  ```\n   The web action `hello` was invoked, returning back a JSON object including the parameter `name` sent via query parameter. You can pass parameters to the action via simple query parameters, or via the request body. Web actions allow you to invoke an action in a public way without the OpenWhisk authorization API key.\n\n### Full control over the HTTP response\n\n  The `--response-type` flag controls the target URL of the web action to be proxied by the API Gateway. Using `--response-type json` as above returns the full result of the action in JSON format and automatically sets the Content-Type header to `application/json` which enables you to easily get started.\n\n  Once you get started, you will want to have full control over the HTTP response properties like `statusCode` and `headers`, and you may want to return different content types in the `body`. You can do this by using `--response-type http`, this will configure the target URL of the web action with the `http` extension.\n\n  You can choose to change the code of the action to comply with the return of web actions with `http` extension or include the action in a sequence passing its result to a new action that transforms the result to be properly formatted for an HTTP response. You can read more about response types and web actions extensions in the [Web Actions](webactions.md) documentation.\n\n  Change the code for the `hello.js` returning the JSON properties `body`, `statusCode` and `headers`\n  ```javascript\n  function main({name:name='Serverless API'}) {\n      return {\n        body: {payload:`Hello world ${name}`},\n        statusCode: 200,\n        headers:{ 'Content-Type': 'application/json'}\n      };\n  }\n  ```\n\n  Update the action with the modified result\n  ```\n  wsk action update hello hello.js --web true\n  ```\n  Update the API with `--response-type http`\n  ```\n  wsk api create /hello /world get hello --response-type http\n  ```\n  Let's call the updated API\n  ```\n  curl https://${APIHOST}:9001/api/${GENERATED_API_ID}/hello/world\n  ```\n  ```json\n  {\n  \"payload\": \"Hello world Serverless API\"\n  }\n  ```\n  Now you are in full control of your APIs, can control the content like returning HTML, or set the status code for things like Not Found (404), or Unauthorized (401), or even Internal Error (500).\n\n### Exposing multiple web actions\n\nLet's say you want to expose a set of actions for a book club for your friends.\nYou have a series of actions to implement your backend for the book club:\n\n| action | HTTP method | description |\n| ----------- | ----------- | ------------ |\n| getBooks    | GET | get book details  |\n| postBooks   | POST | adds a book |\n| putBooks    | PUT | updates book details |\n| deleteBooks | DELETE | deletes a book |\n\nLet's create an API for the book club, named `Book Club`, with `/club` as its HTTP URL base path and `books` as its resource and `{isbn}` as a path parameter used to identify a specific book by its ISBN.\n\nWhen using path parameters, the API must be defined with a response type of `http`, and the path, starting with the base path and including the actual path parameter value(s), will be available in the `__ow_path` field of the action's JSON parameter. Refer to the [Web Actions HTTP Context](webactions.md#http-context) documentation for more details about this and other HTTP context fields that are available to web actions invoked with a `http` response type.\n```\nwsk api create -n \"Book Club\" /club /books/{isbn} get getBooks --response-type http\nwsk api create /club /books get getBooks                       --response-type http\nwsk api create /club /books post postBooks                     --response-type http\nwsk api create /club /books/{isbn} put putBooks                --response-type http\nwsk api create /club /books/{isbn} delete deleteBooks          --response-type http\n```\n\nNotice that the first action exposed with base path `/club` gets the API label with name `Book Club` any other actions exposed under `/club` will be associated with `Book Club`\n\nLet's list all the actions that we just exposed.\n\n```\nwsk api list /club -f\n```\n```\nok: APIs\nAction: getBooks\n  API Name: Book Club\n  Base path: /club\n  Path: /books/{isbn}\n  Verb: get\n  URL: https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/{isbn}\nAction: getBooks\n  API Name: Book Club\n  Base path: /club\n  Path: /books\n  Verb: get\n  URL: https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books\nAction: postBooks\n  API Name: Book Club\n  Base path: /club\n  Path: /books\n  Verb: post\n  URL: https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books\nAction: putBooks\n  API Name: Book Club\n  Base path: /club\n  Path: /books/{isbn}\n  Verb: put\n  URL: https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/{isbn}\nAction: deleteBooks\n  API Name: Book Club\n  Base path: /club\n  Path: /books/{isbn}\n  Verb: delete\n  URL: https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/{isbn}\n```\n\nNow just for fun let's add a new book `JavaScript: The Good Parts` with a HTTP __POST__\n```\ncurl -X POST -d '{\"name\":\"JavaScript: The Good Parts\", \"isbn\":\"978-0596517748\"}' -H \"Content-Type: application/json\" https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books\n```\n```\n{\n  \"result\": \"success\"\n}\n```\n\nLet's get a list of books using our action `getBooks` via HTTP __GET__\n```\ncurl -X GET https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books\n```\n```\n{\n  \"result\": [{\"name\":\"JavaScript: The Good Parts\", \"isbn\":\"978-0596517748\"}]\n}\n```\n\nLet's delete a specify book using our action `deleteBooks` via HTTP __DELETE__. In this example, the `deleteBooks` action's `__ow_path` field value will be `/club/books/978-0596517748`, where `978-0596517748` is path's `{isbn}` actual value.\n```\ncurl -X DELETE https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/978-0596517748\n```\n\n### Exporting the configuration\nLet's export API named `Book Club` into a file that we can use as a base to to re-create the APIs using a file as input.\n```\nwsk api get \"Book Club\" > club-swagger.json\n```\n\nLet's test the swagger file by first deleting all exposed URLs under a common base path.\nYou can delete all of the exposed URLs using either the base path `/club` or API name label `\"Book Club\"`:\n```\nwsk api delete /club\n```\n```\nok: deleted API /club\n```\n### Changing the configuration\n\nYou can edit the configuration file to configure API Gateway extensions such as disabling or enabling CORS, for more info on the format of the configuration file refer to the API Gateway [docs](https://github.com/apache/openwhisk-apigateway/blob/master/doc/v2/management_interface_v2.md#gateway-specific-extensions).\n\n### Importing the configuration\n\nNow let's restore the API named `Book Club` by using the file `club-swagger.json`\n```\nwsk api create --config-file club-swagger.json\n```\n```\nok: created api /club/books/{isbn} get for action getBooks\nhttps://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/{isbn}\nok: created api /club/books/{isbn} put for action putBooks\nhttps://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/{isbn}\nok: created api /club/books/{isbn} delete for action deleteBooks\nhttps://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/{isbn}\nok: created api /club/books get for action getBooks\nhttps://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books\nok: created api /club/books post for action postBooks\nhttps://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books\n```\n\nWe can verify that the API has been re-created\n```\nwsk api list /club\n```\n```\nok: apis\nAction                    Verb         API Name        URL\ngetBooks                   get         Book Club       https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books\npostBooks                 post         Book Club       https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books\ngetBooks                   get         Book Club       https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/{isbn}\nputBooks                   put         Book Club       https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/{isbn}\ndeleteBooks             delete         Book Club       https://${APIHOST}:9001/api/${GENERATED_API_ID}/club/books/{isbn}\n```\n"
  },
  {
    "path": "docs/catalog.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Using OpenWhisk-enabled services\n\nIn OpenWhisk, a catalog of packages gives you an easy way to enhance your app with useful capabilities, and to access external services in the ecosystem. Examples of external services that are OpenWhisk-enabled include Cloudant, The Weather Company, Slack, and GitHub.\n\nThe catalog is available as packages in the `/whisk.system` namespace. See [Browsing packages](./packages.md#browsing-packages) for information about how to browse the catalog by using the command line tool.\n\n## Existing packages in catalog\n\n| Package | Description |\n| --- | --- |\n| [/whisk.system/alarms](https://github.com/apache/openwhisk-package-alarms/blob/master/README.md) | Package to create periodic triggers |\n| [/whisk.system/cloudant](https://github.com/apache/openwhisk-package-cloudant/blob/master/README.md) | Package to work with [Cloudant NoSQL DB](https://console.ng.bluemix.net/docs/services/Cloudant/index.html) service |\n| [/whisk.system/github](https://github.com/apache/openwhisk-catalog/blob/master/packages/github/README.md) | Package to create webhook triggers for [GitHub](https://developer.github.com/) |\n| [/whisk.system/messaging](https://github.com/apache/openwhisk-package-kafka/blob/master/README.md) | Package to work with [Message Hub](https://console.ng.bluemix.net/docs/services/MessageHub/index.html) service |\n| [/whisk.system/pushnotifications](https://github.com/apache/openwhisk-package-pushnotifications/blob/master/README.md) | Package to work with [Push Notification](https://console.ng.bluemix.net/docs/services/mobilepush/index.html) service |\n| [/whisk.system/slack](https://github.com/apache/openwhisk-catalog/blob/master/packages/slack/README.md) | Package to post to the [Slack APIs](https://api.slack.com/) |\n| [/whisk.system/watson-translator](https://github.com/apache/openwhisk-catalog/blob/master/packages/watson-translator/README.md) | Package for [text translation and language identification](https://www.ibm.com/watson/developercloud/language-translator.html) |\n| [/whisk.system/watson-speechToText](https://github.com/apache/openwhisk-catalog/blob/master/packages/watson-speechToText/README.md) | Package to convert [speech into text](https://www.ibm.com/watson/developercloud/speech-to-text.html) |\n| [/whisk.system/watson-textToSpeech](https://github.com/apache/openwhisk-catalog/blob/master/packages/watson-textToSpeech/README.md) | Package to convert [text into speech](https://www.ibm.com/watson/developercloud/text-to-speech.html) |\n| [/whisk.system/weather](https://github.com/apache/openwhisk-catalog/blob/master/packages/weather/README.md) | Package to work with [Weather Company Data](https://console.ng.bluemix.net/docs/services/Weather/index.html) service |\n| [/whisk.system/websocket](https://github.com/apache/openwhisk-catalog/blob/master/packages/websocket/README.md) | Package to work with a [Web Socket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server |\n\n<!--\nTODO: place holder until we have a README for samples\n| [/whisk.system/samples](https://github.com/apache/openwhisk-catalog/blob/master/packages/samples/README.md) | offers sample actions in different languages |\n-->\n<!--\nTODO: place holder until we have a README for utils\n| [/whisk.system/utils](https://github.com/apache/openwhisk-catalog/blob/master/packages/utils/README.md) | offers utilities actions such as cat, echo, and etc. |\n-->\n"
  },
  {
    "path": "docs/cli.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# OpenWhisk CLI\n\nOpenWhisk offers a powerful command line interface that allows complete management of all aspects of the system.\n\n## Setting up the OpenWhisk CLI\n\n- Building OpenWhisk from a cloned repository results in the generation of the command line interface. The generated CLIs are located in `openwhisk/bin/`. The main CLI is located in `openwhisk/bin/wsk` that runs on the operating system, and CPU architecture on which it was built. Executables for other operating system, and CPU architectures are located in the following directories: `openwhisk/bin/mac/`, `openwhisk/bin/linux/`, `openwhisk/bin/windows/`.\n\n- To download the CLI from an existing deployment, you will need to download the CLI using the deployment's base URL.\nA list of downloadable CLIs for various operating systems, and CPU architectures can be obtained from the following\nlocation `{BASE URL}/cli`. The `{BASE URL}` is the OpenWhisk API hostname or IP address.\n\nThere are two required properties to configure in order to use the CLI:\n\n1. **API host** (name or IP address) for the OpenWhisk deployment you want to use.\n2. **Authorization key** (username and password) which grants you access to the OpenWhisk API.\n\nThe API host can be acquired from the `edge.host` property in `whisk.properties` file, which is generated during\ndeployment of OpenWhisk. Run the following command from your `openwhisk` directory to set the API host:\n\n```\n./bin/wsk property set --apihost <openwhisk_baseurl>\n```\n\n**Tip:** If you are using a local OpenWhisk deployment with a self-signed SSL certificate, you can use `--insecure` to bypass certificate validation.\n\nIf you know your authorization key, you can configure the CLI to use it. Otherwise, you will need to obtain an\nauthorization key for most CLI operations. A _guest_ account is available in local installations with an authorization\nkey located in [ansible/files/auth.guest](../ansible/files/auth.guest). To configure the CLI to use the guest account,\nyou can run the following command from your `openwhisk` directory:\n\n```\n./bin/wsk property set --auth `cat ansible/files/auth.guest`\n```\n\n**Tip:** The OpenWhisk CLI stores properties in the `~/.wskprops` configuration file by default. The location of this file can be altered by setting the `WSK_CONFIG_FILE` environment variable.\n\nThe required properties described above have the following keys in the `.wskprops` file:\n\n- **APIHOST** - Required key for the API host value.\n- **AUTH** - Required key for the Authorization key.\n\nTo verify your CLI setup, try [creating and running an action](./samples.md).\n\n### Optional Whisk Properties\n\nSome OpenWhisk providers make use of optional properties that can be added to the `.wskprops` file.  The following keys are optional:\n\n- **APIGW_ACCESS_TOKEN** - Optional, provider-specific authorization token for an independently hosted API Gateway service used for managing OpenWhisk API endpoints.\n\n- **APIGW_TENANT_ID** - Optional, provider-relative identifier of the tenant (owner for access control purposes) of any API endpoints that are created by the CLI.\n\n### Configure command completion for Openwhisk CLI\n\nFor bash command completion to work, bash 4.1 or newer is required. The most recent Linux distributions should have the correct version of bash but Mac users will most likely have an older version.\n\nMac users can check their bash version and update it by running the following commands:\n\n```\nbash --version\nbrew install bash\n```\n\nThis requires [Homebrew](https://brew.sh/) to be installed. The updated bash will be installed in `/usr/local/bin`.\n\nTo write the bash command completion to your local directory, run the following command:\n\n```\nwsk sdk install bashauto\n```\nThe command script `wsk_cli_bash_completion.sh` will now be in your current directory. To enable command line completion of wsk commands, source the auto completion script into your bash environment.\n\n```\nsource wsk_cli_bash_completion.sh\n```\n\nAlternatively, to install bash command completion, run the following command:\n\n```\neval \"`wsk sdk install bashauto --stdout`\"\n```\n\nFor Mac users, autocomplete doesn't insert space after using TAB. To workaround this, you need to modify the output script like the following:\n```\neval \"`wsk sdk install bashauto --stdout | sed 's/-o nospace//'`\"\n```\n\n**Note:** Every time a new terminal is opened, this command must run to enable bash command completion. Alternatively, adding the previous command to the `.bashrc` or `.profile` will prevent this.\n\n## Using the OpenWhisk CLI\n\nAfter you have configured your environment, you can begin using the OpenWhisk CLI to do the following:\n\n* Run your code snippets, or actions, on OpenWhisk. See [Creating and invoking actions](./actions.md).\n* Use triggers and rules to enable your actions to respond to events. See [Creating triggers and rules](./triggers_rules.md).\n* Learn how packages bundle actions and configure external events sources. See [Using and creating packages](./packages.md).\n* Explore the catalog of packages and enhance your applications with external services, such as a [Cloudant event source](./catalog.md#using-the-cloudant-package). See [Using OpenWhisk-enabled services](./catalog.md).\n\n## Configure the CLI to use an HTTPS proxy\n\nThe CLI can be setup to use an HTTPS proxy. To setup an HTTPS proxy, an environment variable called `HTTPS_PROXY` must be created. The variable must be set to the address of the HTTPS proxy, and its port using the following format:\n`{PROXY IP}:{PROXY PORT}`.\n\n## Configure the CLI to use client certificate\nThe CLI has an extra level of security from client to apihost, system provides default client certificate configuration which deployment process generated, then you can refer to below steps to use client certificate:\n* The client certificate verification is off default, you can configure `nginx_ssl_verify_client` to `on` or `optional` to open it for your corresponding environment configuration.\n* Create your own client certificate instead of system provides if you want, after created, you can configure `openwhisk_client_ca_cert` to your own ca cert path for your corresponding environment configuration.\n* Run the following command to pass client certificate:\n```\n./bin/wsk property set --cert <client_cert_path> --key <client_key_path>\n```\n"
  },
  {
    "path": "docs/conductors.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Conductor Actions\n\nConductor actions make it possible to build and invoke a series of actions, similar to sequences. However, whereas the components of a sequence action must be specified before invoking the sequence, conductor actions can decide the series of actions to invoke at run time.\n\nIn this document, we specify conductor actions and illustrate them with a simple example: a _tripleAndIncrement_ action.\n\nSuppose we define a _triple_ action in a source file `triple.js`:\n\n```javascript\nfunction main({ value }) { return { value: value * 3 } }\n```\n\nWe create the action _triple_:\n\n```\nwsk action create triple triple.js\n```\n\nWe define an _increment_ action in a source file `increment.js`:\n\n```javascript\nfunction main({ value }) { return { value: value + 1 } }\n```\n\nWe create the action _increment_:\n\n```\nwsk action create increment increment.js\n```\n\n## Conductor annotation\n\nWe define the _tripleAndIncrement_ action in a source file `tripleAndIncrement.js`:\n\n```javascript\nfunction main(params) {\n    let step = params.$step || 0\n    delete params.$step\n    switch (step) {\n        case 0: return { action: 'triple', params, state: { $step: 1 } }\n        case 1: return { action: 'increment', params, state: { $step: 2 } }\n        case 2: return { params }\n    }\n}\n```\n\nWe create a _conductor_ action by specifying the _conductor_ annotation:\n\n```\nwsk action create tripleAndIncrement tripleAndIncrement.js -a conductor true\n```\n\nA _conductor action_ is an action with a _conductor_ annotation with a value that is not _falsy_, i.e., a value that is different from zero, null, false, and the empty string.\n\nAt this time, the conductor annotation is ignored on sequence actions.\n\nBecause a conductor action is an action, it has all the attributes of an action (name, namespace, default parameters, limits...) and it can be managed as such, for instance using the `wsk action` CLI commands. It can be part of a package or be a web action.\n\nIn essence, the _tripleAndIncrement_ action builds a sequence of two actions by encoding a program with three steps:\n\n- step 0: invoke the _triple_ action on the input dictionary,\n- step 1: invoke the _increment_ action on the output dictionary from step 1,\n- step 2: return the output dictionary from step 2.\n\nAt each step, the conductor action specifies how to continue or terminate the execution by means of a _continuation_. We explain continuations after discussing invocation and activations.\n\n## Invocation\n\nA conductor action is invoked like a regular [action](actions.md), for instance:\n\n```\nwsk action invoke tripleAndIncrement -r -p value 3\n```\n```json\n{\n    \"value\": 10\n}\n```\n\nBlocking and non-blocking invocations are supported. As usual, a blocking invocation may timeout before the completion of the invocation.\n\n## Activations\n\nOne invocation of the conductor action results in multiple activations, for instance:\n\n```\nwsk action invoke tripleAndIncrement -p value 3\n```\n```\nok: invoked /_/tripleAndIncrement with id 4f91f9ed0d874aaa91f9ed0d87baaa07\n```\n```\nwsk activation list\n```\n<pre>\nDatetime            Activation ID                    Kind      Start Duration   Status   Entity\n2019-03-16 20:03:00 8690bc9904794c9390bc9904794c930e nodejs:6  warm  2ms        success  guest/tripleAndIncrement:0.0.1\n2019-03-16 20:02:59 7e76452bec32401db6452bec32001d68 nodejs:6  cold  32ms       success  guest/increment:0.0.1\n2019-03-16 20:02:59 097250ad10a24e1eb250ad10a23e1e96 nodejs:6  warm  2ms        success  guest/tripleAndIncrement:0.0.1\n2019-03-16 20:02:58 4991a50ed9ed4dc091a50ed9edddc0bb nodejs:6  cold  33ms       success  guest/triple:0.0.1\n2019-03-16 20:02:57 aee63124f3504aefa63124f3506aef8b nodejs:6  cold  34ms       success  guest/tripleAndIncrement:0.0.1\n2019-03-16 20:02:57 22da217c8e3a4b799a217c8e3a0b79c4 sequence  warm  3.46s      success  guest/tripleAndIncrement:0.0.1\n</pre>\n\nThere are six activation records in this example, one matching the activation id returned on invocation (`22da217c8e3a4b799a217c8e3a0b79c4`) plus five additional records for activations _caused_ by this invocation. The _primary_ activation record is the last one in the list because it has the earliest start time.\n\nThe five additional activations are:\n\n- one activation of the _triple_ action with input `{ value: 3 }` and output `{ value: 9 }`,\n- one activation of the _increment_ action with input `{ value: 9 }` and output `{ value: 10 }`,\n- three _secondary_ activations of the _tripleAndIncrement_ action.\n\n### Causality\n\nWe say the invocation of the conductor action is the _cause_ of _component_ action invocations as well as _secondary_ activations of the conductor action. These activations are _derived_ activations.\n\nThe cause field of the _derived_ activation records is set to the id for the _primary_ activation record.\n\n### Primary activations\n\nThe primary activation record for the invocation of a conductor action is a synthetic record similar to the activation record of a sequence action. The primary activation record summarizes the series of derived activations:\n\n- its result is the result of the last action in the series (possibly unboxed, see below),\n- its logs are the ordered list of component and secondary activations,\n- its duration is the sum of the durations of these activations,\n- its start time is less or equal to the start time of the first derived activation in the series,\n- its end time is greater or equal to the end time of the last derived activation in the series.\n\n```\nwsk activation get 4f91f9ed0d874aaa91f9ed0d87baaa07\n```\n```\nok: got activation 4f91f9ed0d874aaa91f9ed0d87baaa07\n{\n    \"namespace\": \"guest\",\n    \"name\": \"composition\",\n    \"version\": \"0.0.1\",\n    \"subject\": \"guest\",\n    \"activationId\": \"4f91f9ed0d874aaa91f9ed0d87baaa07\",\n    \"start\": 1516379705819,\n    \"end\": 1516379707803,\n    \"duration\": 457,\n    \"response\": {\n        \"status\": \"success\",\n        \"statusCode\": 0,\n        \"success\": true,\n        \"result\": {\n            \"value\": 12\n        }\n    },\n    \"logs\": [\n        \"3624ad829d4044afa4ad829d40e4af60\",\n        \"a1f58ade9b1e4c26b58ade9b1e4c2614\",\n        \"47cb5aa5e4504f818b5aa5e450ef810f\",\n        \"eaec119273d94087ac119273d90087d0\",\n        \"fd89b99a90a1462a89b99a90a1d62a8e\"\n    ],\n    \"annotations\": [\n        {\n            \"key\": \"topmost\",\n            \"value\": true\n        },\n        {\n            \"key\": \"path\",\n            \"value\": \"guest/tripleAndIncrement\"\n        },\n        {\n            \"key\": \"conductor\",\n            \"value\": true\n        },\n        {\n            \"key\": \"kind\",\n            \"value\": \"sequence\"\n        },\n        {\n            \"key\": \"limits\",\n            \"value\": {\n                \"logs\": 10,\n                \"memory\": 256,\n                \"timeout\": 60000\n            }\n        }\n    \"publish\": false\n}\n```\n\nIf a component action itself is a sequence or conductor action, the logs contain only the id for the component activation. They do not contain the ids for the activations caused by this component. This is different from nested sequence actions.\n\n### Secondary activations\n\nThe secondary activations of the conductor action are responsible for orchestrating the invocations of the component actions.\n\nAn invocation of a conductor action starts with a secondary activation and alternates secondary activations of this conductor action with invocations of the component actions. It normally ends with a secondary activation of the conductor action. In our example, the five derived activations are interleaved as follows:\n\n 1. secondary _tripleAndIncrement_ activation,\n 2. _triple_ activation,\n 3. secondary _tripleAndIncrement_ activation,\n 4. _increment_ activation,\n 5. secondary _tripleAndIncrement_ activation.\n\nIntuitively, secondary activations of the conductor action decide which component actions to invoke by running before, in-between, and after the component actions. In this example, the _tripleAndIncrement_ main function runs three times.\n\nOnly an internal error (invocation failure or timeout) may result in an even number of derived activations.\n\n### Annotations\n\nPrimary activation records include the annotations `{ key: \"conductor\", value: true }` and `{ key: \"kind\", value: \"sequence\" }`. Secondary activation records and activation records for component actions include the annotation `{ key: \"causedBy\", value: \"sequence\" }`.\n\nThe memory limit annotation in the primary activation record reflects the maximum memory limit across the conductor action and the component actions.\n\n## Continuations\n\nA conductor action should return either an _error_ dictionary, i.e., a dictionary with an _error_ field, or a _continuation_, i.e., a dictionary with up to three fields `{ action, params, state }`. In essence, a continuation specifies what component action to invoke if any, as well as the parameters for this invocation, and the state to preserve until the next secondary activation of the conductor action.\n\nThe execution flow in our example is the following:\n\n 1. The _tripleAndIncrement_ action is invoked on the input dictionary `{ value: 3 }`. It returns `{ action: 'triple', params: { value: 3 }, state: { $step: 1 } }` requesting that action _triple_ be invoked on _params_ dictionary `{ value: 3 }`.\n 2. The _triple_ action is invoked on dictionary `{ value: 3 }` returning `{ value: 9 }`.\n 3. The _tripleAndIncrement_ action is automatically reactivated. The input dictionary for this activation is `{ value: 9, $step: 1 }` obtained by combining the result of the _triple_ action invocation with the _state_ of the prior secondary _tripleAndIncrement_ activation (see below for details). It returns `{ action: 'increment', params: { value: 9 }, state: { $step: 2 } }`.\n 4. The _increment_ action is invoked on dictionary `{ value: 9 }` returning `{ value: 10 }`.\n 5. The _tripleAndIncrement_ action is automatically reactivated on dictionary `{ value: 10, $step: 2 }` returning `{ params: { value: 10 } }`.\n 6. Because the output of the last secondary _tripleAndIncrement_ activation specifies no further action to invoke, this completes the execution resulting in the recording of the primary activation. The result of the primary activation is obtained from the result of the last secondary activation by extracting the value of the _params_ field: `{ value: 10 }`.\n\n### Detailed specification\n\nIf a secondary activation returns an error dictionary, the conductor action invocation ends and the result of this activation (output and status code) are those of this secondary activation.\n\nIn a continuation dictionary, the _params_ field is optional and its value if present should be a dictionary. The _action_ field is optional and its value if present should be a string. The _state_ field is optional and its value if present should be a dictionary. If the value _v_ of the _params_ field is not a dictionary it is automatically boxed into dictionary `{ value: v }`. If the value _v_ of the _state_ field is not a dictionary it is automatically boxed into dictionary `{ state: v }`.\n\nIf the _action_ field is defined in the output of the conductor action, the runtime attempts to convert its value (irrespective of its type) into the fully qualified name of an action and invoke this action (using the default namespace if necessary). The action name should be a fully qualified name, which is of the form `/namespace/package-name/action-name` or `/namespace/action-name`. Failure to specify a fully qualified name may result in ambiguity or even a parsing error. There are four failure modes:\n\n- parsing failure,\n- resolution failure,\n- entitlement check failure,\n- internal error (invocation failure or timeout).\n\nIn the last failure scenario, the conductor action invocation ends with an _internal error_ status code and an error message describing the reason for the failure.\n\nIf there is no error, _action_ is invoked on the _params_ dictionary if specified (auto boxed if necessary) or if not on the empty dictionary. Upon completion of this invocation, the conductor action is activated again. The input dictionary for this activation is a combination of the output dictionary for the component action and the value of the _state_ field from the prior secondary conductor activation. Fields of the _state_ dictionary (auto boxed if necessary) are added to the output dictionary of the component activation, overriding values of existing fields if necessary.\n\nIn the first three failures scenarios, the conductor action is activated again. The input dictionary for this activation is a combination of an error object with an error message describing the reason for the failure and the value of the _state_ field from the prior secondary conductor activation (as in the previous scenario).\n\nOn the other hand, if the _action_ field is not defined in the output of the conductor action, the conductor action invocation ends. The output for the conductor action invocation is either the value of the _params_ field in the output dictionary of the last secondary activation if defined (auto boxed if necessary) or if absent the complete output dictionary.\n\n## Limits\n\nThere are limits on the number of component action activations and secondary conductor activations in a conductor action invocation. These limits are assessed globally, i.e., if some components of a conductor action invocation are themselves conductor actions, the limits apply to the combined counts of activations across all the conductor action invocations.\n\nThe maximum number _n_ of permitted component activations is equal to the maximum number of components in a sequence action. It is configured via the same configuration parameter. The maximum number of secondary conductor activations is _2n+1_.\n\nIf the maximum number of permitted component activations is exceeded the conductor action is activated again. The input dictionary for this activation is a combination of an error object with an error message describing the reason for the failure and the value of the _state_ field from the prior secondary conductor activation.\n\nIf the maximum number of secondary conductor activations is exceeded, the conductor action invocation ends with an _application error_ status code and an error message describing the reason for the failure.\n"
  },
  {
    "path": "docs/dedicated-invokers.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Dedicated Invokers\n\nActions run on any invokers in OpenWhisk. But users may want to run their actions on a certain invoker(s) for some reason such as IP-based ACL.\nThis is to provide dedicated invokers for a namespace. Operators can configure a dedicated namespace for invokers and all activations from the namespace will be delivered to the dedicated invokers only.\n\n## Tagging Invokers\nOperators can configure any tags for invokers.\n\n```bash\ninvoker0 ansible_host=${INVOKER01} tags=\"['dedicated']\" dedicatedNamespaces=\"['namespace1']\"\ninvoker1 ansible_host=${INVOKER02} tags=\"['dedicated']\" dedicatedNamespaces=\"['namespace2']\"\n```\n\nUsers can add the following annotations to their actions.\n\n```\nwsk action update params tests/dat/actions/params.js -i -a invoker-resources '[\"dedicated\"]'\n```\n\nSo this feature is based on the [tag-based-scheduling](./tag-based-scheduling.md).\nThe `dedicatedNamespaces` field is used to make sure the invokers are not used by other than allowed namespaces and users can decide whether to run their actions on dedicated invokers using the `dedicated` tag.\n"
  },
  {
    "path": "docs/deploy.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\nThis page documents configuration options that should be considered when deploying OpenWhisk. Some deployment options require special treatment wrt to the underlying infrastructure/deployment model. Please carefully read about the constraints before you decide to enable them.\n\n# Controller Clustering\n\nThe system can be configured to use Apache Pekko clustering to manage the distributed state of the Controller's load balancing algorithm.  This imposes the following constraints on a deployment\n\n## Cluster setup\n\nTo setup a cluster, the controllers need to be able to discover each other. There are 2 basic ways to achieve this:\n\n1. Provide the so called **seed-nodes** explicitly on deployment. Essentially you have a static list of possible seed nodes which are used to build a cluster. In an Ansible based deployment, they are determined for you from the `hosts` file. On any other deployment model, the `CONFIG_pekko_cluster_seedNodes.$i` variables will need to be provided according to the [Pekko cluster documentation](https://pekko.apache.org/docs/pekko/current/typed/cluster.html).\n2. Discover the nodes from an external service. This is built upon [pekko-management](https://pekko.apache.org/docs/pekko-management/current/) and by default [Kubernetes](https://pekko.apache.org/docs/pekko-management/current/discovery/kubernetes.html) and [Mesos (Marathon)](https://pekko.apache.org/docs/pekko-management/current/discovery/marathon.html) are supported. You can refer to the respective documentation above to configure discovery accordingly.\n\n\n## Controller nodes must have static IPs/Port combination.\n\nIt guarantees that failed nodes are able to join the cluster again.\nThis limitation refers to the fact that Pekko clustering doesn't allow to add new nodes when one of the existing members is unreachable (e.g. JVM failure). If each container receives a its ip and port dynamically upon the restart, in case of controller failure, it could come back online under a new ip/port combination which makes cluster consider it as a new member and it won't be added to the cluster (in certain cases it could join as a weeklyUp member). However, the cluster will still replicate the state across the online nodes, it will have trouble to get back to the previous state with desired number of members until the old member is explicitly \"downed\".\n\nHow to down the members.\n1. manually (sending an HTTP or JMX request to the controller). For this case an external supervisor for the cluster is required, which will down the nodes and provide an up-to-date list of seed nodes.\n2. automatically by setting the \"auto-down-property\" in controller that will allow the leader to down the node after a certain timeout. In order to mitigate brain split one could define a list of seed nodes which are reachable under static IPs or have static DNS entries.\n\nLink to Pekko clustering documentation:\nhttps://pekko.apache.org/docs/pekko/current/typed/cluster.html\n\n## Shared state vs. Sharding\n\nOpenWhisk used to support both shared state and a sharding model. The former has since been deprecated and removed.\n\nThe sharding loadbalancer has the caveat of being limited in its scalability in its current implementation. It uses \"horizontal\" sharding, which means that the slots on each invoker are evenly divided to the loadbalancers. For example: In a system with 2 loadbalancers and invokers which have 16 slots each, each loadbalancer would get 8 slots on each invoker. In this specific case, a cluster of loadbalancers > 16 instances does not make sense, since each loadbalancer would only have a fraction of a slot above that. The code guards against that but it is strongly recommended not to deploy more sharding loadbalancers than there are slots on each invoker.\n\n# Invoker use of runc\n\nTo improve performance, Invokers attempt to maintain warm containers for frequently executed actions. To optimize resource usage, the action containers are paused/unpaused between invocations.  The system can be configured to use either runc or docker to perform the pause/unpause operations by setting the value of the environment variable INVOKER_USE_RUNC to true or false respectively. If not set, it will default to true (use runc).\n\nUsing runc obtains significantly better performance, but requires that the version of runc within the invoker container is an exact version match to the runc of the host environment.  Failure to get an exact version match will results in error messages like:\n```\n2017-09-29T20:15:54.551Z] [ERROR] [#sid_102] [RuncClient] code: 1, stdout: , stderr: json: cannot unmarshal object into Go value of type []string [marker:invoker_runc.pause_error:6830148:259]\n```\nWhen a runc operations results in an error, the container will be killed by the invoker.  This results in missed opportunities for container reuse and poor performance.  Setting INVOKER_USE_RUNC to false can be used as a workaround until proper usage of runc can be configured for the deployment.\n"
  },
  {
    "path": "docs/dev/configuration.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Scala component configuration\n\nOur Scala components are in the process of switching from our own `WhiskConfig` to the TypesafeConfig based `pureconfig`. That will give us a more sane way of parsing environment values, overriding them and defining sensible defaults for them.\n\nTypesafeConfig is able to override its values via JVM system properties. Unfortunately, docker only supports passing environment variables in a comfortable way.\n\nTo solve that mismatch, we use a script to transform environment variables into JVM system properties to spare the developer of manually overriding all the values via explicit environment -> TypesafeConfig value mapping. That script applies the following transformations to all environment variables starting with a defined prefix (\"CONFIG_\" in our case):\n\n- `_` becomes `.` (TypesafeConfig hierarchies are built using `.`)\n- `camelCased` becomes `camel-cased` (Kebabcase is usually used for TypesafeConfig keys)\n- `PascalCased` stays `PascalCased` (Defining classnames for overriding SPIs is crucial)\n\n### Examples:\n\n- `CONFIG_whisk_loadbalancer_invokerBusyThreshold` becomes `-Dwhisk.loadbalancer.invoker-busy-threshold`\n- `CONFIG_pekko_remote_netty_tcp_bindPort` becomes `-Dpekko.remote.netty.tcp.bind-port`\n- `CONFIG_whisk_spi_LogStoreProvider` becomes `-Dwhisk.spi.LogStoreProvider`\n"
  },
  {
    "path": "docs/dev/future.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Potential Future Enhancements\n\n## Web actions potential future enhancements\n\nIt is worthwhile to consider how to make several of the features currently offered by web actions more generally available to all action. It may also be useful to generalize the `web-export` annotation to provide additional control over which HTTP methods an action is prepared the handle and similarly which content types it supports. One can imagine doing this with a richer `web-export` annotation which may define at least these additional properties:\n\n1. `methods`: array of HTTP methods accepted.\n2. `extensions`: array of supported extensions.\n3. `authentication`: one of `{\"none\", \"builtin\"}` where `none` is for anonymous access and `builtin` for OpenWhisk authentication.\n\nAs in `-a web-export '{ \"methods\": [\"get\", \"post\"], \"extensions\": [\"http\"], \"authentication\": \"none\" }'` for a web action that only accepts `get` and `post` requests, handled `.http` extensions only, and permits anonymous access. A richer set of annotations will allow the controller to reject requests early if a web action does not support a particular method for example. The current implementation will accept `get`, `post`, `put` and `delete` HTTP methods without discrimination for any web action.\n"
  },
  {
    "path": "docs/dev/modules.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n<!--\nDO NOT EDIT.\nThis page is generated via script `./gradlew :tools:dev:renderModuleDetails`. See tools/dev/README.md for details.\n-->\n\n# Modules\n\n\n## Main\n\n| Module | Description | Module Status |\n|---\t|---\t|---    |\n| [openwhisk](https://github.com/apache/openwhisk) | Apache OpenWhisk is an open source serverless cloud platform | [![Build Status](https://travis-ci.com/apache/openwhisk.svg?branch=master)](https://travis-ci.com/apache/openwhisk) |\n| [openwhisk-apigateway](https://github.com/apache/openwhisk-apigateway) | Apache OpenWhisk API Gateway service for exposing actions as REST interfaces. | [![Build Status](https://travis-ci.com/apache/openwhisk-apigateway.svg?branch=master)](https://travis-ci.com/apache/openwhisk-apigateway) |\n| [openwhisk-catalog](https://github.com/apache/openwhisk-catalog) | Curated catalog of Apache OpenWhisk packages to interface with event producers and consumers | [![Build Status](https://travis-ci.com/apache/openwhisk-catalog.svg?branch=master)](https://travis-ci.com/apache/openwhisk-catalog) |\n| [openwhisk-cli](https://github.com/apache/openwhisk-cli) | Apache OpenWhisk Command Line Interface (CLI) | [![Build Status](https://travis-ci.com/apache/openwhisk-cli.svg?branch=master)](https://travis-ci.com/apache/openwhisk-cli) |\n| [openwhisk-composer](https://github.com/apache/openwhisk-composer) | Apache OpenWhisk Composer provides a high-level programming model in JavaScript for composing serverless functions | [![Build Status](https://travis-ci.com/apache/openwhisk-composer.svg?branch=master)](https://travis-ci.com/apache/openwhisk-composer) |\n| [openwhisk-composer-python](https://github.com/apache/openwhisk-composer-python) | Apache OpenWhisk Composer Python provides a high-level programming model in Python for composing serverless functions | [![Build Status](https://travis-ci.com/apache/openwhisk-composer-python.svg?branch=master)](https://travis-ci.com/apache/openwhisk-composer-python) |\n| [openwhisk-wskdeploy](https://github.com/apache/openwhisk-wskdeploy) | Apache OpenWhisk utility for deploying and managing OpenWhisk projects and packages | [![Build Status](https://travis-ci.com/apache/openwhisk-wskdeploy.svg?branch=master)](https://travis-ci.com/apache/openwhisk-wskdeploy) |\n\n\n## Clients\n\n| Module | Description | Module Status |\n|---\t|---\t|---    |\n| [openwhisk-client-go](https://github.com/apache/openwhisk-client-go) | Go client library for the Apache OpenWhisk platform | [![Build Status](https://travis-ci.com/apache/openwhisk-client-go.svg?branch=master)](https://travis-ci.com/apache/openwhisk-client-go) |\n| [openwhisk-client-js](https://github.com/apache/openwhisk-client-js) | JavaScript client library for the Apache OpenWhisk platform | [![Build Status](https://travis-ci.com/apache/openwhisk-client-js.svg?branch=master)](https://travis-ci.com/apache/openwhisk-client-js) |\n\n\n## Runtimes\n\n| Module | Description | Module Status |\n|---\t|---\t|---    |\n| [openwhisk-runtime-deno](https://github.com/apache/openwhisk-runtime-deno) | Apache OpenWhisk Runtime Deno supports Apache OpenWhisk functions written in Deno | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-deno.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-deno) |\n| [openwhisk-runtime-docker](https://github.com/apache/openwhisk-runtime-docker) | Apache OpenWhisk SDK for building Docker \"blackbox\" runtimes | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-docker.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-docker) |\n| [openwhisk-runtime-dotnet](https://github.com/apache/openwhisk-runtime-dotnet) | Apache OpenWhisk Runtime .Net supports Apache OpenWhisk functions written in .Net languages | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-dotnet.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-dotnet) |\n| [openwhisk-runtime-go](https://github.com/apache/openwhisk-runtime-go) | Apache OpenWhisk Runtime Go supports Apache OpenWhisk functions written in Go | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-go.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-go) |\n| [openwhisk-runtime-java](https://github.com/apache/openwhisk-runtime-java) | Apache OpenWhisk Runtime Java supports Apache OpenWhisk functions written in Java and other JVM-hosted languages | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-java.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-java) |\n| [openwhisk-runtime-nodejs](https://github.com/apache/openwhisk-runtime-nodejs) | Apache OpenWhisk Runtime NodeJS supports Apache OpenWhisk functions written in JavaScript for NodeJS | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-nodejs.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-nodejs) |\n| [openwhisk-runtime-php](https://github.com/apache/openwhisk-runtime-php) | Apache OpenWhisk Runtime PHP supports Apache OpenWhisk functions written in PHP | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-php.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-php) |\n| [openwhisk-runtime-python](https://github.com/apache/openwhisk-runtime-python) | Apache OpenWhisk Runtime Python supports Apache OpenWhisk functions written in Python | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-python.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-python) |\n| [openwhisk-runtime-ruby](https://github.com/apache/openwhisk-runtime-ruby) | Apache OpenWhisk Runtime Ruby supports Apache OpenWhisk functions written in Ruby | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-ruby.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-ruby) |\n| [openwhisk-runtime-rust](https://github.com/apache/openwhisk-runtime-rust) | Apache OpenWhisk Runtime Rust supports Apache OpenWhisk functions written in Rust | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-rust.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-rust) |\n| [openwhisk-runtime-swift](https://github.com/apache/openwhisk-runtime-swift) | Apache OpenWhisk Runtime Swift supports Apache OpenWhisk functions written in Swift | [![Build Status](https://travis-ci.com/apache/openwhisk-runtime-swift.svg?branch=master)](https://travis-ci.com/apache/openwhisk-runtime-swift) |\n\n\n## Deployments\n\n| Module | Description | Module Status |\n|---\t|---\t|---    |\n| [openwhisk-deploy-kube](https://github.com/apache/openwhisk-deploy-kube) | The Apache OpenWhisk Kubernetes Deployment repository supports deploying the Apache OpenWhisk system on Kubernetes and OpenShift clusters. | [![Build Status](https://travis-ci.com/apache/openwhisk-deploy-kube.svg?branch=master)](https://travis-ci.com/apache/openwhisk-deploy-kube) |\n\n\n## Packages\n\n| Module | Description | Module Status |\n|---\t|---\t|---    |\n| [openwhisk-package-alarms](https://github.com/apache/openwhisk-package-alarms) | Apache OpenWhisk package that can be used to create periodic, time-based alarms. | [![Build Status](https://travis-ci.com/apache/openwhisk-package-alarms.svg?branch=master)](https://travis-ci.com/apache/openwhisk-package-alarms) |\n| [openwhisk-package-cloudant](https://github.com/apache/openwhisk-package-cloudant) | The Apache OpenWhisk cloudant package enables you to work with a Cloudant/CouchDB database | [![Build Status](https://travis-ci.com/apache/openwhisk-package-cloudant.svg?branch=master)](https://travis-ci.com/apache/openwhisk-package-cloudant) |\n| [openwhisk-package-deploy](https://github.com/apache/openwhisk-package-deploy) | Apache openwhisk | [![Build Status](https://travis-ci.com/apache/openwhisk-package-deploy.svg?branch=master)](https://travis-ci.com/apache/openwhisk-package-deploy) |\n| [openwhisk-package-kafka](https://github.com/apache/openwhisk-package-kafka) | Apache OpenWhisk package for communicating with Kafka or Message Hub | [![Build Status](https://travis-ci.com/apache/openwhisk-package-kafka.svg?branch=master)](https://travis-ci.com/apache/openwhisk-package-kafka) |\n| [openwhisk-package-pushnotifications](https://github.com/apache/openwhisk-package-pushnotifications) | OpenWhisk Package for Bluemix Push Notifications Service | [![Build Status](https://travis-ci.com/apache/openwhisk-package-pushnotifications.svg?branch=master)](https://travis-ci.com/apache/openwhisk-package-pushnotifications) |\n| [openwhisk-pluggable-provider](https://github.com/apache/openwhisk-pluggable-provider) | Apache OpenWhisk pluggable trigger feed event provider  | [![Build Status](https://travis-ci.com/apache/openwhisk-pluggable-provider.svg?branch=master)](https://travis-ci.com/apache/openwhisk-pluggable-provider) |\n\n\n## Samples and Examples\n\n| Module | Description |\n|---\t|---\t|\n| [openwhisk-slackinvite](https://github.com/apache/openwhisk-slackinvite) | Invite for Apache OpenWhisk Team on Slack |\n\n\n## Development Tools\n\n| Module | Description |\n|---\t|---\t|\n| [openwhisk-devtools](https://github.com/apache/openwhisk-devtools) | Development tools for building and deploying Apache OpenWhisk |\n| [openwhisk-intellij-plugin](https://github.com/apache/openwhisk-intellij-plugin) | Intellij plugin for Apache OpenWhisk |\n| [openwhisk-vscode-extension](https://github.com/apache/openwhisk-vscode-extension) | VSCode extension for Apache OpenWhisk |\n| [openwhisk-wskdebug](https://github.com/apache/openwhisk-wskdebug) | Debugging and live development tool for Apache OpenWhisk |\n\n\n## Utilities\n\n| Module | Description |\n|---\t|---\t|\n| [openwhisk-release](https://github.com/apache/openwhisk-release) | Tools and documentation for Apache OpenWhisk Release Managers |\n| [openwhisk-utilities](https://github.com/apache/openwhisk-utilities) | Shared utilities used across Apache OpenWhisk project repositories. |\n\n\n## Others\n\n| Module | Description |\n|---\t|---\t|\n| [openwhisk-test](https://github.com/apache/openwhisk-test) | Test repo. for Apache OpenWhisk client-side tooling. |\n| [openwhisk-website](https://github.com/apache/openwhisk-website) | Apache OpenWhisk website (openwhisk.apache.org) content; built using Jekyll |\n\n\n## Archived\n\n| Module | Description |\n|---\t|---\t|\n| [openwhisk-GitHubSlackBot](https://github.com/apache/openwhisk-GitHubSlackBot) | [DEPRECATED] - Demonstration of integration of GitHub Pull Request management with Slack and using Alarms |\n| [openwhisk-client-python](https://github.com/apache/openwhisk-client-python) | [DEPRECATED] - REST API of OpenWhisk can be used directly from Python |\n| [openwhisk-client-swift](https://github.com/apache/openwhisk-client-swift) | [DEPRECATED] - openwhisk-client-swift is a Swift client SDK for OpenWhisk with support for iOS, WatchOS2, and Darwin CLI apps |\n| [openwhisk-debugger](https://github.com/apache/openwhisk-debugger) | [DEPRECATED] - The OpenWhisk debugger project |\n| [openwhisk-deploy-mesos](https://github.com/apache/openwhisk-deploy-mesos) | Apache OpenWhisk deployment scripts and configuration files for running under Apache Mesos. |\n| [openwhisk-deploy-openshift](https://github.com/apache/openwhisk-deploy-openshift) | [DEPRECATED] - This project can be used to deploy Apache OpenWhisk to the OpenShift platform |\n| [openwhisk-external-resources](https://github.com/apache/openwhisk-external-resources) | ✨ Curated list of awesome OpenWhisk things ✨ |\n| [openwhisk-package-jira](https://github.com/apache/openwhisk-package-jira) | [DEPRECATED] - Interact with JIRA software software development tool used for issue tracking, and project management functions |\n| [openwhisk-package-rss](https://github.com/apache/openwhisk-package-rss) | [DEPRECATED] - RSS feed package |\n| [openwhisk-package-template](https://github.com/apache/openwhisk-package-template) | [DEPRECATED] - This is a template to be use when creating new packages for OpenWhisk |\n| [openwhisk-playground](https://github.com/apache/openwhisk-playground) | [DEPRECATED] - This library provides functionality of executing a snippet of source code as OpenWhisk action for OpenWhisk Xcode Source Editor Extension |\n| [openwhisk-podspecs](https://github.com/apache/openwhisk-podspecs) | [DEPRECATED] - CocoaPods Podspecs repo for openwhisk-client-swift |\n| [openwhisk-runtime-ballerina](https://github.com/apache/openwhisk-runtime-ballerina) | Apache OpenWhisk Runtime Ballerina supports Apache OpenWhisk functions written in Ballerina |\n| [openwhisk-sample-matos](https://github.com/apache/openwhisk-sample-matos) | [DEPRECATED] - sample application with Message Hub and Object Store |\n| [openwhisk-sample-slackbot](https://github.com/apache/openwhisk-sample-slackbot) | [DEPRECATED] - A proof-of-concept Slackbot to invoke OpenWhisk actions. |\n| [openwhisk-selfserve-test](https://github.com/apache/openwhisk-selfserve-test) | [DEPRECATED] - Apache openwhisk |\n| [openwhisk-tutorial](https://github.com/apache/openwhisk-tutorial) | [DEPRECATED] - An interactive learning environment for the Apache OpenWhisk command line |\n| [openwhisk-vscode](https://github.com/apache/openwhisk-vscode) | [DEPRECATED] - Visual Studio Code extension (prototype) for authoring OpenWhisk actions inside the editor. |\n| [openwhisk-workshop](https://github.com/apache/openwhisk-workshop) | [DEPRECATED] - OpenWhisk workshop to help developers learn how to build serverless applications using the platform. |\n| [openwhisk-xcode](https://github.com/apache/openwhisk-xcode) | [DEPRECATED] - Collection of OpenWhisk tools for OS X implemented in Swift 3. |\n\n\n"
  },
  {
    "path": "docs/dev/troubleshooting/build-failures.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Build Failures\n\nThis guide details problems that may occur during the OpenWhisk build process.\n\n## Dependency download failures from JCenter\n\nOccasionally build failures occur when the JCenter repository is experiencing problems. An example of such a failure\nis shown below.\n\n```\nFAILURE: Build failed with an exception.\n* What went wrong:\nA problem occurred configuring root project 'openwhisk'.\n> Could not resolve all files for configuration ':classpath'.\n   > Could not download groovy-all.jar (org.codehaus.groovy:groovy-all:2.4.7)\n      > Could not get resource 'https://jcenter.bintray.com/org/codehaus/groovy/groovy-all/2.4.7/groovy-all-2.4.7.jar'.\n         > Could not GET 'https://jcenter.bintray.com/org/codehaus/groovy/groovy-all/2.4.7/groovy-all-2.4.7.jar'.\n            > Connect to akamai.bintray.com:443 [akamai.bintray.com/23.45.134.89] failed: Connection timed out (Connection timed out)\n```\n\nTo determine if this error is indeed related to JCenter issues, check the JFrog Bintray\n[status page](http://status.bintray.com/).\n"
  },
  {
    "path": "docs/feeds.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Implementing feeds\n\nOpenWhisk supports an open API, where any user can expose an event producer service as a **feed** in a **package**.   This section describes architectural and implementation options for providing your own feed.\n\nThis material is intended for advanced OpenWhisk users who intend to publish their own feeds.  Most OpenWhisk users can safely skip this section.\n\n# Feed Architecture\n\nThere are at least 3 architectural patterns for creating a feed: **Hooks**, **Polling** and **Connections**.\n\n## Hooks\nIn the *Hooks* pattern, we set up a feed using a [webhook](https://en.wikipedia.org/wiki/Webhook) facility exposed by another service.   In this strategy, we configure a webhook on an external service to POST directly to a URL to fire a trigger.  This is by far the easiest and most attractive option for implementing low-frequency feeds.\n\n<!-- The github feed is implemented using webhooks.  Put a link here when we have the open repo ready -->\n\n## Polling\nIn the \"Polling\" pattern, we arrange for an OpenWhisk *action* to poll an endpoint periodically to fetch new data. This pattern is relatively easy to build, but the frequency of events will of course be limited by the polling interval.\n\n## Connections\nIn the \"Connections\"  pattern, we stand up a separate service somewhere that maintains a persistent connection to a feed source.    The connection based implementation might interact with a service endpoint via long polling, or to set up a push notification.\n\n<!-- Our cloudant changes feed is connection based.  Put a link here to\nan open repo -->\n\n<!-- What is the foundation for the Message Hub feed? If it is \"connections\" then lets put a link here as well -->\n\n# Difference between Feed and Trigger\n\nFeeds and triggers are closely related,\nbut technically distinct concepts.\n\n- OpenWhisk processes **events** which flow into the system.\n\n- A **trigger** is technically a name for a class of events.   Each event belongs to exactly one trigger; by analogy, a trigger resembles a *topic* in topic-based pub-sub systems. A **rule** *T -> A* means \"whenever an event from trigger *T* arrives, invoke action *A* with the trigger payload.\n\n- A **feed** is a stream of events which all belong to some trigger *T*. A feed is controlled by a **feed action** which handles creating, deleting, pausing, and resuming the stream of events which comprise a feed. The feed action typically interacts with external services which produce the events, via a REST API that manages notifications.\n\n#  Implementing Feed Actions\n\nThe *feed action* is a normal OpenWhisk *action*, but it should accept the following parameters:\n* **lifecycleEvent**: one of 'CREATE', 'READ', 'UPDATE', 'DELETE', 'PAUSE', or 'UNPAUSE'.\n* **triggerName**: the fully-qualified name of the trigger which contains events produced from this feed.\n* **authKey**: the Basic auth credentials of the OpenWhisk user who owns the trigger just mentioned.\n\nThe feed action can also accept any other parameters it needs to manage the feed.  For example the cloudant changes feed action expects to receive parameters including *'dbname'*, *'username'*, etc.\n\nWhen the user creates a trigger from the CLI with the **--feed** parameter, the system automatically invokes the feed action with the appropriate parameters.\n\nFor example,assume the user has created a `mycloudant` binding for the `cloudant` package with their username and password as bound parameters. When the user issues the following command from the CLI:\n\n`wsk trigger create T --feed mycloudant/changes -p dbName myTable`\n\nthen under the covers the system will do something equivalent to:\n\n`wsk action invoke mycloudant/changes -p lifecycleEvent CREATE -p triggerName T -p authKey <userAuthKey> -p password <password value from mycloudant binding> -p username <username value from mycloudant binding> -p dbName mytype`\n\nThe feed action named *changes* takes these parameters, and is expected to take whatever action is necessary to set up a stream of events from Cloudant, with the appropriate configuration, directed to the trigger *T*.\n\nFor the Cloudant *changes* feed, the action happens to talk directly to a *cloudant trigger* service we've implemented with a connection-based architecture.   We'll discuss the other architectures below.\n\nA similar feed action protocol occurs for `wsk trigger delete`, `wsk trigger update` and `wsk trigger get`.\n\n# Implementing Feeds with Hooks\n\nIt is easy to set up a feed via a hook if the event producer supports a webhook/callback facility.\n\nWith this method there is _no need_ to stand up any persistent service outside of OpenWhisk.  All feed management happens naturally though stateless OpenWhisk *feed actions*, which negotiate directly with a third part webhook API.\n\nWhen invoked with `CREATE`, the feed action simply installs a webhook for some other service, asking the remote service to POST notifications to the appropriate `fireTrigger` URL in OpenWhisk.\n\nThe webhook should be directed to send notifications to a URL such as:\n\n`POST /namespaces/{namespace}/triggers/{triggerName}`\n\nThe form with the POST request will be interpreted as a JSON document defining parameters on the trigger event. OpenWhisk rules pass these trigger parameters to any actions to fire as a result of the event.\n\n# Implementing Feeds with Polling\n\nIt is possible to set up an OpenWhisk *action* to poll a feed source entirely within OpenWhisk, without the need to stand up any persistent connections or external service.\n\nFor feeds where a webhook is not available, but do not need high-volume or low latency response times, polling is an attractive option.\n\nTo set up a polling-based feed, the feed action takes the following steps when called for `CREATE`:\n\n1.   The feed action sets up a periodic trigger (*T*) with the desired frequency, using the `whisk.system/alarms` feed.\n2.   The feed developer creates a `pollMyService` action which simply polls the remote service and returns any new events.\n3.  The feed action sets up a *rule* *T -> pollMyService*.\n\nThis procedure implements a polling-based trigger entirely using OpenWhisk actions, without any need for a separate service.\n\n# Implementing Feeds via Connections\n\nThe previous 2 architectural choices are simple and easy to implement. However, if you want a high-performance feed, there is no substitute for persistent connections and long-polling or similar techniques.\n\nSince OpenWhisk actions must be short-running,  an action cannot maintain a persistent connection to a third party . Instead, we must\nstand up a separate service (outside of OpenWhisk) that runs all the time.   We call these *provider services*.  A provider service can maintain connections to third party event sources that support long polling or other connection-based notifications.\n\nThe provider service should provide a REST API that allows the OpenWhisk *feed action* to control the feed.   The provider service acts as a proxy between the event provider and OpenWhisk -- when it receives events from the third party, it sends them on to OpenWhisk by firing a trigger.\n\nThe Cloudant *changes* feed is the canonical example -- it stands up a `cloudanttrigger` service which mediates between Cloudant notifications over a persistent connection, and OpenWhisk triggers.\n<!-- TODO: add a reference to the open source implementation -->\n\nThe *alarm* feed is implemented with a similar pattern.\n\nThe connection-based architecture is the highest performance option, but imposes more overhead on operations compared to the polling and hook architectures.\n\n"
  },
  {
    "path": "docs/images/OpenWhisk_flow_of_processing.draw.io.xml",
    "content": "<mxfile userAgent=\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:60.0) Gecko/20100101 Firefox/60.0\" version=\"8.6.2\" editor=\"www.draw.io\" type=\"device\"><diagram id=\"02ac4891-0b59-94ec-de01-a3d4adb2037f\" name=\"Page-1\">7X1rV+LY1vWv6Y9Pj9yw9CMSxNgkFBLE8A0CHQggvoKG5Ne/c669w0XUtrqqurrPoM7wBJLs29rrvuamf7Nri03jafA48Zej8fw3yxhtfrPd3yzr3DnH//NGrm58sfSN5Gk6UrfM3Y3OtBjrm4a++zwdjVcHL66Xy/l6+nh4M14+PIzj9cG9wdPTMjt87c/l/HDUx0EyPrrRiQfz47u96Wg90cuqGLv71+NpMilHNg39ZDiIZ8nT8vlBj/ewfBirJ4tB2Y1+dTUZjJbZ3i27/ptde1ou1+rTYlMbz0nVkmKq3dU7T7dTfho/rD/T4MzW81jn5bLHI1BBf5WZ25eyljGbGPg2WS/m+GjiYzper3O9c4Pn9RK3lk/ryTJZPgzmzeXyUb/35/JhrV8zz/X32nK+fJIh7Sv5h/vH09crWi2fn2I9wYrmhcFTMtZv6VVw6nvN9JIb4+VivH7K8cLTeD5YT18ON3ig+STZvrcjGD5omr1DP+e76DfeTNf3vP07VqW+RuWjB0xFPTMr5fdIvhvb71/HT1PMefykuz7tR+UH7ceXD/bj/LQfn90PPZeXwfxZd1rD0p6W8zlo9HqndttCmmST6XrceRzIsjJYmMOt+izJ5D09hrSbzud775lXjllxPiLty/hpPd58SDb91LbPf9cL1ibPLPkx2xkQ84vW/ZM943FmfD+tLeeI2Eckpll6/PxSt8Z1MCx7MD4mwdkhAaxysfsEsL+UZNongfkjaGD/NQlgch/5cTRYD1br5dP4E7x2yDOGcelUjTd5ZrsHf800+xQxv/wkelhnnyfIdCEO0SXnPoUb1BwMx/Ovy9V0PV0+4PlwuV4vF3hhzgeXWydnjzJ/yr+9PqrzacK2a6q6y8HqUTlqf043lPNLGbJa3jXKO3p3frOr6qt1tXpJfrMuN9gRq/b1OrD6+eVj3zWmd9fzbJh7ybhhroYP/oW3mBij6+pZM7+wR3b8PCr856F989AsvMx3qy+x3X/wppfOsLd5jotHZ9iYPw8KYzq4vjVid/nSRKtRXrH9vPISL+IXP5xVWp0L9R7GGS3m85Fx8zLG2H6tmnlu3Qxyxwym1U1QeM9+GE2/pptZv9cvcC1G1/NVP3SeI2tTNMOrL8PeVTEoHs14kS09y5zE1uqxZRv6+c2X/mK+GrrL1E/9TT/1st2zYPusv+gvcP+8aQfGuLeZY5x1dB88eY357GvnRn3GGr92vAzXCq7rlusXfs0x8d0e9G6NAecfdu1m2rVwb9nvzR8G1+0LL/XzoIh4T/q561UeR9eg68Nd0b+/CePGVdrvXjzf3Qdzbwp//vJr7aJcrTG4v8VqK5fxw+10aF3l/Vo1x3N93znHrqXDxlURg+JqVXVSK+/3AsNrBCuMWGAF6fDaX/tTUNuaz0aNBLMqqewlg8bdY9+aGFjVluLNwjlvWjFnc305QYsEvU9im/wQrOLcKLAe0ibww2rWTOtW021vmmk7H7rVdTOt2lHYfQ7S2SroOFark2VNt7rya0aBdzbN0HsOwoTP7KCTmc00NtVzvBe2n0FZPO8v/UYdz6JK01V9yXP070+drFXLQOvYHrKdvBdv5L2iXr63wgrBd1k+rDlZkGfZ23PK8mYYo93NIAwjp4n9wlrYnz1wLyc+24TxanuVvuurFvY+qGUG14P3UrSz1B/mIevBPDrOJuhxfZHdDP1nP52gP28lf6of0KpeaUr/pHyyu+pxSCO/Y2Ct7QLr2DQ7WMuivgENNlhbeVVr6WQV1V9911ca7a5ln7ljYO5CM7VGPm+vtlc9t6DmGKDzJphiDr0Mc41Bn7Y1qBnPvjuZvNP/2pc9jLlPTrDI1qR3MM2w9x74QvZU0XO6nbfau9CX+9ibDdpU9P6AX4QOZmAJLcFrpGVfj++vtle9PvaB7xzfwLxJC8x3Tn4ivfJA0Y3XcnzsI/uJwB83K9AYe3iz9K8j8Ew9x7wL9Kuv2zlhryPyCtZ6Y5V96/d2NNzbP3kmf7oP7GcLz0I3xvMZ7pN3blKMX/jYoyb5tObYrZoB+nBPb6VfRcMu6FNeyzkloGF7hX0rWtegU9o1352/Gz0LH+YYa5Fko7TkA2+1vZb0JA/kTo4+sx2/HNEd+x6DFni/sdfflrfjvf6M4/5C3V+420fw3UbkprjpQYdCRm+1PGr+CXc8R3n0c4P76GidsBG+q2Xltdxr6Bgvp87ayXd7tb3uybfqr725S33Iladp6TuKlryWdActO9RtfoE+S17YkI/1dW/sOvY62PKiluF8T4ZJm4LvfiCb5Z4UJZ0DV8m7XEveo24QufPxHugX3gyGooeSVYu8hX2KKBuQR6w3V/JYVzoRY4FuWGMivMDn/nSrx/mcvO30FkaO6wbzzfEO1z+hrLZEv3SdrV6HzAZulzJp+RZlkrrvRuv4WPTczgZgrVPKTBeyMUMfMXVHRfR3ESm7sZuLIXyNMX2xJV1lL8I9e0F91+GaIlPZEunD2utDdDvlBmvkfAplS0pexDtT4SHaL1Ov0Vbj0e6Ua6QuoG27oy0RGwJ6Zn4vUbYH/Um7cq9pP6Crg7RvKToczZ02xAo6WCPv5zLebl36mZ6PpWxptG9L1Tuypq6l+o627Skb1KflevQYJU02qi/hA7O1pW25Vs67uwoa/KzHlz2PjD16qPnPHzGWYYs9tpJ37LChaaH2Ruayv8cy3tYncNQ+Jq9okSk6gV/Zx45fM72WUn8qGut17faiXJPo3UyPs12LGuM+0vN/Z4yw/XZ7WVt3HVwtxbuC32cMaq+9N/hoDfiMM+hH6o60Snu0hBwXvh1Bj+DaExsPn6UOv6ta6bi+E7oe+czwKQMW2tFHmEI/1BzHnyX0Ux3s07olMmLAX4CsX0fqWk82wd2n5mRx/8QfoL4r7rwgxZrcvkUd3XKvUrXGGW0K/DMD6/fEPoJW1M8V8L0hNpN2SulCypPdFLtL+0EdESv/waUMGPTHsG8R7632nq9kbUJ7+m4x5RPebIb3Z4Uac67nNVL0Aw8qvd+WcWBLJ1jL3v7SRxA9mAUGdXSXftJOltwI9G7T5hqBVVd+ljtDP/uyShsQ50pnJqIzW7U9HVP41EU25m0r3aDnXsoj9HlL9Bn5hu23bbUe7g90jPAXXnlQdEktg7PpiFcR2aMpe04cfDfxmd56JSpkVAcjWdwVX9pV6Ylxx4xR2gYF+2pnp9o7T+ul1t2IVi8CCxwHzc9nVxZ3plVPHNzLAzvKKTW+kdif5DIHXCpcprnfa5Hb7t/m/gEpGM5X+E4PaaOiBfnMtebwVEFpz4AVgTdEzxBeWFrfjFJynmd03KRCr7Y39V4QN2XR/e3yKG5ClMrIOAwhYYrLSglYiuXH+j+QgM0nJMA6lIDoLyQg2klA/g0SYH6DBOTZGxIQMyKAtecYoBveb7nvcH/YZT+boPMu95tq3t2/w/21i2K0YHT6vSmj8unrNOPZcZLt3D7OKF38gISS84kM289OMpqm/YoAzltpxos38qzm+Q+ggWn9bRpsyff3ifCPLPHir1f4Tdn6wVNcJuvPjhOqo8H4/M8Y91frp+VsvPfkLD4fD/98Oz1vvUPKv0GyH5FqPc60eg8vWM2PqHN8on7xYf1jr07yXYQsnxoH0me+IXw/q8hRjvU/mNAuk9Kz6CAhfclksUpiW7dp07pb9XvmfPhwWzSLOgL586l3PVkPG5WitQhSuAHL0fVt1pqev0T2zRwuwuNocZcOLXM9tCpFc3GR9/OL5xjh8bbdw82sn/5VEvw886fnaGXm/Ua0ju3586hx5TR7lcLbJeDPmGDfnwN6spsPsR4X7d1q1rS53n8uab+dm3UxG9xfvvQbs/0xi6F1+xg38KxTKYb2XR5ZdwuO3+9Unvv37Zfbq9s65ox284zvNuFy9q9vZ3tzehwu1kVkXWX98Gheu2cdNb/h4mrdvw+yqBfM4/xi7zkLDHd5bM1fhtgPJmT3E/ZB2obbOZkPeqPlaPtdkv50MHK8uzePR6t/f1MMehfPLATAzdp4197GL2K4RF6yR4uzfuMilTlh7vw8sO7yZm8OnnGeNe2tQe/Obi8uVFHBrSZ+4fFv+nUapeNG/YtXq57v9yn99W6lnQf3v2+RBzcvMdblWSxh4H9TL4nIFw3S/GI1tL0Lb+pbCKr5h7XdgF6383hxQVo9jq5nLAXg/sV0AJ4Gj70M2bZXSTl3hAJ4NspJL4yFdlfPkdVlGxZCHka9+ax//1Yb8ImNEe8v560HmesLCx2D+9uKLqLs0xVzGc1jazIBrWw8f0137FdlNrTj9V75wgnC7qtxRmhz+zLKzb3iSz2Dq17u6XMEOe9f9x/799gzt276Ne7b5GVkcY14f9GfDK9ZiDkY/2xoXawxLyZA8qCoom1yOPYi4D4aTKy3Qq8C9/z1c66bLnXhp34WuLP35s5+LPIT1vwUW8EkbnTPsO5n0qZ5z/3yjPfoM+xdOYOe+The3M1Yznn1Xjq6v8n7vYrR7G0m495dvuNzutU308CJwlXiuRtI0y1248qIOqosOOzdGVHvFkFn3fBSZ+HZk0krrybCpYvbRWt+U789vM9+0qh4vIfkP3nX1IiPE3CnSUnzXr+Lv6+N27S16INDzcnIhZT3rqAxxNV/osZhYQz9UZOso/v2/oxr2xH5hjEGTfWIOfTCC9adHuqR1zbgNh325tKuaQbGoLdZdXom+LbLkPXtmT7ADjQ286PnbvYSWf7ZnXWRDw6fnTdtmVPnth4d0Dt72aPv4U5IKVDzr3wOpt6rlX9FwAot0VbBq7yJ4HqedRlog1P3NIp6XycPZF87VaZSc4/lMjdKJECkNqpluZ9X1yydQFIqPnaS9yPeY4BYJBsJ0KhZwqgS1OT+Bu8aLbfLtF8BbeYgGE6o3RHMmwgA7abrJao81d34YaTLJZAKBOgYDwF8tYAWZqo5R7CZtdzY5LwQwGYIohE0+ggyZxbve/UsCVgKCnm/vvFrl6nHMgk+B26VJQ8EqHULc00kDR/WKfUMzk0/9SpBXmXwXTAIZQpb+mT7tG0EhfTJdCSlPWESA+uyfaZPEWi3XM/yp1UG0zIXSYSkiR10Ll3PjVgewHgzpsU3LGdiHliLn6Ad51sJmNKWlF1145NeYXsjpW+mGN2kkCSDW09geRFsR9AaM0lsSNkxhRXvVHOWW4K0a/kM1FMJuvk5Yam7mUK7QVM13agC+hoYE3sMDUWahjOmJy3StAU6BSHWl3YRbHeNZlr20ebeZn7IPWsz6VK0OC+Om7LM5G2C1F8F2Fus3wY9LfTPZI+p9ol9+aSHz3HJT1gfxzVatSrWhjmTrmHbJE/0WIimBn4Q3txQM+9rQFiz5yEtVOMCljQw44f+ZHR9l4vFqXkvBzwuErh5jK+kr1eSBtu4iPfk8msjViMvaBOwVyyTX99Mhg+BaBoFALjNB/fJEhwObpoZWK2zS08c3FPFaTfCChMLO8aUiAMp2gT57FAS3TLddGAn4MtU5pF9+xJPTeprC+9AV9Hmwq5ea4BC52ZBHdWa+kUrvJ0F4dZvkT6V/eGaDPgUkL6iajK1x3QQ7CakrV0JplWm8HLuNNciGoAFkDAqMPcKOYy7DlsliVav4UMrOBVIQcV3WRyktMIvBIe2OpmS/qIL7q2DDmw7gwaI7KAmz2gXM3Ih+mUiFRIEupTtwi64xqcWoV9lw6cqglyeQTq9jLQE5wnAICCdVTuM1cZ8wZlK+5itMMoC9cyG1ENCEhNSyu+YMzk9XkmKroiwR13HF20EDg4TrKNrN6WwFectcCWkKPdqBsEN6CcyQIO1aLUwNnzXt0EvSFwbn2dFU4ANVUhp3aa2YgIe9LbJ5ZIydSnB8QbSsGI7zrslz5hqrRdcr9wPZW2ZrDOENijq1LgbFvA4B1WwqVqYL9Y+o1Yz5D0Wcpn+S9s2pNSAxEEztB2sw5GiVBrhc6xpCKku2ptyXzCG5ReJ40mBLgEN61xPRjADaOaI9mVKN5xBPpis5pjQEmHseI2MPIF5gkcK8gS1fhvzSExFO/g6Yb0iRaW0W4GlAB8myopQ44b0p0Sjamvh50EuBTussS3pZWhHrA8aA/eloFmAjq6kO2GVYmirpPCE/hHew7NcUsQY1wOv1BNJDaeUx4jFPRY9oA3Bw7BgaJdLmhp0A7/o9GodNPDBmyyQCb+DV1kIrEoBJQhnNoto5CPIecE9akphuE5+rFAHYJ45+BsWq215dchNqHgF/er0aIL9Ba0hQ7Sm1P5SuGcqXmhShzadPSvr17UV2CXC2qvQtLAinFcoY5nKAs9Ai4T3bfA02niGWCSMB15xYFmZFkbsN8N3j9YZfAteTf1yD0GvGfYmUjyJ+6Az9gMxCfYC8wedq472DNZ7eoKyfqRHVJkA9HfhXdQ4Niyqm5Dnjp63OlWTPA1PQYq6fM7SA2gJvdktXvmmNaWxP9DQsONtUAS2eFvKPLhHSQWV2jaT1Io7ZhXsvhXUZqUWffAlaqBfVV1LwQK+ErTx6o9asO9bPR75se7mYdfHse9FL7CE2sGvYyRkMKqKLfq9BuzfLSOpYoj++50ko0YSGE0aHY+l+9MFlQNPf2gh0u4FBiLAdYQIfLS4MjD/uY6+NIwOcyjhce7lJLLaLCDJ3NSaJRITCBxsN/xBzCMEF3VIQ9joELsXtumj0L+BVLAwVGVpuMLn3FFffEr9veiyVJ6Mp6/s9c57/xEWMfcbPqKc6NAipnsWMfQ4f0g5tCH8OBbHqC2pHSHh4LqYUALhRL02WIo2tczh8+vlm9HB9+5JbN/mQ2s937PuX8p7zfvRY//6dtli6RVarMXMzGvrL5H+dq0WoRO+FJE88jrWOsvE0hKGA6tB2CKsSe41DFoUyAKi6052+EyklpCnLjRVTGueIHagLEF7JSydHzyjlhA4lVixdsWfXq490eawwoQFHDx7Zy45NKOLuU8/NZdcPIfQ3/z1XN7lPzO2ugeASMx3fgjxrGY3iM7Hyu9MFGyVGbEN9np1Nri+mTPz59nie+bB1q+9K/qw7n8p7y4LdNBF09nbkWft4uEtnTIifHQvk1XOcy9jkvm1LuRlpuUm0dfJin52azGhzFx40+DBT6slP5EeBdf3rr7VBT5C9KDRaREtKT6G8KaxB390jmn91hq+XoM+ncuDzI2ar+L3vb0oM0glHFdnLx+X8eJugXtpH/xb6jHShVk171rRub+4yIfYO72eFdYx/6NmVGgRA0KeFOwnf3PeP3WO7b+aoyHQIXhZv26O0cdzlBzCO3Nj5PUqG6J15KE8MPdzffM4riX0vOl9MlLYQcMO7lGfwMty6VlTxuGRwTsDDxp/dL4pfkygXyvDRpcZwMdhIzvzirodpXfz6DAuRaThJRJb1kzRtSIjAj3f5WBa4kdo/UsAQNqFHawbZUyMKMVodVSuAh4aPd/SD4HOYhZRF8NdnwX+NaIKeEsxPPW4Qhsknm/YziRCSgXyDG+NdpZgBMrdjJ5kEaQxdH03aQosM4Ln14WMEmiA+/DgVZTgQ0fC8y0I9IC35vr0ipnptOmJUpZb4u3XbU/BNiVaUVBjeLSwi7CbmMeMY1O/ElpIb89ABJSENeh7eK9qzjEjvaSdI1KCT4M28IK7qxZ5hu+HjFx8QyA2U2lHrz0Z1iTXIR64ikDh6WPtLUZy8HQZmTZDjj9jBAFasj8P649M7eFznhuJGlVkgWhMcl3imWIHchXxIFJJu+aRfSj9kle8P+xdgXfnzh8SCSbQH11nD85zeC9nzqzKaMukrcOcYLfpYbcfX2dpv8l3q5nbzHor5ZgxKzy0mwc+ATztz/mzW3+P+5BUJCsQxgkj2633H8avbdO/W5atttHv3S78zoEsG8z0fFqWZ/SlKKPgq8K3JHfHqNhF7JAz5zdjhAbfKTZUzq9NoA5zXRWfoBnRAQKFFnipj2jIY4YoRfTvRipPSPlFFMaoC3JjCAQwTBT/C7yW95xcsiNTgdXTB8/8/D/Mr25iKn6tv+LX9jfyq8c4iXlH5oeZ000Uj0HXgOZKR7Bql1CXVZTfj+g1hK6BXmEeEzEg35WcpkS/BbNDCf1KnXtmXjdWeWLolRYzUGFbgdWY8XHBodTdzCJIBJ1oUBqiZ9en7rYFiknQEyHfjEc7kglBXBUpfU7opot5TFUWBO/SdqwEEp3WaR8S0W9Kl6m11f1E5o+5+uRDt57xyEbAbFSnagj8mRkl6maB2HfBQ1XHc9uGzI354wK+m0T5r3Q21ow+3tLF9NmZRaQNwvfLszcidV1p+B7pHS4unvvhGlx08zKwurDMfiVotM3A3d4r64JnzAn6zLeKJdg9p2cDDp3E7mM+tO+e+7VDTUAvXVdDNmItQlgBDZJntr7FSJj5T4LXEYUI8DSFBihUdSGQfAotb12yDQE4C3OAhMewqu0cXoCtLEy3AgvL7MN7GvS7ovi+ezvvN6LNYRTfdvakZKOkA16hhuH5/OzOmJMTLRBIlIXoXuVNDcnfcceFw2eGrEvA4dWMlrSlcpLqc1hPVE4TfcghJuHuTKo7RaxyS26XeT/mfV49Y+WmSvpUJEes6EWPwlZRHOfiZWUOV95NfYsHBdX4UVFKkOozsry99Yj12q4h0XlMT6JEVTWSsZ2mu9/3z+LqLRcfcqJUpDUn5pIhxWwxK/pzrLk5rO0oXSa601FHyZi9g50p9P2Q2UzJtfAzKeHoTDZ9SMld8DgLKMOMpNaTdUjOjLkNUhr2C3GCHOlymA2v0PZJxjudIQ6XTCA/o319ozL6ddovU2f4WO+DBMbar5LsYdaS7L4He59s/LzM/Pv8LhlI+IuGmhvahvBLpd8qs79qvDDGPKqsSx7epz/r4ntNcTA4tOKnbZsZXMlIYycVUNVhnarCcRQ9YurBQo4G8TMlI2wrGqo+LHKNcG2BHYOvrjPjOWnKet7xs10/vsqwQyq4h56qGOBdrNfevsu2av3M7LJWuNr2mfqaZlXmb7T/RZ6owp+VvcmkGiF7SB8D+r6YKX6A7wYaHufafmLVCdK0i3v2/QcBI3dZk82xp9C2DqULUl0luJjxAPaaa4E2FYA0NEAIvyqdJbA5z6wuIEaQvAI0J7Qr9pr95NS06If5JB5RZNwQdh0lH8zOM8vfZc1Z6s2SuZ9KFaGATElFTI5MUMakUhDzeAE0f7XSVFUa+KczS2ycVAmI/JDsuSNWxp1RrhzR6vD9lCZkNYf2vq5iEvAoeEnLAXgP/g73RapQ4GdVO5a5Gly/r6ocoFvX4BET5Ru0KVuFGot83y4kU640JH3EitSqEfO0mOOqKVA/eCCX42CsmrBihHfRRqojrH6p+v1uTtgHOYoGejJeYjUQth68F0J/iCWrck+xh9yjxFRVxTatHzQna+6xQVrjHdZ3Wd2wpAKUVm2pFQtfYo145slezhiHbV7v+fu8c8DL+oCy1szngoErHKIMfwwm0znEZL4BCTett6Cv1o8Ahf8bfnmixCx/AAm3DOsn4aWd4x86+TQk/D38/9+ChP/EJX7ipyR+NSZ8uw3/Eky48+W/Cgr/Bkr+elB4+UtzJ1D4CRR+AoWfQOEnUPgJFH4ChZ9A4SdQ+AkUfgKFn0DhJ1D4CRR+AoWfQOEnUPgJFH4ChZ9A4SdQ+AkUfgKFn0DhJ1D4CRR+AoX/60C2J1D4CRT+X+LXEyj8BAo/gcJPoPATKPwECj+Bwk+g8BMo/AQK/xmg8G/HZP5SUPgnfkP6Z4PCKyW4+pf8TnjlE79V/B4o/OK7SfDPLNH86yX+alD4dhv+JaDwyvHPx/9HQOHfQMlfDwqvfMN/C/QECj+Bwk+g8BMo/AQKP4HCT6DwEyj8BAo/gcJPoPATKPwECj+Bwk+g8BMo/AQKP4HCT6DwEyj8BAo/gcJPoPATKPwECj+Bwk+g8BMo/AQKP4HCT6DwEyj8BAo/gcJPoPATKPwECj+Bwk+g8BMo/B8EhX87JvNXgsIrZ0cYzPEoGZew1YflA2GYO9ircQhxHW+m63ve/r2iv0XlkwfMZO8Rv0a6g3S8Xuca2Tp4Xi9xa/m0niyT5cNg3lwSn7kD0O4BYN8Dyr4LWF8tn59ivSyNPl0PnpJxCerWi+eKP9yop/F8sJ6+jA86/y6yf/lBZDf36W78flF+/Tp+mmJW4yfd+L+wHRe/bDfOf9BunP8P7UZ5kuAXbMe/4KCKbZ//XjlQy5XKsVo+P/85UHnrE1D5zxw+eJ89DPl3fCjBMC6dqnHA7ebbZu69AzEfH+L4WYcLrOMf9v9fOVygAmy7/8DkwTcC74/B3yGvUealzrkGqJ3rkBHjNhfBKs6NzdgqE5r9RT9dX3ytXegUgANnCKH/g4+3v14rYOrX2VU+aty8RPc3MwS4cJD0/XQHUUD4KS3KgHYbiLLwnw4bV0VcGNPI2hRwWMtEKBwxh1AaQ4KOhkBfCuVk+zqZWF+LY90x4Lzd+HC+nwXQaUZFi8FlT9oYqk1724YOJ0Ebd1I0Scwo1H3SgS3BHAI+iAxxoLvoE0EAHNLCN6NcnOXe0XyYhBNnlk7zXYoQH8Hq230be20Dwn2cIDdyQpyaWI928DlPE20cJoPlfgf3a/oqxU9/hXefJR2EwKbVhbPMawP3wpkEE5LwLhI6+4TXsUiUE57nu3OPCUSMj0AwIQB15TeO6LXRSXRCb+yh2yWcZuXXCa50CFUrglmCYNqxhwKNQnCKPWi5Iz9goIAg008RsDGAanBsFm0JfbpaEtozVDC4DQtKCCDWDHAk5TJLihaTEj0pqqDN5YQBg8xrWoJthIZC1+A+YiHJ6rhRoffUjlQiUyVAOwdtLElyhndCk4AAZELLFu/wl+yb2i/dd+UdfiGNONeeXwS+AHCZ/mHyuZhf6nf1GFVNW0I3VZ+aXzQvttW6alt+EfpL3yloyyIYxy9m2Lf+RPqeSt/5tm/yLdOKM2PjK4Ay3r9cBUwt5gbBKG/wJtOK2McQ8ydPMMhPRyvZn7S9lrbHdCKPrNG+CN0Ziw4ssmQs6gitXR9j6Tkerj+TYLKUM+kvVuveAqqEXzdNWUdWIexMZNDKOH+Cjp/f2DfFs7mB+dQzCaLZpmGsBZQtIPGrY35SMin7HdxF1D2boJ5Yvh0R0la8yU8dzU8q8GeqVQeLojEX/PS91lM/td5wP96KCn+EMS37+OFB4S/xbd90bn+Vb3v2ifO5P9u3Pbdfubam/QZz2T8r5WD9fff+7Ptp8M8s8fPOKP2/1Xr5NP7keeJ9qXjHdT+mm/X3nPWfR5//3f8+lHjrYeOq0r+/+ZNAAHW4zZQjgfCE/eh+Po+n1c3uCOoHx2EJelSWZOrJccJRbWgLoC7xa1WV0E+jyue8eV3gvND38x/m5ecsHWpPfxr1gqe+ffMy6lVmMQtVCsg0iW1GLWpGB0BHFoin+0Ayz5IEbLotILDfcNDgMVvDOY4XvG28ELjwhwi9LwjChM/B0qCUB7UvF86e5RiAXPtL+Eimb9UFbESfWl9L204Qrg2/XF3RL/3y7XEcAa7uPxO/pbzuxRAxfUKPRRblR9TtZtpmIl1fS1+w7vAIjwKVVivtsO7Ive1zj8ULOepDkGowfwxUn3LAi4es8o4bC9jgnTEyBZCmf1ZnYl31T38evt7Or+aVIK3Z6pB+BCmXtItUG/pr5bV8Jxd6ZB3XK+Q4jCH0NTR9jS194XcLmCCVcawol1Iv/X9DgGShtxbf2Y3KfdPf+4Mw9Gw5oNbJTMYhA8YH0oZ96auaj2qbcp48CpDIcznWUV71ezI2Y6pi3uNxHhYHo8M90IBz7qe6YtyU62+VsYvrVTSNLfW5C/qOei05QBekBH41BSjNKwHfiNEKtA/baznsxjXSpw8DrhH9sf3t0q/7cnyDxzAU/RAzpSyCVFfsuxXe8DAf2t2kmCdj34ovx1O4x+yDoNnZKqBPbyVZW/jD1/zhV/b23ZF4jAcxQV/hI7e+A1KBTgIGMiNbCj8zOSiYa/6XuFe/y7jdlBh8Ie8Y+h0jOIjfOL/LwTDUvC00maSMQQQC0SEQ0GABDuuRK2JYg7GniRjRZvzWqvsEK/GPtFF+uwKVqz1IpbjD4lmO/bqUYrMUnFW8Q/hAUJCHoS/SiMWi1fZa8nTqPQuoKuXeRArcqN8L9t6T+ag4PPcFOH+r+sS8eKRoR0fyBucWmeB7ysBWNzEX0FKgHzsQMBbpKrkQK7iSgwBbCAdtTdeaEBggIKboPijkUO61v/Y11CQMEVvJ/hOEOMspZ3J4siP6wGbRHLHaWuSGxV4B990WgeRVJDbOtOxmPMAr3+98h7JNcBPeKT9LcRXv2BJ3sm1K3vZWiB2fd99vsN9VDQSdpFIMFTgLC4azXMClkAv0VcFeIwZ2bImDBXgpRUYTvFDe122oz2KjW0hhkLkDHgDlc7EJ0BkuQUl+2rUVf1N+2nIog4dNEEdqGkXQ15OBgmIcZKvmwZIlbGVt3qN0PZcoV1bK2c5hYUYWD0a23Hna5Io4s44hWScBgIQRI2edhRJpNLVUmrtIPi4zPE6r4VMCTUWRmEeIBb4FSqGNZ5QHxEZTOc4DizBj6da+Sz2Tc4sKanFCHpmpUhZPIIFTJaHyvScSmylrJ9dSqsmxNrMnAtQJuYPJ7rqzAJwPj1vbQ+EM9Twgd5Db3VGP8Ahqnt70XUtffGDpYeGZ0YDlonVLPVuti9q7zUNdLB1vlCSJJihIY/negPWsSVbMEMtXSi612dSgJhYpFwtSXrfr4vPbFeEUzHL5RV/2VMrXst66kuScEov7GMtXElFeS89C7VU6o/TDGtAKGdTwBg+sBWLxYBlrwuVG655H0SNmRskbPLSJ53cT0kAktryWGo3jY91RIfxVZi2YfXMkGwiNJOug1swJwa7T0skPjaj+ZztNFfpr9d3PdXt9LZ+zXO/xHaNdCFR1Ixk1LflyTCZNmGkpeBy+JZDVw737gAeMj3iAR9EDWvQ0Ei0/cLmuO/CWzwwdj7PzKJK+br0sQjPBw7cpaGtprV2hzJJfm9IHs0Q8YnJnKUj8fCnvzHlEn15DkPoKHm8F6kBorjQp2lP+9GfxHsTjAC22MiTtHAVtaFvMKHIewUIOc1I/2P5DZIoXZb6r7df84Y5XXnV5hA5eYVt+QISZRVpDZsHlWJOyQuZeNlRpdckutk14bAryD64MsGY/HIl3Ix6a/EyDyC+th/xsgv5OC1X4XT+XbDJ/YkBbGoxvSjs5rkO5ejWvO3qw/LESer1KEw9qhhxAveNBTx7KliP6zB77oqOUlwo5oDczlczehrAQtM+Ez0THlJ6mYahjTpLFpwcgljxIbz/U8NEHGt6jtNvMY0p+W8UYBHGtttfST5qKlncUx0Wb3bX0AWaEKIqkt9xRqjUAqEmJrNrKpsFXlJnTP6D/2kccQVvVLUapHPKoDMRadAtIO38kQuV/Zde6cqzYVzGJFdCXovWp1xXn5ll53frjLdGgzNm3mXe1egsDfqn/jLZyfLnVS6i5zKiQuYtE+blIvM7t82cIWD/gj3lI/QBrSSC1/kp2fLr1PSix+UCARomhdzsPzMc1JUOAxL1k3bqjxHHHrpatns+DIEqSQ/9Zg7HkByzkwCjnQtCz7a+VZNcFQNfkMXORQubhbz7c+fijnTdKj0E8zK78hAbtSnnder6B0vWQoYnoaB643l5LHS3A1noeulyV6LDNsEY5rBcKYnU1EQ4jZFpfd1EsV9JVh6sXvvKUcvHmDJElOYIYrZU9hGdHni/qSt5VxcOUCIU/wSLRX1fZHHq5i7qpM/DmQSZedkzBv+App/RjaOdU1SGmF2YIbI7eMekDfSZ2Qrw5ennK1wENtf/Q5vMKK1HQt2vVBjQVb4xe2taf2NpC+jXivZZcK/OvqypAEav50+aksap2lBwhc/R1hF3f8H1wtdIF7t2lL/A5b7W9br1+2uL61r4oO5VJbfVrwtTSOxCwb0i3lXAD4zXa4BgEdnbxk7L9x7m3b8n2/5qc/r8ppf8xhG5HjPru7gfFk3egQT+D1NtCzQFqqfLZTbg43gTrR2+CNK0+PQ3yvRcel9OH9Wqv56+8sVfgOARUVrQwXX3udfvM/vB1s/wt2Lffxwc13x3zbBf+KX768hb452wOCl+Opi8HjHb2/56Xa73B/7eSHa/iBVBoI5tYPiftdMp7d3M+/nO9/8pZoq8y1Opx8PA3xzrsiO//kI7endFwm/P/v1hxOftbPw0eViUHkwKfHe93PNR/amAm+mXsw/ngtlrb0e133pbdK+++0hiwE+tDRXD4i7paC++XfPStcmNjLJQAw6Mix2I6Gs3fKy8d6qE9xUHheh80dvDLtp+uVL5r/r5UXklsqUj2zd8bxSjn280fvj4tud876QQlJv5yNOYb/x8=</diagram></mxfile>"
  },
  {
    "path": "docs/images/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis folder contains approved OpenWhisk logo images bearing the Trademark (TM) symbol that indicates that the OpenWhisk project has been granted (or has filed for) a trademark for the OpenWhisk name and/or logo in various geographies.\n"
  },
  {
    "path": "docs/intra-concurrency.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Intra Instance Concurrency\n\nConcurrency within a container instance of an actions can improve container reuse, and may be beneficial in cases where:\n\n* your action can tolerate multiple activations being processed at once\n* you can rely on external log collection (e.g. via docker log drivers or some decoupled collection process like fluentd)\n\n## Enabling\n\nConcurrent activation processing within the same action container can be enabled by:\n\n* enable the pekko http client at invoker config\n  * e.g. CONFIG_whisk_containerPool_pekkoClient=true\n* use a kind that supports concurrency (**currently only the nodejs family / language**)\n* enable concurrency at runtime container env (nodejs container only allows concurrency when started with an env var __OW_ALLOW_CONCURRENT=true)\n  * e.g. CONFIG_whisk_containerFactory_containerArgs_extraArgs_env_0=\"__OW_ALLOW_CONCURRENT=true\"\n* disable log collection at invoker\n  * e.g. CONFIG_whisk_spi_LogStoreProvider=\"org.apache.openwhisk.core.containerpool.logging.LogDriverLogStoreProvider\"\n* (optional) enable alternate log retrieve at controller\n  * If you want OW api /activations/\\<id\\>/logs to return logs, you need to have an alternate log collection mechanism for action containers\n  * e.g. CONFIG_whisk_spi_LogStoreProvider=org.apache.openwhisk.core.containerpool.logging.SplunkLogStoreProvider\n* set the max concurrency limit config to a value > 1\n  * e.g. CONFIG_whisk_concurrencyLimit_max=4\n* set the concurrency limit > 1 on any action that you want to process activations concurrently.\n\n\n## Known issues\n\nKnown issues with enabling concurrent activation processing are:\n\n* Due to how activations are sent from Controller to Invoker, the only way to process n activations (where n > number of containers allowed in the Invoker) is to read additional messages from Kafka before they might be able to be processed.\nThis means that these messages will be buffered in memory until a container is available to process them. If these activations are for actions that tolerate concurrency, and there is a warm container with enough concurrency-capacity to process them, they will be processed immediately.\nIf the activations are for actions that do NOT support concurrency, or there are no containers available, the messages will remain in memory until containers become available. As such, the variety of action configurations may affect how many messages are buffered in memory at any time, and if a crash occurred those messages would be lost.\n\n* Indirectly related to concurrency, since log collection must be decoupled, the logs are not stored with the resulting activation entity. This causes these different API behaviors:\n  * `wsk activation get` and `wsk activation poll` does not return logs for primitive actions (only returns logs for sequences/compositions)\n  * `wsk activation logs` does not return logs for sequences/compositions (only returns logs for primitive actions)\n"
  },
  {
    "path": "docs/logging.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Logging in OpenWhisk\n\n## Default Logging Provider\n\nOpenWhisk uses Logback as default logging provider via slf4j.\n\nLogback can be configured in the configuration file [logback.xml](../common/scala/src/main/resources/logback.xml).\n\nBesides other things this configuration file defines the default log level for OpenWhisk.\nPekko Logging inherits the log level defined here.\n\n## Enable debug-level logging for selected namespaces\n\nFor performance reasons it is recommended to leave the default log level on INFO level. For debugging purposes one can enable DEBUG level logging for selected namespaces\n(aka special users). A list of namespace ids can be configured using the `nginx_special_users` property in the `group_vars/all` file of an environment.\n\nExample:\n\n```\nnginx_special_users:\n- 9cfe57b0-7fc1-4cf4-90b6-d7e9ea91271f\n- 08f65e83-2d12-5b7c-b625-23df81c749a9\n```\n\n## Using JMX to adapt the loglevel at run-time\n\nOne can alter the log level of a single component (Controller or Invoker) on the fly using JMX.\n\n### Example for using [jmxterm](ttp://wiki.cyclopsgroup.org/jmxterm/) to alter the log level\n\n1. Create a command file for jmxterm\n```\nopen <targethost>:<jmx port> -u <jmx username> -p <jmx password>\nrun -b ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator getLoggerLevel ROOT\nrun -b ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator setLoggerLevel ROOT DEBUG\nrun -b ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator getLoggerLevel ROOT\nclose\n```\n\n2. Issue the command with the created file like this:\n```\njava -jar jmxterm-1.0.0-uber.jar -n silent < filename\n```\n"
  },
  {
    "path": "docs/metrics.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# OpenWhisk Metric Support\n\nOpenWhisk distinguishes between system and user metrics (events).\n\nSystem metrics typically contain information about system performance and provide a possibility to send them to Kamon or write them to log files in logmarker format. These metrics are typically used by OpenWhisk providers/operators.\n\nUser metrics encompass information about action performance which is sent to Kafka in a form of events. These metrics are to be consumed by OpenWhisk users, however they could be also used for billing or audit purposes. It is to be noted that at the moment the events are not directly exposed to the users and require an additional Kafka Consumer based micro-service for data processing.\n\n## System specific metrics\n### Configuration\n\nBoth capabilities can be enabled or disabled separately during deployment via Ansible configuration in the 'group_vars/all' file of an environment.\n\nThere are four configurations options available:\n\n- **metrics_log** [true / false  (default: true)]\n\n  Enable/disable whether the metric information is written out to the log files in logmarker format.\n\n  *Beware: Even if set to false all messages using the log markers are still written out to the log*\n\n- **metrics_kamon** [true / false (default: false)]\n\n  Enable/disable whether metric information is sent to the configured StatsD server.\n\n- **metrics_kamon_tags: false** [true / false  (default: false)]\n\n  Enable/disable whether to use the Kamon tags when sending metrics.\n\n  *Notice: Tags are supported in only some Kamon backends. (OpenTSDB, Datadog, InfluxDB)*\n\n- **metrics_kamon_statsd_host** [hostname or ip address]\n\n  Hostname or ip address of the StatsD server\n\n- **metrics_kamon_statsd_port** [port number (default:8125)]\n\n  Port number of the StatsD server\n\nExample configuration:\n\n```\nmetrics_kamon: true\nmetrics_kamon_tags: false\nmetrics_kamon_statsd_host: '192.168.99.100'\nmetrics_kamon_statsd_port: '8125'\nmetrics_log: true\n```\n\n### Testing the StatsD metric support\n\nThe Kamon project provides an integrated docker image containing StatsD and a connected Grafana dashboard via [this Github project](https://github.com/kamon-io/docker-grafana-graphite). This image is helpful for testing the metrics sent via StatsD.\n\nPlease follow these [instructions](https://github.com/kamon-io/docker-grafana-graphite/blob/master/README.md) to start the docker image in your local docker environment.\n\nThe docker image exposes StatsD via the (standard) port 8125 and a Grafana dashboard via port 8080 on your docker host.\n\nThe address of your docker host has to be configured in the `metrics_kamon_statsd_host` configuration property.\n\n### Metric Names\n\nAll metric names have to be prefixed by a prefix that you specify and are subject to modification by graphite, datadog, or statsd. For example if prefix used is `openwhisk` then metric names would be like `openwhisk.counter.controller_activation_start`. This document assumes that metric name prefix is `openwhisk`\n\nCurrently OpenWhisk emits following types of metrics\n\n#### Counter\n\nCounter [record the count](http://kamon.io/documentation/0.6.x/kamon-core/metrics/instruments/#counters) of metric and there names are prefixed with `openwhisk.counter`. For example `openwhisk.counter.controller_activation_start`. Counters just counts and resets to zero upon each flush.\n\n#### Histograms\n\nHistogram record the [distribution](http://kamon.io/documentation/0.6.x/kamon-core/metrics/instruments/#histograms) of given metric and there names are prefixed with `openwhisk.histogram`. For example `openwhisk.histogram.controller_activation_finish`. A histogram metrics may result in multiple values at the metric aggregator level. For example in [Datadog](https://docs.datadoghq.com/developers/metrics/histograms/) for each histogram metric following values are record\n\n* `my_metric.avg` - Average of aggregated values during the flush interval.\n* `my_metric.count` - Count of aggregated values during the flush interval.\n* `my_metric.median` - Median of aggregated values during the flush interval.\n* `my_metric.95percentile` - 95th percentile value of aggregated values during the flush interval.\n* `my_metric.max` - Max of aggregated values during the flush interval.\n* `my_metric.min` - Min of aggregated values during the flush interval.\n\n#### Gauges\n\nGauges record the [distribution](https://kamon.io/docs/latest/core/metrics/#gauges) of given metric and their names are prefixed with `openwhisk.gauge`. For example `openwhisk.gauge.loadbalancer_totalHealthyInvoker_counter`. A gauge metrics provides the value at the given point and reports the same data unless the value has been changed be incremental or decremental than before. Gauges are useful for reporting metrics like kafka queue size or disk size.\n\n### Metric Details\n\nBelow are some of the important metrics emitted by OpenWhisk setup\n\n#### Controller metrics\n\nMetrics below are emitted from within a Controller instance.\n\n##### Controller Startup\n\n* `openwhisk.counter.controller_startup<controller_id>_counter` (counter)\n  * Example _openwhisk.counter.controller_startup0_counter_\n  * Records count of controller instance startup\n\n##### Controller Activation Retrieval During Blocking Invocations\n\n* `openwhisk.counter.controller_blockingActivationDatabaseRetrieval_counter` (counter) - Records the count of activations the controller has retrieved from the activation store during blocking invocations\n\n##### Activation Submission\n\nFollowing metrics record stats around activation handling within Controller\n\n* Normal actions\n  * `openwhisk.counter.controller_activation_start` (counter) - Records the count of non blocking activations started.\n  * `openwhisk.histogram.controller_activation_finish` (histogram) - Records the overall time taken for non blocking activation to be submitted to Load balancer.\n* Blocking actions\n  * `openwhisk.counter.controller_blockingActivation_start` (counter) - Records the count of blocking activations started.\n  * `openwhisk.histogram.controller_blockingActivation_finish` (histogram) - Records the time taken for a blocking activation to finish or timeout.\n\n##### Load Balancer\n\nAggregate metrics for inflight activations.\n\n* `openwhisk.gauge.loadbalancer<controllerId>_activationsInflight_counter` (gauge) - Records the number of activations being worked upon for a given controller. As a gauge this will give inflight activation count at the given point in time unless the change in value occurs.\n* `openwhisk.gauge.loadbalancer<controllerId>_memory<invokerType>Inflight_counter` (gauge) - Records the amount of RAM memory in use for in flight activations. This is not actual runtime memory but the memory specified per action limits. **invokerType** defines whether it is a managed or a blackbox invoker.\n\nMetrics below are for current memory capacity\n\n* `openwhisk.histogram.loadbalancer_totalCapacity<invokerType>_counter` (histogram) - Current memory capacity for all usable managed and blackbox invokers, total user memory in shard managed by controller. **invokerType** defines whether it is a managed or a blackbox invoker.\n\nMetrics below are captured within load balancer\n\n* `openwhisk.counter.loadbalancer_activations_counter` (counter) -  Records the count of activations sent to Kafka.\n* `openwhisk.counter.controller_kafka_start` (counter) - Records the count of activations sent to Kafka.\n* `openwhisk.counter.controller_kafka_error` (counter) - Records the count of activations which encountered some failure while submitting to Kafka.\n* `openwhisk.histogram.controller_kafka_finish` (histogram) - Records the time taken when activation was successfully submitted to Kafka.\n* `openwhisk.histogram.controller_kafka_error` (histogram) - Records the time taken when activation submission to Kafka resulted in failure.\n* `openwhisk.counter.controller_loadbalancer_start` (counter) - Records the count of activations submitted to load balancer.\n* `openwhisk.histogram.controller_loadbalancer_finish` (histogram) - Records the time taken to submit to load balancer.\n\nMetrics below are for invoker state as recorded within load balancer monitoring.\n\n* `openwhisk.gauge.loadbalancer_totalHealthyInvoker<invokerType>_counter`(gauge) - Records the count of managed invokers considered healthy based on health pings. **invokerType** defines whether it is a managed or a blackbox invoker.\n* `openwhisk.gauge.loadbalancer_totalUnresponsiveInvoker<invokerType>_counter` (gauge) - Records the count of managed invokers considered unresponsive when health pings arriving fine but the invokers do not respond with active-acks in given time. **invokerType** defines whether it is a managed or a blackbox invoker.\n* `openwhisk.gauge.loadbalancer_totalOfflineInvoker<invokerType>_counter` (gauge) - Records the count of managed invokers considered offline when no health pings arrive from the invokers. **invokerType** defines whether it is a managed or a blackbox invoker.\n* `openwhisk.gauge.loadbalancer_totalUnhealthyInvoker<invokerType>_counter` (gauge) - Records the count of managed invokers considered unhealthy when health pings arrive fine but the invokers report system errors. **invokerType** defines whether it is a managed or a blackbox invoker.\n\nMetrics below provide information about completion ack processing in load balancers. Depending on configuration setting `metrics_kamon_tags` (see above), a base metric with tags or a set of metrics without tags will be emitted.\n\n* Base metric `openwhisk.counter.loadbalancer_completionAck_counter`: count of processed regular or forced completion acks.\n* Tag `controller_id`: the controller's id.\n* Tag `type`: the exact type of completion ack.\n  * Type `regular`: a regular completion ack sent by an invoker and received in time. Does not include completion acks for healthcheck actions.\n  * Type `forced`: no completion ack was received in time and the timeout forced the completion ack to close.\n  * Type `healthcheck`: a regular completion ack for healthcheck actions sent by an invoker and received in time.\n  * Type `regularAfterForced`: a regular completion ack sent by an invoker and not received in time. The completion ack was already forced.\n  * Type `forcedAfterRegular`: a timeout tries to force a completion ack that has already been closed by a regular completion ack. A race condition that can occur if the regular completion ack is received near the timeout.\n* If `metrics_kamon_tags` is set to `false`, a set of metrics will be emitted constructed using following scheme: `openwhisk.counter.loadbalancer<controller_id>_completionAck_<type>_counter`.\n\n#### Invoker metrics\n\n##### Container Init\n\n* `openwhisk.counter.invoker_activationInit_start` (counter) - Count of container initializations done.\n* `openwhisk.histogram.invoker_activationInit_finish` (histogram) - Time taken for successful container initializations.\n* `openwhisk.histogram.invoker_activationInit_error` (histogram) - Time taken container initialization failed. Count metrics of this histogram would give insight on failed initialization count.\n\n##### Container Run\n\n* `openwhisk.counter.invoker_activationRun_start` (counter) - Count of action executions performed.\n* `openwhisk.histogram.invoker_activationRun_finish` (histogram) - Time taken for action execution for success case.\n* `openwhisk.histogram.invoker_activationRun_error` (histogram) - Time taken for action execution for failed cases. Count metrics of this histogram would give insight on failed execution count.\n\n##### Container Start\n\n* `openwhisk.counter.invoker_containerStart.cold_counter` (counter) - Count of number of cold starts.\n* `openwhisk.counter.invoker_containerStart.recreated_counter` (counter) - Count of number of times container is recreated.\n* `openwhisk.counter.invoker_containerStart.warm_counter` (counter) - Count of number of times a warm container is used.\n\n##### Log Collection\n\n* `openwhisk.counter.invoker_collectLogs_start` (counter) - Count of number of times log were collected.\n* `openwhisk.counter.invoker_collectLogs_error` (counter) - Count of number of failed logs collections.\n* `openwhisk.histogram.invoker_collectLogs_error` (histogram) - Time taken for failed log collection.\n* `openwhisk.histogram.invoker_collectLogs_finish` (histogram) - Time taken for successful log collection.\n\n##### Activation Handling\n\n* `openwhisk.counter.invoker_activation_start` (counter) - Count of activations handled\n\n##### Docker Metrics\n\nFollowing metrics capture stats around various docker command executions.\n\n* pause\n  * `openwhisk.counter.invoker_docker.pause_start`\n  * `openwhisk.counter.invoker_docker.pause_error`\n  * `openwhisk.counter.invoker_docker.pause_timeout`\n  * `openwhisk.histogram.invoker_docker.pause_finish`\n  * `openwhisk.histogram.invoker_docker.pause_error`\n* ps\n  * `openwhisk.counter.invoker_docker.ps_start`\n  * `openwhisk.counter.invoker_docker.ps_error`\n  * `openwhisk.counter.invoker_docker.ps_timeout`\n  * `openwhisk.histogram.invoker_docker.ps_finish`\n  * `openwhisk.histogram.invoker_docker.ps_error`\n* pull\n  * `openwhisk.counter.invoker_docker.pull_start`\n  * `openwhisk.counter.invoker_docker.pull_error`\n  * `openwhisk.counter.invoker_docker.pull_timeout`\n  * `openwhisk.histogram.invoker_docker.pull_finish`\n  * `openwhisk.histogram.invoker_docker.pull_error`\n* rm\n  * `openwhisk.counter.invoker_docker.rm_start`\n  * `openwhisk.counter.invoker_docker.rm_error`\n  * `openwhisk.counter.invoker_docker.rm_timeout`\n  * `openwhisk.histogram.invoker_docker.rm_finish`\n  * `openwhisk.histogram.invoker_docker.rm_error`\n* run\n  * `openwhisk.counter.invoker_docker.run_start`\n  * `openwhisk.counter.invoker_docker.run_error`\n  * `openwhisk.counter.invoker_docker.run_timeout`\n  * `openwhisk.histogram.invoker_docker.run_finish`\n  * `openwhisk.histogram.invoker_docker.run_error`\n* unpause\n  * `openwhisk.counter.invoker_docker.unpause_start`\n  * `openwhisk.counter.invoker_docker.unpause_error`\n  * `openwhisk.counter.invoker_docker.unpause_timeout`\n  * `openwhisk.histogram.invoker_docker.unpause_finish`\n  * `openwhisk.histogram.invoker_docker.unpause_error`\n\n#### Kafka Metrics\n\nMetrics below are emitted per kafka topic.\n\n* `openwhisk.histogram.kafka_<topic name>.delay_start` - Time delay between when a message was pushed to Kafka and when it is read within a consumer. This metric is recorded for every message read.\n* `openwhisk.gauge.kafka_<topic name>_counter` - Records the Queue size of the topic. By default this metric is emitted every 60 secs.\n\nMetrics per topic\n* `cacheInvalidation` - Emitted per controller while reading the cache invalidation messages.\n  * `openwhisk.histogram.kafka_cacheInvalidation.delay_start`\n  * `openwhisk.histogram.kafka_cacheInvalidation_counter.count`\n* `health` - Emitted per controller while reading the invoker health pings.\n  * `openwhisk.histogram.kafka_health.delay_start`\n  * `openwhisk.histogram.kafka_health_counter`\n* `completed<controllerId>` - Topic to receive completed activations. This is emitted per controller for its own topic. For example for controller id 0 metric names would be\n  * `openwhisk.histogram.kafka_completed0.delay_start`\n  * `openwhisk.histogram.kafka_completed0_counter`\n* `invoker<invokerId>` - Topic to receive activations to complete. This is emitted per invoker for its own topic. For example for invoker id 0 metric names would be\n  * `openwhisk.histogram.kafka_invoker0_counter`\n  * `openwhisk.histogram.kafka_invoker0.delay_start`\n\n#### Database Metrics\n\n##### Cache Metrics\n\n* `openwhisk.counter.database_cacheHit_counter` - Count of cache hits.\n* `openwhisk.counter.database_cacheMiss_counter` - Count of cache misses.\n\nMetrics below are emitted for database related operations and follow a pattern\n\n* `openwhisk.counter.database_<operation type>_start` - Count of database operations done for given type. Example `openwhisk.counter.database_getDocument_start`.\n* `openwhisk.counter.database_<operation type>_error` - Count of database operations done for given type which resulted in error. Example `openwhisk.counter.database_getDocument_error`.\n* `openwhisk.histogram.database_<operation type>_finish` - Time taken for successful completion of given database operation. Example `openwhisk.histogram.database_getDocument_finish`.\n* `openwhisk.histogram.database_<operation type>_error` - Time taken for failed completion of given database operation. Example `openwhisk.histogram.database_getDocument_error`.\n\nOperation Types\n\n* `deleteDocument`\n* `getDocument`\n* `queryView`\n* `saveDocument`\n* `saveDocumentBulk`\n\n#### CosmosDB RU Metrics\n\nWhen database used is CosmosDB then metrics related to CosmosDB Resource Units is also emitted.\n\nIf Kamon tags are enabled then metric name is `openwhisk.counter.cosmosdb_ru_used` with following tags\n\n- `mode` - `read` or `write`\n- `collection` - Name of collection. Example `activations`, `whisks` and `subjects`\n- `action` - Type of operation performed. Example `get`, `put`, `del`, `query` and `count`\n\nIf Kamon tags are not enabled then metric name is of the form `openwhisk.counter.cosmosdb.ru.<collection>.<action>`\n\n## User specific metrics\n### Configuration\nUser metrics are enabled by default and could be explicitly disabled by setting the following property in one of the Ansible configuration files:\n```\nuser_events: false\n```\n\n### Supported events\nActivation is an event that occurs after after each activation. It includes the following execution metadata:\n```\nwaitTime - internal system hold time\ninitTime - time it took to initialize an action, e.g. docker init\nstatusCode - status code of the invocation: 0 - success, 1 - application error, 2 - action developer error, 3 - internal OpenWhisk error\nduration - actual time the action code was running\nkind - action flavor, e.g. Node.js\nconductor - true for conductor backed actions\nmemory - maximum memory allowed for action container\ncausedBy - contains the \"causedBy\" annotation (can be \"sequence\" or nothing at the moment)\nsize - size (in bytes) of the invocation response\nuserDefinedStatusCode - status code represents `statusCode` set in result response. (if not set, this field will not be present)\n```\nMetric is any user specific event produced by the system and it at this moment includes the following information:\n```\nConcurrentRateLimit - a user has exceeded its limit for concurrent invocations.\nTimedRateLimit - the user has reached its per minute limit for the number of invocations.\nConcurrentInvocations - the number of in flight invocations per user.\n```\n\nExample events that could be consumed from Kafka.\nActivation:\n```json\n{\n  \"body\": {\n    \"statusCode\": 0,\n    \"duration\": 3,\n    \"name\": \"whisk.system/invokerHealthTestAction0\",\n    \"waitTime\": 583915671,\n    \"conductor\": false,\n    \"kind\": \"nodejs:6\",\n    \"initTime\": 0,\n    \"memory\": 256,\n    \"size\": 463,\n    \"causedBy\": false\n  },\n  \"eventType\": \"Activation\",\n  \"source\": \"invoker0\",\n  \"subject\": \"whisk.system\",\n  \"timestamp\": 1524476122676,\n  \"userId\": \"d0888ad5-5a92-435e-888a-d55a92935e54\",\n  \"namespace\": \"whisk.system\"\n}\n```\nMetric:\n```json\n{\n  \"body\": {\n    \"metricName\": \"ConcurrentInvocations\",\n    \"metricValue\": 1\n  },\n  \"eventType\": \"Metric\",\n  \"source\": \"controller0\",\n  \"subject\": \"guest\",\n  \"timestamp\": 1524476104419,\n  \"userId\": \"23bc46b1-71f6-4ed5-8c54-816aa4f8c502\",\n  \"namespace\": \"guest\"\n}\n```\n\n### User-events consumer service\nAll user metrics can be consumed and published to various services such as Prometheus, Datadog etc via Kamon by using the [user-events service](https://github.com/apache/openwhisk/tree/master/core/monitoring/user-events/README.md).\n"
  },
  {
    "path": "docs/packages.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Using and creating OpenWhisk packages\n\nIn OpenWhisk, you can use packages to bundle together a set of related actions, and share them with others.\n\nA package can include *actions* and *feeds*.\n- An action is a piece of code that runs on OpenWhisk. For example, the Cloudant package includes actions to read and write records to a Cloudant database.\n- A feed is used to configure an external event source to fire trigger events. For example, the Alarm package includes a feed that can fire a trigger at a specified frequency.\n\nEvery OpenWhisk entity, including packages, belongs in a *namespace*, and the fully qualified name of an entity is `/namespaceName[/packageName]/entityName`. Refer to the [naming guidelines](./reference.md#openwhisk-entities) for more information.\n\nThe following sections describe how to browse packages and use the triggers and feeds in them. In addition, if you are interested in contributing your own packages to the catalog, read the sections on creating and sharing packages.\n\n## Browsing packages\n\nSeveral packages are registered with OpenWhisk. You can get a list of packages in a namespace, list the entities in a package, and get a description of the individual entities in a package.\n\n1. Get a list of packages in the `/whisk.system` namespace.\n\n  ```\n  $ wsk package list /whisk.system\n  ```\n  ```\n  packages\n  /whisk.system/cloudant                                                 shared\n  /whisk.system/alarms                                                   shared\n  /whisk.system/watson                                                   shared\n  /whisk.system/websocket                                                shared\n  /whisk.system/weather                                                  shared\n  /whisk.system/system                                                   shared\n  /whisk.system/utils                                                    shared\n  /whisk.system/slack                                                    shared\n  /whisk.system/samples                                                  shared\n  /whisk.system/github                                                   shared\n  /whisk.system/pushnotifications                                        shared\n  ```\n\n2. Get a list of entities in the `/whisk.system/cloudant` package.\n\n  ```\n  $ wsk package get --summary /whisk.system/cloudant\n  ```\n  ```\n  package /whisk.system/cloudant: Cloudant database service\n     (parameters: *apihost, *dbname, *host, overwrite, *password, *username)\n   action /whisk.system/cloudant/read: Read document from database\n     (parameters: dbname, id, params)\n   action /whisk.system/cloudant/write: Write document in database\n     (parameters: dbname, doc)\n   feed   /whisk.system/cloudant/changes: Database change feed\n     (parameters: dbname, filter, query_params)\n  ...\n  ```\n\n  **Note**: Parameters listed under the package with a prefix `*` are predefined, bound parameters. Parameters without a `*` are those listed under the [annotations](./annotations.md) for each entity. Furthermore, any parameters with the prefix `**` are finalized bound parameters. This means that they are immutable, and cannot be changed by the user. Any entity listed under a package inherits specific bound parameters from the package. To view the list of known parameters of an entity belonging to a package, you will need to run a `get --summary` of the individual entity.\n\n  This output shows that the Cloudant package provides the actions `read` and `write`, and the trigger feed called `changes`. The `changes` feed causes triggers to be fired when documents are added to the specified Cloudant database.\n\n  The Cloudant package also defines the parameters `username`, `password`, `host`, and `dbname`. These parameters must be specified for the actions and feeds to be meaningful. The parameters allow the actions to operate on a specific Cloudant account, for example.\n\n3. Get a description of the `/whisk.system/cloudant/read` action.\n\n  ```\n  $ wsk action get --summary /whisk.system/cloudant/read\n  ```\n  ```\n  action /whisk.system/cloudant/read: Read document from database\n     (parameters: *apihost, *dbname, *host, *id, params, *password, *username)\n  ```\n\n  *NOTE*: Notice that the parameters listed for the action `read` were expanded upon from the action summary compared to the package summary above. To see the official bound parameters for actions and triggers listed under packages, run an individual get summary for the particular entity.\n\n  This output shows that the Cloudant `read` action lists eight parameters, seven of which are predefined. These include the database and document ID to retrieve.\n\n\n## Invoking actions in a package\n\nYou can invoke actions in a package, just as with other actions. The next few steps show how to invoke the `greeting` action in the `/whisk.system/samples` package with different parameters.\n\n1. Get a description of the `/whisk.system/samples/greeting` action.\n\n  ```\n  $ wsk action get --summary /whisk.system/samples/greeting\n  ```\n  ```\n  action /whisk.system/samples/greeting: Returns a friendly greeting\n     (parameters: name, place)\n  ```\n\n  Notice that the `greeting` action takes two parameters: `name` and `place`.\n\n2. Invoke the action without any parameters.\n\n  ```\n  $ wsk action invoke --result /whisk.system/samples/greeting\n  ```\n  ```\n  {\n      \"payload\": \"Hello, stranger from somewhere!\"\n  }\n  ```\n\n  The output is a generic message because no parameters were specified.\n\n3. Invoke the action with parameters.\n\n  ```\n  $ wsk action invoke --result /whisk.system/samples/greeting --param name Mork --param place Ork\n  ```\n  ```\n  {\n      \"payload\": \"Hello, Mork from Ork!\"\n  }\n  ```\n\n  Notice that the output uses the `name` and `place` parameters that were passed to the action.\n\n\n## Creating and using package bindings\n\nAlthough you can use the entities in a package directly, you might find yourself passing the same parameters to the action every time. You can avoid this by binding to a package and specifying default parameters. These parameters are inherited by the actions in the package.\n\nFor example, in the `/whisk.system/cloudant` package, you can set default `username`, `password`, and `dbname` values in a package binding and these values are automatically passed to any actions in the package.\n\nIn the following simple example, you bind to the `/whisk.system/samples` package.\n\n1. Bind to the `/whisk.system/samples` package and set a default `place` parameter value.\n\n  ```\n  $ wsk package bind /whisk.system/samples valhallaSamples --param place Valhalla\n  ```\n  ```\n  ok: created binding valhallaSamples\n  ```\n\n2. Get a description of the package binding.\n\n  ```\n  $ wsk package get --summary valhallaSamples\n  ```\n  ```\n  package /namespace/valhallaSamples: Returns a result based on parameter place\n     (parameters: *place)\n   action /namespace/valhallaSamples/helloWorld: Demonstrates logging facilities\n      (parameters: payload)\n   action /namespace/valhallaSamples/greeting: Returns a friendly greeting\n      (parameters: name, place)\n   action /namespace/valhallaSamples/curl: Curl a host url\n      (parameters: payload)\n   action /namespace/valhallaSamples/wordCount: Count words in a string\n      (parameters: payload)\n  ```\n\n  Notice that all the actions in the `/whisk.system/samples` package are available in the `valhallaSamples` package binding.\n\n3. Invoke an action in the package binding.\n\n  ```\n  $ wsk action invoke --result valhallaSamples/greeting --param name Odin\n  ```\n  ```\n  {\n      \"payload\": \"Hello, Odin from Valhalla!\"\n  }\n  ```\n\n  Notice from the result that the action inherits the `place` parameter you set when you created the `valhallaSamples` package binding.\n\n4. Invoke an action and overwrite the default parameter value.\n\n  ```\n  $ wsk action invoke --result valhallaSamples/greeting --param name Odin --param place Asgard\n  ```\n  ```\n  {\n      \"payload\": \"Hello, Odin from Asgard!\"\n  }\n  ```\n\n  Notice that the `place` parameter value that is specified with the action invocation overwrites the default value set in the `valhallaSamples` package binding.\n\n\n## Creating and using trigger feeds\n\nFeeds offer a convenient way to configure an external event source to fire these events to a OpenWhisk trigger. This example shows how to use a feed in the Alarms package to fire a trigger every second, and how to use a rule to invoke an action every second.\n\n1. Get a description of the feed in the `/whisk.system/alarms` package.\n\n  ```\n  $ wsk package get --summary /whisk.system/alarms\n  ```\n  ```\n  package /whisk.system/alarms: Alarms and periodic utility\n     (parameters: *apihost, *cron, *trigger_payload)\n   feed   /whisk.system/alarms/alarm: Fire trigger when alarm occurs\n      (parameters: none defined)\n  ```\n\n  ```\n  $ wsk action get --summary /whisk.system/alarms/alarm\n  ```\n  ```\n  action /whisk.system/alarms/alarm: Fire trigger when alarm occurs\n     (parameters: *apihost, *cron, *trigger_payload)\n  ```\n\n  The `/whisk.system/alarms/alarm` feed takes two parameters:\n  - `cron`: A crontab specification of when to fire the trigger.\n  - `trigger_payload`: The payload parameter value to set in each trigger event.\n  - `apihost`: The API host endpoint that will be receiving the feed.\n\n2. Create a trigger that fires every eight seconds.\n\n  ```\n  $ wsk trigger create everyEightSeconds --feed /whisk.system/alarms/alarm -p cron \"*/8 * * * * *\" -p trigger_payload \"{\\\"name\\\":\\\"Mork\\\", \\\"place\\\":\\\"Ork\\\"}\"\n  ```\n  ```\n  ok: created trigger feed everyEightSeconds\n  ```\n\n3. Create a 'hello.js' file with the following action code.\n\n  ```\n  function main(params) {\n      return {payload:  'Hello, ' + params.name + ' from ' + params.place};\n  }\n  ```\n\n4. Make sure that the action exists.\n\n  ```\n  $ wsk action update hello hello.js\n  ```\n\n5. Create a rule that invokes the `hello` action every time the `everyEightSeconds` trigger fires.\n\n  ```\n  $ wsk rule create myRule everyEightSeconds hello\n  ```\n  ```\n  ok: created rule myRule\n  ```\n\n6. Check that the action is being invoked by polling for activation logs.\n\n  ```\n  $ wsk activation poll\n  ```\n\n  You should see activations every eight seconds for the trigger, the rule, and the action. The action receives the parameters `{\"name\":\"Mork\", \"place\":\"Ork\"}` on every invocation.\n\n\n## Creating a package\n\nA package is used to organize a set of related actions and feeds.\nIt also allows for parameters to be shared across all entities in the package.\n\nTo create a custom package with a simple action in it, try the following example:\n\n1. Create a package called \"custom\".\n\n  ```\n  $ wsk package create custom\n  ```\n  ```\n  ok: created package custom\n  ```\n\n2. Get a summary of the package.\n\n  ```\n  $ wsk package get --summary custom\n  ```\n  ```\n  package /myNamespace/custom\n     (parameters: none defined)\n  ```\n\n  Notice that the package is empty.\n\n3. Create a file called `identity.js` that contains the following action code. This action returns all input parameters.\n\n  ```\n  function main(args) { return args; }\n  ```\n\n4. Create an `identity` action in the `custom` package.\n\n  ```\n  $ wsk action create custom/identity identity.js\n  ```\n  ```\n  ok: created action custom/identity\n  ```\n\n  Creating an action in a package requires that you prefix the action name with a package name. Package nesting is not allowed. A package can contain only actions and can't contain another package.\n\n5. Get a summary of the package again.\n\n  ```\n  $ wsk package get --summary custom\n  ```\n  ```\n  package /myNamespace/custom\n    (parameters: none defined)\n   action /myNamespace/custom/identity\n    (parameters: none defined)\n  ```\n\n  You can see the `custom/identity` action in your namespace now.\n\n6. Invoke the action in the package.\n\n  ```\n  $ wsk action invoke --result custom/identity\n  ```\n  ```\n  {}\n  ```\n\n\nYou can set default parameters for all the entities in a package. You do this by setting package-level parameters that are inherited by all actions in the package. To see how this works, try the following example:\n\n1. Update the `custom` package with two parameters: `city` and `country`.\n\n  ```\n  $ wsk package update custom --param city Austin --param country USA\n  ```\n  ```\n  ok: updated package custom\n  ```\n\n2. Display the parameters in the package and action, and see how the `identity` action in the package inherits parameters from the package.\n\n  ```\n  $ wsk package get custom\n  ```\n  ```\n  ok: got package custom\n  ...\n  \"parameters\": [\n      {\n          \"key\": \"city\",\n          \"value\": \"Austin\"\n      },\n      {\n          \"key\": \"country\",\n          \"value\": \"USA\"\n      }\n  ]\n  ...\n  ```\n\n  ```\n  $ wsk action get custom/identity\n  ```\n  ```\n  ok: got action custom/identity\n  ...\n  \"parameters\": [\n      {\n          \"key\": \"city\",\n          \"value\": \"Austin\"\n      },\n      {\n          \"key\": \"country\",\n          \"value\": \"USA\"\n      }\n  ]\n  ...\n  ```\n\n3. Invoke the identity action without any parameters to verify that the action indeed inherits the parameters.\n\n  ```\n  $ wsk action invoke --result custom/identity\n  ```\n  ```\n  {\n      \"city\": \"Austin\",\n      \"country\": \"USA\"\n  }\n  ```\n\n4. Invoke the identity action with some parameters. Invocation parameters are merged with the package parameters; the invocation parameters override the package parameters.\n\n  ```\n  $ wsk action invoke --result custom/identity --param city Dallas --param state Texas\n  ```\n  ```\n  {\n      \"city\": \"Dallas\",\n      \"country\": \"USA\",\n      \"state\": \"Texas\"\n  }\n  ```\n\n\n## Sharing a package\n\nAfter the actions and feeds that comprise a package are debugged and tested, the package can be shared with all OpenWhisk users. Sharing the package makes it possible for the users to bind the package, invoke actions in the package, and author OpenWhisk rules and sequence actions.\n\n1. Share the package with all users:\n\n  ```\n  $ wsk package update custom --shared yes\n  ```\n  ```\n  ok: updated package custom\n  ```\n\n2. Display the `publish` property of the package to verify that it is now true.\n\n  ```\n  $ wsk package get custom\n  ```\n  ```\n  ok: got package custom\n  ...\n  \"publish\": true,\n  ...\n  ```\n\n\nOthers can now use your `custom` package, including binding to the package or directly invoking an action in it. Other users must know the fully qualified names of the package to bind it or invoke actions in it. Actions and feeds within a shared package are _public_. If the package is private, then all of its contents are also private.\n\n1. Get a description of the package to show the fully qualified names of the package and action.\n\n  ```\n  $ wsk package get --summary custom\n  ```\n  ```\n  package /myNamespace/custom: Returns a result based on parameters city and country\n     (parameters: *city, *country)\n   action /myNamespace/custom/identity\n     (parameters: none defined)\n  ```\n\n  In the previous example, you're working with the `myNamespace` namespace, and this namespace appears in the fully qualified name.\n"
  },
  {
    "path": "docs/parameters.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Working with parameters\n\nWhen working with serverless actions, data is supplied by adding parameters to the actions; these are in the parameter declared as an argument to the main serverless function. All data arrives this way and the values can be set in a few different ways. The first option is to supply parameters when an action or package is created (or updated). This approach is useful for data that stays the same on every execution, equivalent to environment variables on other platforms, or for default values that might be overridden at invocation time. The second option is to supply parameters when the action is invoked - and this approach will override any parameters already set.\n\nThis page outlines how to configure parameters when deploying packages and actions, and how to supply parameters when invoking an action. There is also information on how to use a file to store the parameters and pass the filename, rather than supplying each parameter individually on the command-line.\n\n### Passing parameters to an action at invoke time\n\nParameters can be passed to the action when it is invoked. These examples use JavaScript but all [the other\nlanguages](actions.md#languages-and-runtimes) work the same way.\n\n1. Use parameters in the action. For example, create 'hello.js' file with the following content:\n\n  ```javascript\n  function main(params) {\n      return {payload:  'Hello, ' + params.name + ' from ' + params.place};\n  }\n  ```\n\n  The input parameters are passed as a JSON object parameter to the `main` function. Notice how the `name` and `place` parameters are retrieved from the `params` object in this example.\n\n2. Update the action so it is ready to use:\n\n  ```\n  wsk action update hello hello.js\n  ```\n\n3. Parameters can be provided explicitly on the command-line, or by supplying a file containing the desired parameters\n\n  To pass parameters directly through the command-line, supply a key/value pair to the `--param` flag:\n  ```\n  wsk action invoke --result hello --param name Dorothy --param place Kansas\n  ```\n\n  This produces the result:\n\n  ```json\n  {\n      \"payload\": \"Hello, Dorothy from Kansas\"\n  }\n  ```\n\n  Notice the use of the `--result` option: it implies a blocking invocation where the CLI waits for the activation to complete and then displays only the result. For convenience, this option may be used without `--blocking` which is automatically inferred.\n\n  Additionally, if parameter values specified on the command-line are valid JSON, then they will be parsed and sent to your action as a structured object. For example, if we update our hello action to:\n\n  ```javascript\n  function main(params) {\n      return {payload:  'Hello, ' + params.person.name + ' from ' + params.person.place};\n  }\n  ```\n\n  Now the action expects a single `person` parameter to have fields `name` and `place`. If we invoke the action with a single `person` parameter that is valid JSON:\n\n  ```\n  wsk action invoke --result hello -p person '{\"name\": \"Dorothy\", \"place\": \"Kansas\"}'\n  ```\n\n  The result is the same because the CLI automatically parses the `person` parameter value into the structured object that the action now expects:\n  ```json\n  {\n      \"payload\": \"Hello, Dorothy from Kansas\"\n  }\n  ```\n\n### Setting default parameters on an action\n\nActions can be invoked with multiple named parameters. Recall that the `hello` action from the previous example expects two parameters: the *name* of a person, and the *place* where they're from.\n\nRather than pass all the parameters to an action every time, you can bind certain parameters. The following example binds the *place* parameter so that the action defaults to the place \"Kansas\":\n\n1. Update the action by using the `--param` option to bind parameter values, or by passing a file that contains the parameters to `--param-file` (for examples of using files, see the section on [working with parameter files](#working-with-parameter-files).\n\n  To specify default parameters explicitly on the command-line, provide a key/value pair to the `param` flag:\n\n  ```\n  wsk action update hello --param place Kansas\n  ```\n\n2. Invoke the action, passing only the `name` parameter this time.\n\n  ```\n  wsk action invoke --result hello --param name Dorothy\n  ```\n  ```json\n  {\n      \"payload\": \"Hello, Dorothy from Kansas\"\n  }\n  ```\n\n  Notice that you did not need to specify the `place` parameter when you invoked the action. Bound parameters can still be overwritten by specifying the parameter value at invocation time.\n\n3. Invoke the action, passing both `name` and `place` values, and observe the output:\n\n  ```\n  wsk action invoke --result hello --param name Dorothy --param place \"Washington, DC\"\n  ```\n\n  ```json\n  {\n      \"payload\": \"Hello, Dorothy from Washington, DC\"\n  }\n  ```\n\n  Despite a parameter set on the action when it was created/updated, this is overridden by a parameter that was supplied when invoking the action.\n\n### Setting default parameters on a package\n\nParameters can be set at the package level, and these will serve as default parameters for actions unless:\n\n- The action itself has a default parameter.\n- The action has a parameter supplied at invoke time, which will always be the \"winner\" where more than one parameter is available.\n\nThe following example sets a default parameter of `name` on the `MyApp` package and shows an action making use of it.\n\n1. Create a package with a parameter set:\n\n ```\n wsk package update MyApp --param name World\n ```\n\n2. Create an action in this package:\n\n ```\n    function main(params) {\n        return {payload: \"Hello, \" + params.name};\n    }\n ```\n\n ```\n wsk action update MyApp/hello hello.js\n ```\n\n3. Invoke the action, and observe the default package parameter in use:\n ```\n wsk action invoke --result MyApp/hello\n ```\n\n ```\n    {\n        \"payload\": \"Hello, World\"\n    }\n ```\n\n ### Working with parameter files\n\nIt's also possible to put parameters into a file in JSON format, and then pass the parameters in by supplying the filename with the `param-file` flag. This works for both packages and actions when creating/updating them, and when invoking actions.\n\n1. As an example, consider the very simple \"hello\" example from earlier. Using `hello.js` with this content:\n\n  ```javascript\n  function main(params) {\n      return {payload:  'Hello, ' + params.name + ' from ' + params.place};\n  }\n  ```\n\n2. Update the action with the updated contents of `hello.js`:\n\n  ```\n  wsk action update hello hello.js\n  ```\n\n3. Create a parameter file called `parameters.json` containing JSON-formatted parameters:\n\n  ```json\n  {\n      \"name\": \"Dorothy\",\n      \"place\": \"Kansas\"\n  }\n  ```\n\n4. Use the `parameters.json` filename when invoking the action, and observe the output\n\n  ```\n  wsk action invoke --result hello --param-file parameters.json\n  ```\n\n  ```json\n  {\n      \"payload\": \"Hello, Dorothy from Kansas\"\n  }\n  ```\n"
  },
  {
    "path": "docs/reference.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# OpenWhisk system details\n\nThe following sections provide more details about the OpenWhisk system.\n\n## OpenWhisk entities\n\n### Namespaces and packages\n\nOpenWhisk actions, triggers, and rules belong in a namespace, and optionally a package.\n\nPackages can contain actions and feeds. A package cannot contain another package, so package nesting is not allowed. Also, entities do not have to be contained in a package.\n\nIn IBM Cloud, an organization+space pair corresponds to a OpenWhisk namespace. For example, the organization `BobsOrg` and space `dev` would correspond to the OpenWhisk namespace `/BobsOrg_dev`.\n\nYou can create your own namespaces if you're entitled to do so. The `/whisk.system` namespace is reserved for entities that are distributed with the OpenWhisk system.\n\n\n### Fully qualified names\n\nThe fully qualified name of an entity is\n`/namespaceName[/packageName]/entityName`. Notice that `/` is used to delimit namespaces, packages, and entities.\n\nIf the fully qualified name has three parts:\n`/namespaceName/packageName/entityName`, then the namespace can be entered without a prefixed `/`; otherwise, namespaces must be prefixed with a `/`.\n\nFor convenience, the namespace can be left off if it is the user's *default namespace*.\n\nFor example, consider a user whose default namespace is `/myOrg`. Following are examples of the fully qualified names of a number of entities and their aliases.\n\n| Fully qualified name | Alias | Namespace | Package | Name |\n| --- | --- | --- | --- | --- |\n| `/whisk.system/cloudant/read` |  | `/whisk.system` | `cloudant` | `read` |\n| `/myOrg/video/transcode` | `video/transcode` | `/myOrg` | `video` | `transcode` |\n| `/myOrg/filter` | `filter` | `/myOrg` |  | `filter` |\n\nYou will be using this naming scheme when you use the OpenWhisk CLI, among other places.\n\n### Entity names\n\nThe names of all entities, including actions, triggers, rules, packages, and namespaces, are a sequence of characters that follow the following format:\n\n* The first character must be an alphanumeric character, or an underscore.\n* The subsequent characters can be alphanumeric, spaces, or any of the following: `_`, `@`, `.`, `-`.\n* The last character can't be a space.\n\nMore precisely, a name must match the following regular expression (expressed with Java metacharacter syntax): `\\A([\\w]|[\\w][\\w@ .-]*[\\w@.-]+)\\z`.\n\n## System limits\n\n### Actions\nOpenWhisk has a few system limits, including how much memory an action can use and how many action invocations are allowed per minute.\n\n**Note:** This default limits are for the open source distribution; production deployments like IBM Cloud Functions likely have higher limits.\nAs an operator or developer you can change some of the limits using [Ansible inventory variables](../ansible/README.md#changing-limits).\n\n**Note:** On Openwhisk 2.0 with the scheduler service, **concurrent** in the table below really means the max containers\nthat can be provisioned at once for a namespace. The api _may_ be able to accept more activations than this number at once\ndepending on a number of factors.\n\nThe following table lists the default limits for actions.\n\n| limit | description                                                                               | configurable | unit | default |\n| ----- |-------------------------------------------------------------------------------------------| ------------ | -----| ------- |\n| timeout | a container is not allowed to run longer than N milliseconds                              | per action |  milliseconds | 60000 |\n| memory | a container is not allowed to allocate more than N MB of memory                           | per action | MB | 256 |\n| logs | a container is not allowed to write more than N MB to stdout                              | per action | MB | 10 |\n | instances | an action is not allowed to have more containers than this value (**new scheduler only**) | per action  | number | namespace concurrency limit |\n| concurrent | no more than N activations may be submitted per namespace either executing or queued for execution | per namespace | number | 100 |\n| minuteRate | no more than N activations may be submitted per namespace per minute                      | per namespace | number | 120 |\n| codeSize | the maximum size of the action code                                                       | configurable, limit per action | MB | 48 |\n| parameters | the maximum size of the parameters that can be attached                                   | not configurable, limit per action/package/trigger | MB | 1 |\n| result | the maximum size of the action result                                                     | not configurable, limit per action | MB | 1 |\n\n### Per action timeout (ms) (Default: 60s)\n* The timeout limit N is in the range [100ms..300000ms] and is set per action in milliseconds.\n* A user can change the limit when creating the action.\n* A container that runs longer than N milliseconds is terminated.\n\n### Per action memory (MB) (Default: 256MB)\n* The memory limit M is in the range from [128MB..512MB] and is set per action in MB.\n* A user can change the limit when creating the action.\n* A container cannot have more memory allocated than the limit.\n\n### Per action max instance concurrency (Default: namespace limit for concurrent invocations) **Only applicable using new scheduler**\n* The max containers that will be created for an action before throttling in the range from [1..concurrentInvocations limit for namespace]\n* By default the max allowed containers / server instances for an action is equal to the namespace limit.\n* A user can change the limit when creating the action.\n* Defining a lower limit than the namespace limit means your max container concurrency will be the action defined limit.\n* If using actionConcurrency > 1 such that your action can handle multiple requests per instance, your true concurrency limit is actionContainerConcurrency * actionConcurrency.\n* The actions within a namespaces containerConcurrency total do not have to add up to the namespace limit though you can configure it that way to guarantee an action will get exactly the action container concurrency.\n* For example with a namespace limit of 30 with 2 actions each with a container limit of 20; if the first action is using 20, there will still be space for 10 for the other.\n\n### Per action logs (MB) (Default: 10MB)\n* The log limit N is in the range [0MB..10MB] and is set per action.\n* A user can change the limit when creating or updating the action.\n* Logs that exceed the set limit are truncated and a warning is added as the last output of the activation to indicate that the activation exceeded the set log limit.\n\n### Per action artifact (MB) (Default: 48MB)\n* The maximum code size for the action is 48MB.\n* It is recommended for a JavaScript action to use a tool to concatenate all source code including dependencies into a single bundled file.\n\n### Per activation payload size (MB) (Fixed: 1MB)\n* The maximum POST content size plus any curried parameters for an action invocation or trigger firing is 1MB.\n\n### Per activation result size (MB) (Fixed: 1MB)\n* The maximum size of a result returned from an action is 1MB.\n\n### Per namespace concurrent invocation (Default: 100)\n* The number of activations that are either executing or queued for execution for a namespace cannot exceed 100.\n* A user is currently not able to change the limits.\n\n### Invocations per minute (Fixed: 120)\n* The rate limit N is set to 120 and limits the number of action invocations in one minute windows.\n* A user cannot change this limit when creating the action.\n* A CLI or API call that exceeds this limit receives an error code corresponding to HTTP status code `429: TOO MANY REQUESTS`.\n\n### Size of the parameters (Fixed: 1MB)\n* The size limit for the parameters on creating or updating of an action/package/trigger is 1MB.\n* The limit cannot be changed by the user.\n* An entity with too big parameters will be rejected on trying to create or update it.\n\n### Per Docker action open files ulimit (Fixed: 1024:1024)\n* The maximum number of open files is 1024 (for both hard and soft limits).\n* The docker run command use the argument `--ulimit nofile=1024:1024`.\n* For more information about the ulimit for open files see the [docker run](https://docs.docker.com/engine/reference/commandline/run) documentation.\n\n### Per Docker action processes ulimit (Fixed: 1024)\n* The maximum number of processes available to the action container is 1024.\n* The docker run command use the argument `--pids-limit 1024`.\n* For more information about the ulimit for maximum number of processes see the [docker run](https://docs.docker.com/engine/reference/commandline/run) documentation.\n\n### Triggers\n\nTriggers are subject to a firing rate per minute as documented in the table below.\n\n| limit | description | configurable | unit | default |\n| ----- | ----------- | ------------ | -----| ------- |\n| minuteRate | no more than N triggers may be fired per namespace per minute | per user | number | 60 |\n\n### Triggers per minute (Fixed: 60)\n* The rate limit N is set to 60 and limits the number of triggers that may be fired in one minute windows.\n* A user cannot change this limit when creating the trigger.\n* A CLI or API call that exceeds this limit receives an error code corresponding to HTTP status code `429: TOO MANY REQUESTS`.\n"
  },
  {
    "path": "docs/rest_api.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Using REST APIs with OpenWhisk\n\nAfter your OpenWhisk environment is enabled, you can use OpenWhisk with your web apps or mobile apps with REST API calls.\n\nFor more details about the APIs for actions, activations, packages, rules, and triggers, see the [OpenWhisk API documentation](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/openwhisk/openwhisk/master/core/controller/src/main/resources/apiv1swagger.json).\n\n\nAll the capabilities in the system are available through a REST API. There are collection and entity endpoints for actions, triggers, rules, packages, activations, and namespaces.\n\nThese are the collection endpoints:\n- `https://$APIHOST/api/v1/namespaces`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/actions`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/triggers`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/rules`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/packages`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/activations`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/limits`\n\nThe `$APIHOST` is the OpenWhisk API hostname (for example, localhost, 172.17.0.1, and so on).\nFor the `{namespace}`, the character `_` can be used to specify the user's *default\nnamespace*.\n\nYou can perform a GET request on the collection endpoints to fetch a list of entities in the collection.\n\nThere are entity endpoints for each type of entity:\n- `https://$APIHOST/api/v1/namespaces/{namespace}`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/actions/[{packageName}/]{actionName}`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/triggers/{triggerName}`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/rules/{ruleName}`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/packages/{packageName}`\n- `https://$APIHOST/api/v1/namespaces/{namespace}/activations/{activationName}`\n\nThe namespace and activation endpoints support only GET requests. The actions, triggers, rules, and packages endpoints support GET, PUT, and DELETE requests. The endpoints of actions, triggers, and rules also support POST requests, which are used to invoke actions and triggers and enable or disable rules.\n\nAll APIs are protected with HTTP Basic authentication.\nYou can use the [wskadmin](../tools/admin/wskadmin) tool to generate a new namespace and authentication.\nThe Basic authentication credentials are in the `AUTH` property in your `~/.wskprops` file, delimited by a colon.\nYou can also retrieve these credentials using the CLI running `wsk property get --auth`.\n\n\nThe following is an example that uses the [cURL](https://curl.haxx.se) command tool to get the list of all packages in the `whisk.system` namespace:\n\n```bash\ncurl -u USERNAME:PASSWORD https://$APIHOST/api/v1/namespaces/whisk.system/packages\n```\n\n```json\n[\n  {\n    \"name\": \"slack\",\n    \"binding\": false,\n    \"publish\": true,\n    \"annotations\": [\n      {\n        \"key\": \"description\",\n        \"value\": \"Package that contains actions to interact with the Slack messaging service\"\n      }\n    ],\n    \"version\": \"0.0.1\",\n    \"namespace\": \"whisk.system\"\n  }\n]\n```\n\nIn this example the authentication was passed using the `-u` flag, you can pass this value also as part of the URL as `https://USERNAME:PASSWORD@$APIHOST`.\n\nThe OpenWhisk API supports request-response calls from web clients. OpenWhisk responds to `OPTIONS` requests with Cross-Origin Resource Sharing headers. Currently, all origins are allowed (that is, Access-Control-Allow-Origin is \"`*`\"), the standard set of methods are allowed (that is, Access-Control-Allow-Methods is \"`GET, DELETE, POST, PUT, HEAD`\"), and Access-Control-Allow-Headers yields \"`Authorization, Origin, X-Requested-With, Content-Type, Accept, User-Agent`\".\n\n**Attention:** Because OpenWhisk currently supports only one key per namespace, it is not recommended to use CORS beyond simple experiments. Use [Web Actions](webactions.md) or [API Gateway](apigateway.md) to expose your actions to the public and not use the OpenWhisk authorization key for client applications that require CORS.\n\n## Using the CLI verbose mode\n\nThe OpenWhisk CLI is an interface to the OpenWhisk REST API.\nYou can run the CLI in verbose mode with the flag `-v`, this will print truncated information about the HTTP request and response. To print all information use the flag `-d` for debug.\n\n**Note:** HTTP request and response bodies will only be truncated if they exceed 1000 bytes.\n\nLet's try getting the namespace value for the current user.\n```\nwsk namespace list -v\n```\n```\nREQUEST:\n[GET]  https://$APIHOST/api/v1/namespaces\nReq Headers\n{\n  \"Authorization\": [\n    \"Basic XXXYYYY\"\n  ],\n  \"User-Agent\": [\n    \"OpenWhisk-CLI/1.0 (2017-08-10T20:09:30+00:00)\"\n  ]\n}\nRESPONSE:Got response with code 200\nResp Headers\n{\n  \"Content-Type\": [\n    \"application/json; charset=UTF-8\"\n  ]\n}\nResponse body size is 28 bytes\nResponse body received:\n[\"john@example.com_dev\"]\n```\n\nAs you can see you the printed information provides the properties of the HTTP request, it performs a HTTP method `GET` on the URL `https://$APIHOST/api/v1/namespaces` using a User-Agent header `OpenWhisk-CLI/1.0 (<CLI-Build-version>)` and Basic Authorization header `Basic XXXYYYY`.\nNotice that the authorization value is your base64-encoded OpenWhisk authorization string.\nThe response is of content type `application/json`.\n\n## Actions\n\n**Note:** In the examples that follow, `$AUTH` and `$APIHOST` represent environment variables set respectively to your OpenWhisk authorization key and API host.\n\nTo create or update an action send a HTTP request with method `PUT` on the the actions collection. For example, to create a `nodejs:6` action with the name `hello` using a single file content use the following:\n```bash\ncurl -u $AUTH -d '{\"namespace\":\"_\",\"name\":\"hello\",\"exec\":{\"kind\":\"nodejs:6\",\"code\":\"function main(params) { return {payload:\\\"Hello \\\"+params.name}}\"}}' -X PUT -H \"Content-Type: application/json\" https://$APIHOST/api/v1/namespaces/_/actions/hello?overwrite=true\n```\n\nTo perform a blocking invocation on an action, send a HTTP request with a method `POST` and body containing the input parameter `name` use the following:\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/actions/hello?blocking=true \\\n-X POST -H \"Content-Type: application/json\" \\\n-d '{\"name\":\"John\"}'\n```\nYou get the following response:\n```json\n{\n  \"duration\": 2,\n  \"name\": \"hello\",\n  \"subject\": \"john@example.com_dev\",\n  \"activationId\": \"c7bb1339cb4f40e3a6ccead6c99f804e\",\n  \"publish\": false,\n  \"annotations\": [{\n    \"key\": \"limits\",\n    \"value\": {\n      \"timeout\": 60000,\n      \"memory\": 256,\n      \"logs\": 10\n    }\n  }, {\n    \"key\": \"path\",\n    \"value\": \"john@example.com_dev/hello\"\n  }],\n  \"version\": \"0.0.1\",\n  \"response\": {\n    \"result\": {\n      \"payload\": \"Hello John\"\n    },\n    \"success\": true,\n    \"status\": \"success\"\n  },\n  \"end\": 1493327653769,\n  \"logs\": [],\n  \"start\": 1493327653767,\n  \"namespace\": \"john@example.com_dev\"\n}\n```\nIf you just want to get the `response.result`, run the command again with the query parameter `result=true`\n```bash\ncurl -u $AUTH \"https://$APIHOST/api/v1/namespaces/_/actions/hello?blocking=true&result=true\" \\\n-X POST -H \"Content-Type: application/json\" \\\n-d '{\"name\":\"John\"}'\n```\nYou get the following response:\n```json\n{\n  \"payload\": \"hello John\"\n}\n```\n\n## Annotations and Web Actions\n\nTo create an action as a web action, you need to add an [annotation](annotations.md) of `web-export=true` for web actions. Since web-actions are publicly accessible, you should protect pre-defined parameters (i.e., treat them as final) using the annotation `final=true`. If you create or update an action using the CLI flag `--web true` this command will add both annotations `web-export=true` and `final=true`.\n\nRun the curl command providing the complete list of annotations to set on the action\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/actions/hello?overwrite=true \\\n-X PUT -H \"Content-Type: application/json\" \\\n-d '{\"namespace\":\"_\",\"name\":\"hello\",\"exec\":{\"kind\":\"nodejs:6\",\"code\":\"function main(params) { return {payload:\\\"Hello \\\"+params.name}}\"},\"annotations\":[{\"key\":\"web-export\",\"value\":true},{\"key\":\"raw-http\",\"value\":false},{\"key\":\"final\",\"value\":true}]}'\n```\nYou can now invoke this action as a public URL with no OpenWhisk authorization. Try invoking using the web action public URL including an optional extension such as `.json` or `.http` for example at the end of the URL.\n```bash\ncurl https://$APIHOST/api/v1/web/john@example.com_dev/default/hello.json?name=John\n```\n```json\n{\n  \"payload\": \"Hello John\"\n}\n```\nNote that this example source code will not work with `.http`, see [web actions](webactions.md) documentation on how to modify.\n\n## Sequences\n\nTo create an action sequence, you need to create it by providing the names of the actions that compose the sequence in the desired order, so the output from the first action is passed as input to the next action.\n\n$ wsk action create sequenceAction --sequence /whisk.system/utils/split,/whisk.system/utils/sort\n\nCreate a sequence with the actions `/whisk.system/utils/split` and `/whisk.system/utils/sort`.\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/actions/sequenceAction?overwrite=true \\\n-X PUT -H \"Content-Type: application/json\" \\\n-d '{\"namespace\":\"_\",\"name\":\"sequenceAction\",\"exec\":{\"kind\":\"sequence\",\"components\":[\"/whisk.system/utils/split\",\"/whisk.system/utils/sort\"]},\"annotations\":[{\"key\":\"web-export\",\"value\":true},{\"key\":\"raw-http\",\"value\":false},{\"key\":\"final\",\"value\":true}]}'\n```\n\nTake into account when specifying the names of the actions, they have to be full qualified.\n\n## Triggers\n\nTo create a trigger, the minimum information you need is a name for the trigger. You could also include default parameters that get passed to the action through a rule when the trigger gets fired.\n\nCreate a trigger with name `events` with a default parameter `type` with value `webhook` set.\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/triggers/events?overwrite=true \\\n-X PUT -H \"Content-Type: application/json\" \\\n-d '{\"name\":\"events\",\"parameters\":[{\"key\":\"type\",\"value\":\"webhook\"}]}'\n```\n\nNow whenever you have an event that needs to fire this trigger it just takes an HTTP request with a method `POST` using the OpenWhisk Authorization key.\n\nTo fire the trigger `events` with a parameter `temperature`, send the following HTTP request.\n\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/triggers/events \\\n-X POST -H \"Content-Type: application/json\" \\\n-d '{\"temperature\":60}'\n```\n\n### Triggers with Feed Actions\n\nThere are special triggers that can be created using a feed action. The feed action configures a feed provider such that events from the provider results in triggers being fired. Learn more about these feed providers in the [feeds.md] documentation.\n\nSome of the available triggers that leverage a feed action are periodic/alarms, Slack, Github, Cloudant/Couchdb, and messageHub/Kafka. You also can create your own feed action and feed provider.\n\nLet's create a trigger with name `periodic` to be fired at a specified frequency, every 2 hours (i.e. 02:00:00, 04:00:00, ...).\n\nUsing the CLI this will be done with one command\n```bash\nwsk trigger create periodic --feed /whisk.system/alarms/alarm \\\n  --param cron \"0 */2 * * *\" -v\n```\nAs you will see because we are using the `-v` flag is that two HTTP requests are sent, one is to create a trigger `periodic` and the other is to invoke a feed action `/whisk.system/alarms/alarm` with the parameters to configure the feed provider to fire the trigger every 2 hours.\n\nTo do the same with the REST API, lets create the trigger first\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/triggers/periodic?overwrite=true \\\n-X PUT -H \"Content-Type: application/json\" \\\n-d '{\"name\":\"periodic\",\"annotations\":[{\"key\":\"feed\",\"value\":\"/whisk.system/alarms/alarm\"}]}'\n```\n\nAs you can see the annotation `feed` is stored in the trigger. Later we will use this annotation to know which feed action to use when deleting the trigger.\n\nNow that the trigger is created, lets invoke the feed action\n```bash\ncurl -u $AUTH \"https://$APIHOST/api/v1/namespaces/whisk.system/actions/alarms/alarm?blocking=true&result=false\" \\\n-X POST -H \"Content-Type: application/json\" \\\n-d \"{\\\"authKey\\\":\\\"$AUTH\\\",\\\"cron\\\":\\\"0 */2 * * *\\\",\\\"lifecycleEvent\\\":\\\"CREATE\\\",\\\"triggerName\\\":\\\"/_/periodic\\\"}\"\n```\n\nDeleting the trigger is a similar to creating the trigger, this time deleting the trigger and also using the feed action to configure the feed provider to delete the handler for the trigger.\n\nInvoke the feed action to delete the trigger handler from the feed provider\n```bash\ncurl -u $AUTH \"https://$APIHOST/api/v1/namespaces/whisk.system/actions/alarms/alarm?blocking=true&result=false\" \\\n-X POST -H \"Content-Type: application/json\" \\\n-d \"{\\\"authKey\\\":\\\"$AUTH\\\",\\\"lifecycleEvent\\\":\\\"DELETE\\\",\\\"triggerName\\\":\\\"/_/periodic\\\"}\"\n```\n\nNow delete the trigger with a HTTP request using `DELETE` method\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/triggers/periodic \\\n-X DELETE -H \"Content-Type: application/json\"\n```\n\n## Rules\n\nTo create a rule that associates a trigger with an action, send a HTTP request with a `PUT` method providing the trigger and action in the body of the request.\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/rules/t2a?overwrite=true \\\n-X PUT -H \"Content-Type: application/json\" \\\n-d '{\"name\":\"t2a\",\"status\":\"\",\"trigger\":\"/_/events\",\"action\":\"/_/hello\"}'\n```\n\nRules can be enabled or disabled, and you can change the status of the rule by updating its status property. For example, to disable the rule `t2a` send in the body of the request `status: \"inactive\"` with a `POST` method.\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/rules/t2a?overwrite=true \\\n-X POST -H \"Content-Type: application/json\" \\\n-d '{\"status\":\"inactive\",\"trigger\":null,\"action\":null}'\n```\n\n## Packages\n\nTo create an action in a package you have to create a package first, to create a package with name `iot` send an HTTP request with a `PUT` method\n\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/packages/iot?overwrite=true \\\n-X PUT -H \"Content-Type: application/json\" \\\n-d '{\"namespace\":\"_\",\"name\":\"iot\"}'\n```\n\nTo force delete a package that contains entities, set the force parameter to true. Failure\nwill return an error either for failure to delete an action within the package or the package itself.\nThe package will not be attempted to be deleted until all actions are successfully deleted.\n\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/packages/iot?force=true \\\n-X DELETE\n```\n\n## Activations\n\nTo get the list of the last 3 activations use a HTTP request with a `GET` method, passing the query parameter `limit=3`\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/activations?limit=3\n```\n\nTo get all the details of an activation including results and logs, send a HTTP request with a `GET` method passing the activation identifier as a path parameter\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/activations/f81dfddd7156401a8a6497f2724fec7b\n```\n\n## Limits\n\nTo get the limits set for a namespace (i.e. invocationsPerMinute, concurrentInvocations, firesPerMinute, actionMemoryMax, actionLogsMax...)\n```bash\ncurl -u $AUTH https://$APIHOST/api/v1/namespaces/_/limits\n```\nNote that the default system values are returned if no specific limits are set for the user corresponding to the authenticated identity.\n"
  },
  {
    "path": "docs/samples.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# OpenWhisk samples\nHere is a list of [OpenWhisk community resources](https://github.com/apache/openwhisk-external-resources), which includes more samples, complete OpenWhisk applications, articles, tutorials, podcasts and much more.\n\n<!-- TODO\n\"Complete listing of OpenWhisk samples can be found here.\" <- need to insert a link to the OpenWhisk samples repo when there is one -->\n\n## OpenWhisk Hello World example\nTo get started with OpenWhisk, try the following JavaScript code example.\n\n```\n/**\n * Hello world as an OpenWhisk action.\n */\nfunction main(params) {\n    var name = params.name || 'World';\n    return {payload:  'Hello, ' + name + '!'};\n}\n```\n\nTo use this example, follow these steps:\n\n1. Save the code to a file. For example, *hello.js*.\n\n2. From the OpenWhisk CLI command line, create the action by entering this command:\n\n    ```\n    $ wsk action create hello hello.js\n    ```\n\n3. Then, invoke the action by entering the following commands.\n\n    ```\n    $ wsk action invoke hello --result\n    ```\n\n    This command outputs:\n\n    ```\n    {\n        \"payload\": \"Hello, World!\"\n    }\n    ```\n\n    ```\n    $ wsk action invoke hello --result --param name Fred\n    ```\n\n    This command outputs:\n\n    ```\n    {\n        \"payload\": \"Hello, Fred!\"\n    }\n    ```\n\nYou can also use the event-driven capabilities in OpenWhisk to invoke this action in response to events. Follow the [alarm service example](./packages.md#creating-and-using-trigger-feeds) to configure an event source to invoke the `hello` action every time a periodic event is generated.\n\n## CLI tutorial\n\nIf you prefer learning while doing, consider using this [OpenWhisk Workshop](https://github.com/apache/openwhisk-workshop).\n"
  },
  {
    "path": "docs/security.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Securing your actions\n\nThe actions that you create will run in a sandboxed environment, namely a container. The code that you\nwrite nonetheless should follow best practices to ensure that the code is reasonably secure against remote\ncode exploits and malicious inputs. You should also be cognizant of the packages you bundle and check them\nroutinely for vulnerabilities.\n\nThere are several considerations to be mindful of when authoring actions:\n\n- **Sanitize Function Arguments:** Every invocation of the action receives input arguments which may be from untrusted sources.\n- **Check Dependencies for Vulnerabilities:** When bundling third party dependencies, you should be aware of any vulnerabilities you inherit.\n- **Authenticate Requests:** When using [web actions](webactions.md#securing-web-actions), you can enable built-in authentication to reject unwanted requests.\n- **Seal Parameters:** Parameters with pre-defined values may be sealed when used with [web actions](webactions.md#protected-parameters) to prevent parameter hijacking.\n\nActions which are vulnerable to code injection attacks or parameter hijacking could end up leaking bound\naction parameters, or worse persisting malicious code within the sandbox for the lifetime of the function\nexecution. Moreover, an action sandbox may be reused for more than one function invocation, and hence an\nattacker could persist their code for the lifetime of the sandbox as well.\n"
  },
  {
    "path": "docs/single_entrypoint_proxy_contract.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Action Proxy Single Entrypoint Interface\n\nThe typical endpoints used by the OpenWhisk control plane are not used in single entrypoint execution environments such as Knative. Initialization and running are still essential to how OpenWhisk runtimes function, but they are done in a different methodology than `/init` and `/run` endpoints. The proxy that shapes how the calls are preprocessed and postprocessed to emulate some of the functionality provided by the OpenWhisk control plane. In single entrypoint supported runtime proxy implementations, both initialization and running are done via the `/` root endpoint. The sections below explain the interface the runtime proxy must adhere to initialize and run via a single entrypoint execution environment.\n\n## Init\n\nTo initialize an undifferintiated stem cell, the interface is to pass a JSON object containing the key `init` to the `/` endpoint. The value corresponding to the `init` key is the same JSON object as the [initialization of standard OpenWhisk actions](actions-new.md#initialization). For example:\n```json\n{\n  \"init\": {\n    \"name\" : \"hello\",\n    \"main\" : \"main\",\n    \"code\" : \"function main(params) {return { payload: 'Hello ' + params.name + ' from ' + params.place +  '!' };}\",\n    \"binary\": false,\n    \"env\": {}\n  }\n}\n```\nJust as with the OpenWhisk control plane, specialized function containers need no explicit initialization.\n\n## Run\n\nTo run an action, the interface is to pass a JSON object containing the key `activation` to the `/` endpoint. The value corresponding to the `activation` key is largely the same JSON object as the [activation of standard OpenWhisk actions](actions-new.md#activation). The key difference is that `value` is not used under the `activation` key to pass parameters to the underlying function. To see the interface for passing keys to the underlying functions see section below.\nExample of an activation:\n```json\n{\n  \"activation\": {\n    \"namespace\": \"\",\n    \"action_name\": \"hello\",\n    \"api_host\": \"\",\n    \"api_key\": \"\",\n    \"activation_id\": \"\",\n    \"transaction_id\": \"\",\n    \"deadline\": 1000000\n  },\n  \"value\": {\n    \"name\": \"Alan Turing\",\n    \"place\": \"England\"\n  }\n}\n```\nOne thing to note is when these values are present outside of the context of the OpenWhisk control plane, they may not actually be used for anything. However, the `activation` key is still necessary to signal the intent to run the function.\n\n## Passing parameters\n\nSimilar to the description of the `value` key in the `activation` object during the [activation of standard OpenWhisk actions](actions-new.md#activation), a top level `value` key in the JSON object passed to the `/` endpoint (with a corresponding top level `activation` key) is how parameters are passed to the underlying function being run.\nIn the following example:\n```json\n{\n  \"activation\": {\n    \"namespace\": \"\",\n    \"action_name\": \"hello\",\n    \"api_host\": \"\",\n    \"api_key\": \"\",\n    \"activation_id\": \"\",\n    \"transaction_id\": \"\",\n    \"deadline\": 1000000\n  },\n  \"value\": {\n    \"name\": \"Alan Turing\",\n    \"location\": \"England\"\n  }\n}\n```\n\nThe underlying function would receive a parameters map with the keys `name` and `location` with the values `Alan Turing` and `England` respectively.\n\n## Init/Run\n\nOpenWhisk stem cell runtimes being executed in a single entrypoint execution environment can be both initialized and activated at the same time by passing both `init` and `activation` keys in the same JSON object to the `/` endpoint. This will first initialize the runtime, following the same procedures described above, and then subsequently activate the same runtime.\nFor example:\n```json\n{\n  \"init\": {\n    \"name\" : \"hello\",\n    \"main\" : \"main\",\n    \"code\" : \"function main(params) {return { payload: 'Hello ' + params.name + ' from ' + params.place +  '!' };}\",\n    \"binary\": false,\n    \"env\": {}\n  },\n  \"activation\": {\n    \"namespace\": \"\",\n    \"action_name\": \"hello\",\n    \"api_host\": \"\",\n    \"api_key\": \"\",\n    \"activation_id\": \"\",\n    \"transaction_id\": \"\",\n    \"deadline\": 1000000\n  },\n  \"value\": {\n    \"name\": \"Alan Turing\",\n    \"location\": \"England\"\n  }\n}\n```\nThe above JSON object would instruct the runtime to be initialized with the function under `init.code` and be run with the function being passed the object `{name: \"Alan Turing\", location: \"England\"}`. It would then return the JSON object\n```json\n{\n  \"payload\": \"Hello Alan Turing from England!\"\n}\n```\n\n## Example Cases\nBelow is a table outlining the standardized behaviors that any action proxy implementation needs to fulfill. NodeJS was the sample language used, but corresponding example cases could be written in the language of the corresponding runtime it is showcasing.\n\n\n<table border=\"2\" cellspacing=\"0\" cellpadding=\"6\" rules=\"groups\" frame=\"hsides\">\n\n\n<colgroup>\n<col/>\n\n<col/>\n\n<col/>\n\n<col/>\n\n<col/>\n\n<col/>\n\n<col/>\n</colgroup>\n<thead>\n<tr>\n<th scope=\"col\" >Test Name</th>\n<th scope=\"col\" >Action Code (In NodeJS)</th>\n<th scope=\"col\" >Input</th>\n<th scope=\"col\" >Output</th>\n<th scope=\"col\" >Status code</th>\n<th scope=\"col\" >Mime type</th>\n<th scope=\"col\" >Notes</th>\n<th scope=\"col\" >Environment Variables</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Hello World</td>\n<td>\n  <code><pre>\nfunction main() {\n  return {payload: 'Hello World!'};\n}\n  </pre></code>\n</td>\n<td>\n  <code><pre>\n{\n \"value\": {\n   \"name\": \"Joe\",\n   \"place\": \"TX\"\n }\n}\n  </pre></code>\n</td>\n<td>\n  <code><pre>\n{\n  \"payload\": 'Hello World!'\n}\n  </pre></code>\n</td>\n<td>200</td>\n<td>application/json</td>\n<td>&#xa0;</td>\n</tr>\n</tbody>\n<tbody>\n<tr>\n<td>Hello World With Params</td>\n<td>\n  <code><pre>\nfunction main(params) {\n  return { payload: 'Hello ' + params.name + ' from ' + params.place +  '!' };\n}\n  </pre></code>\n</td>\n<td>\n  <code><pre>\n{\n \"value\": {\n   \"name\": \"Joe\",\n   \"place\": \"TX\"\n }\n}\n  </pre></code>\n</td>\n<td><code><pre>{\n  \"payload\": \"Hello Joe from TX!\"\n}</pre></code></td>\n<td>200</td>\n<td>application/json</td>\n<td>&#xa0;</td>\n</tr>\n</tbody>\n<tbody>\n<tr>\n<td>Web Action Hello World (With Input)</td>\n<td>\n  <code><pre>\nfunction main({name}) {\n  var msg = 'you did not tell me who you are.';\n  if (name) {\n    msg = `hello \\({name}`\n  }\n  return {body: `&lt;html&gt;&lt;body&gt;&lt;h3&gt;{msg}&lt;/h3&gt;&lt;/body&gt;&lt;/html&gt;`}\n}\n  </pre></code>\n</td>\n<td>\n  <code><pre>\n{\n  \"value\": {\n    \"name\": \"Joe\"\n  }\n}</pre></code></td>\n<td><code><pre>\n&lt;html&gt;&lt;body&gt;&lt;h3&gt;hello Joe&lt;/h3&gt;&lt;/body&gt;&lt;/html&gt;\n  </pre></code>\n</td>\n<td >200</td>\n<td >text/html</td>\n<td >&#xa0;</td>\n</tr>\n</tbody>\n<tbody>\n<tr>\n<td>Web Action Hello World (Without Input)</td>\n<td>&lt;Same as above&gt;</td>\n<td>n/a</td>\n<td>\n  <code><pre>\n&lt;html&gt;&lt;body&gt;&lt;h3&gt;you did not tell me who you are.&lt;/h3&gt;&lt;/body&gt;&lt;/html&gt;\n  </pre></code></td>\n<td>200</td>\n<td>text/html</td>\n<td>&#xa0;</td>\n</tr>\n</tbody>\n<tbody>\n<tr>\n<td>Web Action Raw</td>\n<td><code><pre>function main(params) {\n  return { response: params };\n}</pre></code></td>\n<td ><code><pre>{\n  \"value\": {\n    \"name\": \"Joe\"\n  }\n}</pre></code></td>\n<td ><code><pre>{\n  \"response\": {\n    \"__ow_body\": \"eyJuYW1lIjoiSm9lIn0=\",\n    \"__ow_query\": {},\n    \"__ow_user\": \"\",\n    \"__ow_method\": \"POST\",\n    \"__ow_headers\": {\n      \"host\": \"localhost\",\n      \"user-agent\": \"curl/7.54.0\",\n      \"accept\": \"*/*\",\n      \"content-type\": \"application/json\",\n      \"content-length\": \"394\"\n    },\n    \"__ow_path\": \"\"\n  }\n}</td>\n<td >200</td>\n<td >application/json</td>\n<td >OpenWhisk controller plays an important role in handling web actions and that's why when run from OpenWhisk the response is lacking _<sub>ow</sub><sub>*</sub> parameters</td>\n<td>__OW_ACTION_RAW=true</td>\n</tr>\n</tbody>\n<tbody>\n<tr>\n<td >run sample with init that does nothing</td>\n<td >No code Init'd</td>\n<td >No Input</td>\n<td >No Output</td>\n<td >Init should 403</br>Run should 500</td>\n<td ></td>\n<td >&#xa0;</td>\n</tr>\n</tbody>\n<tbody>\n<tr>\n<td >deploy a zip based script</td>\n<td >Zipped version of Hello World:<code><pre>\nfunction main() {\n  return {payload: 'Hello World!'};\n}</pre></code></td>\n<td ><code><pre>\n{\n \"value\": {\n   \"name\": \"Joe\",\n   \"place\": \"TX\"\n }\n</td>\n<td ><code><pre>{\n  \"payload\": 'Hello World!'\n}</pre><code></td>\n<td >Init should 200</br>Run should 200</td>\n<td >application/json</td>\n<td >&#xa0;</td>\n</tr>\n</tbody>\n<tbody>\n<tr>\n<td >accept a src not-main action</td>\n<td >__OW_ACTION_MAIN set to hello:<code><pre>\nfunction hello() {\n  return {payload: 'Hello World!'};\n}</pre></code></td>\n<td ><code><pre>\n{\n \"value\": {\n   \"name\": \"Joe\",\n   \"place\": \"TX\"\np }\n</td>\n<td ><code><pre>{\n  \"payload\": 'Hello World!'\n}</pre><code></td>\n<td >Init should 200</br>Run should 200</td>\n<td >application/json</td>\n<td >&#xa0;</td>\n<td> __OW_ACTION_MAIN=hello\n</tr>\n  </tbody>\n<tbody>\n<tr>\n<td >accept a zipped src not-main action</td>\n<td >__OW_ACTION_MAIN set to hello and zipped:<code><pre>\nfunction hello() {\n  return {payload: 'Hello World!'};\n}</pre></code></td>\n<td ><code><pre>\n{\n \"value\": {\n   \"name\": \"Joe\",\n   \"place\": \"TX\"\n }\n</td>\n<td ><code><pre>{\n  \"payload\": 'Hello World!'\n}</pre><code></td>\n<td >Init should 200</br>Run should 200</td>\n<td >application/json</td>\n<td >&#xa0;</td>\n<td>__OW_ACTION_MAIN=hello\n</tr>\n</tbody>\n</table>\n\n## Implementations\n\n### [NodeJS](https://github.com/apache/openwhisk-runtime-nodejs)\n---\nThe links below will point to the [OpenWhisk Test repo](https://github.com/apache/openwhisk-test/) where the example cases are being stored.\n\n| Action Code                                                                                                                                                   | Init                                                                                                                                                                          | Run                                                                                                                                                                         | Init/Run                                                                                                                                                                              | Output                                                                                                                                                               |\n|---------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| [Hello World](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/hello_world.js)                                           | [hello_world-init](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/hello_world-init.json)                                         | [hello_world-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/hello_world-run.json)                                         | [hello_world-init-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/hello_world-init-run.json)                                         | [hello_world](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/output/hello_world.json)                                         |\n| [Hello World with Params](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/hello_world_with_params.js)                   | [hello_world_with_params-init](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/hello_world_with_params-init.json)                 | [hello_world_with_params-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/hello_world_with_params-run.json)                 | [hello_world_with_params-init-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/hello_world_with_params-init-run.json)                 | [hello_world_with_params](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/output/hello_world_with_params.json)                 |\n| [Web Action Hello World](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/web_action_hello_world.js)                     | [web_action_hello_world-init](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/web_action_hello_world-init.json)                   | [web_action_hello_world-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/web_action_hello_world-run.json)                   | [web_action_hello_world-init-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/web_action_hello_world-init-run.json)                   | [web_action_hello_world](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/output/web_action_hello_world.json)                   |\n| [Web Action Hello World (no input)](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/web_action_hello_world_no_input.js) | [web_action_hello_world_no_input-init](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/web_action_hello_world_no_input-init.json) | [web_action_hello_world_no_input-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/web_action_hello_world_no_input-run.json) | [web_action_hello_world_no_input-init-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/web_action_hello_world_no_input-init-run.json) | [web_action_hello_world_no_input](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/output/web_action_hello_world_no_input.json) |\n| [Web Action Raw](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/web_action_raw.js)                                     | [web_action_raw-init](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/web_action_raw-init.json)                                   | [web_action_raw-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/web_action_raw-run.json)                                   | [web_action_raw-init-run](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/input/web_action_raw-init-run.json)                                   | [web_action_raw](https://github.com/apache/openwhisk-test/blob/master/runtimes/proxy/single_entrypoint/output/web_action_raw.json)                                   |\n"
  },
  {
    "path": "docs/spi.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# SPI extensions in OpenWhisk\n\nAlternate implementations of various components follow an SPI (Service Provider Interface) pattern:\n* The pluggable component is defined as an SPI trait:\n```scala\nimport org.apache.openwhisk.spi.Spi\ntrait ThisIsPluggable extends Spi { ... }\n```\n* Implementations implement the Spi trait\n```scala\nclass TheImpl extends ThisIsPluggable { ... }\nclass TheOtherImpl extends ThisIsPluggable { ... }\n```\n\nRuntime resolution of an SPI trait to a specific implementation is provided by:\n* `SpiLoader` - a utility for loading the implementation of a specific SPI, using a resolver to determine the implementations factory classname, and reflection to load the factory object.\n* `application.conf` - each `SPI` is resolved to a classname based on the config key provided to `SpiLoader`.\n\nOnly a single implementation per SPI is usable at runtime, since the key will have a single string value.\n\n# Example\n\nThe process to create and use an SPI is as follows:\n\n## Define the SPI and implementations\n\n* Create your SPI trait `YourSpi` as a trait that is an extension of `whisk.spi.Spi`.\n* Create your factory object which extends `YourSpi` and provides the relevant functionality.\n* Create your functionality classes with whatever name you like. The factory object is supposed to build and return those instances.\n\n## Invoke SpiLoader.get to acquire an instance of the SPI\n\nSpiLoader uses a TypesafeConfig key to use for resolving which implementation should be loaded.\n\nThe config key used to find the implementations classname is `whisk.spi.<SpiInterface>`.\n\nFor example, the SPI interface `org.apache.openwhisk.core.database.ArtifactStoreProvider` would load a specific implementation indicated by the  `whisk.spi.ArtifactStoreProvider` config key.\n\n(so you cannot use multiple SPI interfaces with the same class name in different packages)\n\n\nInvoke the loader using `SpiLoader.get[<the SPI interface>](<implicit resolver>)`\n\n```scala\nval messagingProvider = SpiLoader.get[MessagingProvider]\n```\n\n## Defaults\n\nDefault implementation resolution is dependent on the config values in order of priority from:\n\n1. `application.conf`\n2. `reference.conf`\n\nSo use `reference.conf` to specify defaults.\n\n# Runtime\n\nSince SPI implementations are loaded from the classpath, and a specific implementation is used only if explicitly configured it is possible to optimize the classpath based on your preference of:\n\n* Include only default implementations, and only use default implementations.\n* Include all implementations, and only use the specified implementations.\n* Include some combination of defaults and alternative implementations, and use the specified implementations for the alternatives, and default implementations for the rest.\n\n## Including the implementation\n\nBase OpenWhisk docker images provide 2 extension points in the classpath for including the implementation.\n\n### Application Jars\n\nThe application jars can be added to `$APP_HOME/ext-lib` for e.g. in `openwhisk/controller` image the implementation jars can be added to `/controller/ext-lib` and for `openwhisk/invoker` they can be added to `/invoker/ext-lib`.\n\n### Application Configuration\n\nThe configuration files can be added to `$APP_HOME/config`.\n"
  },
  {
    "path": "docs/tag-based-scheduling.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Tag-based Scheduling\n\nInvoker machines may have different resources such as GPU, high CPU, etc.\nFor those who want to take advantage of such resources, the system should be able to schedule activations to a certain invoker with the resources.\n\n## Tagging invokers\n\nOperators can configure any tags for invokers.\n\n```bash\ninvoker0 ansible_host=${INVOKER0} tags=\"['v1', 'gpu']\"\ninvoker1 ansible_host=${INVOKER1} tags=\"['v1', 'cpu']\"\ninvoker2 ansible_host=${INVOKER2} tags=\"['v2', 'gpu']\"\ninvoker3 ansible_host=${INVOKER3} tags=\"['v2', 'cpu']\"\ninvoker4 ansible_host=${INVOKER4} tags=\"['v1', 'mem']\"\ninvoker5 ansible_host=${INVOKER5} tags=\"['v2', 'mem']\"\ninvoker6 ansible_host=${INVOKER6} tags=\"['v2']\"\ninvoker7 ansible_host=${INVOKER7} tags=\"['v2']\"\ninvoker8 ansible_host=${INVOKER8}\ninvoker9 ansible_host=${INVOKER9}\n```\n\nUsers can add the following annotations to their actions.\n\n```\nwsk action update params tests/dat/actions/params.js -i -a invoker-resources '[\"v2\", \"gpu\"]'\n```\n\nActivation for this action will be delivered to invoker2.\n\nThe annotations and the corresponding target invokers are as follows.\n\n* `[\"v1\", \"gpu\"]` -> `invoker0`\n* `[\"v2\", \"gpu\"]` -> `invoker2`\n* `[\"v1\", \"cpu\"]` -> `invoker1`\n* `[\"v2\"]` -> One of `invoker2`, `invoker3`, `invoker5`, `invoker6`, and `invoker7`\n* `[\"v1\"]` -> One of `invoker0`, `invoker1`, `invoker4`\n* `No annotation` -> One of `invoker8` and `invoker9` is chosen first. if they have no resource, choose one of the invokers with tags.\n"
  },
  {
    "path": "docs/triggers_rules.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Creating triggers and rules\n\nOpenWhisk triggers and rules bring event-driven capabilities to the platform. Events from external and internal event sources are channeled through a trigger, and rules allow your actions to react to these events.\n\n## Creating triggers\n\nTriggers are a named channel for a class of events. The following are examples of triggers:\n- A trigger of location update events.\n- A trigger of document uploads to a website.\n- A trigger of incoming emails.\n\nTriggers can be *fired* (activated) by using a dictionary of key-value pairs. Sometimes this dictionary is referred to as the *event*. As with actions, each firing of a trigger results in an activation ID.\n\nTriggers can be explicitly fired by a user or fired on behalf of a user by an external event source.\nA *feed* is a convenient way to configure an external event source to fire trigger events that can be consumed by OpenWhisk. Examples of feeds include the following:\n- Cloudant data change feed that fires a trigger event each time a document in a database is added or modified.\n- A Git feed that fires a trigger event for every commit to a Git repository.\n\n## Using rules\n\nA rule associates one trigger with one action, with every firing of the trigger causing the corresponding action to be invoked with the trigger event as input.\n\nWith the appropriate set of rules, it's possible for a single trigger event to\ninvoke multiple actions, or for an action to be invoked as a response to events\nfrom multiple triggers.\n\nFor example, consider a system with the following actions:\n- `classifyImage` action that detects the objects in an image and classifies them.\n- `thumbnailImage` action that creates a thumbnail version of an image.\n\nAlso, suppose that there are two event sources that are firing the following triggers:\n- `newTweet` trigger that is fired when a new tweet is posted.\n- `imageUpload` trigger that is fired when an image is uploaded to a website.\n\nYou can set up rules so that a single trigger event invokes multiple actions, and have multiple triggers invoke the same action:\n- `newTweet -> classifyImage` rule.\n- `imageUpload -> classifyImage` rule.\n- `imageUpload -> thumbnailImage` rule.\n\nThe three rules establish the following behavior: images in both tweets and uploaded images are classified, uploaded images are classified, and a thumbnail version is generated.\n\n## Creating and firing triggers\n\nTriggers can be fired when certain events occur, or can be fired manually.\n\nAs an example, create a trigger to send user location updates, and manually fire the trigger.\n\n1. Enter the following command to create the trigger:\n\n  ```\n  $ wsk trigger create locationUpdate\n  ```\n\n  ```\n  ok: created trigger locationUpdate\n  ```\n\n2. Check that you created the trigger by listing the set of triggers.\n\n  ```\n  $ wsk trigger list\n  ```\n\n  ```\n  triggers\n  /someNamespace/locationUpdate                            private\n  ```\n\n  So far you've created a named \"channel\" to which events can be fired.\n\n3. Next, fire a trigger event by specifying the trigger name and parameters:\n\n  ```\n  $ wsk trigger fire locationUpdate --param name Donald --param place \"Washington, D.C.\"\n  ```\n\n  ```\n  ok: triggered locationUpdate with id fa495d1223a2408b999c3e0ca73b2677\n  ```\n\nA trigger that is fired without an accompanying rule to match against has no visible effect.\nTriggers cannot be created inside a package; they must be created directly under a namespace.\n\n## Associating triggers and actions by using rules\n\nRules are used to associate a trigger with an action. Each time a trigger event is fired, the action is invoked with the event parameters.\n\nAs an example, create a rule that calls the hello action whenever a location update is posted.\n\n1. Create a 'hello.js' file with the action code we will use:\n  ```\n  function main(params) {\n     return {payload:  'Hello, ' + params.name + ' from ' + params.place};\n  }\n  ```\n\n2. Make sure that the trigger and action exist.\n  ```\n  $ wsk trigger update locationUpdate\n  ```\n\n  ```\n  $ wsk action update hello hello.js\n  ```\n\n3. Create the rule. Note that the rule will be enabled upon creation, meaning that it will be immediately available to respond to activations of your trigger. The three parameters are the name of the rule, the trigger, and the action.\n  ```\n  $ wsk rule create myRule locationUpdate hello\n  ```\n\n  At any time, you can choose to disable a rule.\n  ```\n  $ wsk rule disable myRule\n  ```\n\n4. Fire the locationUpdate trigger. Each time that you fire an event, the hello action is called with the event parameters.\n  ```\n  $ wsk trigger fire locationUpdate --param name Donald --param place \"Washington, D.C.\"\n  ```\n\n  ```\n  ok: triggered locationUpdate with id 878998285cad448b8998285cad948b30\n  ```\n\n5. Verify that the action was invoked by checking the most recent activation.\n  ```\n  $ wsk activation list --limit 1 hello\n  ```\n\n<pre>\nDatetime            Activation ID                    Kind     Start Duration   Status  Entity\n2019-02-18 11:51:41 0efe54d8fb96486bbe54d8fb96d86bbe nodejs:6 cold  54ms       success guest/hello:0.0.1\n</pre>\n\n  ```\n  $ wsk activation result 0efe54d8fb96486bbe54d8fb96d86bbe\n  ```\n  ```\n  {\n     \"payload\": \"Hello, Donald from Washington, D.C.\"\n  }\n  ```\n\n  You see that the hello action received the event payload and returned the expected string.\n\nYou can create multiple rules that associate the same trigger with different actions.\nTriggers and rules cannot belong to a package. The rule may be associated with an action\nthat belongs to a package however, for example:\n  ```\n  $ wsk rule create recordLocation locationUpdate /whisk.system/utils/echo\n  ```\n\nYou can also use rules with sequences. For example, one can create an action\nsequence `recordLocationAndHello` that is activated by the rule `anotherRule`.\n  ```\n  $ wsk action create recordLocationAndHello --sequence /whisk.system/utils/echo,hello\n  $ wsk rule create anotherRule locationUpdate recordLocationAndHello\n  ```\n"
  },
  {
    "path": "docs/use_cases.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Common use cases\n\nThe execution model that is offered by OpenWhisk supports a variety of use cases. The following sections include typical examples. For a more detailed discussion of Serverless architecture, example uses cases, pros and cons discussion and implementation best practices, please read excellent [Mike Roberts article on Martin Fowler's blog](https://martinfowler.com/articles/serverless.html).\n\n## Microservices\n\nDespite their benefit, microservice-based solutions remain difficult to build using mainstream cloud technologies, often requiring control of a complex toolchain, and separate build and operations pipelines. Small and agile teams, spending too much time dealing with infrastructural and operational complexities (fault-tolerance, load balancing, auto-scaling, and logging), especially want a way to develop streamlined, value-adding code with programming languages they already know and love and that are best suited to solve particular problems.\n\nThe modular and inherently scalable nature of OpenWhisk makes it ideal for implementing granular pieces of logic in actions. OpenWhisk actions are independent of each other and can be implemented using variety of different languages supported by OpenWhisk and access various backend systems. Each action can be independently deployed and managed, is scaled independently of other actions. Interconnectivity between actions is provided by OpenWhisk in the form of rules, sequences and naming conventions. This bodes well for microservices based applications.\n\n## Web apps\n\nEven though OpenWhisk was originally designed for event based programming, it offers several benefits for user-facing applications. For example, when you combine it with a small Node.js stub, you can use it to serve applications that are relatively easy to debug. And because OpenWhisk applications are a lot less computationally intensive than running a server process on a PaaS platform, they are considerably cheaper, as well.\n\nFull web applications can be built and run with OpenWhisk. Combining serverless APIs with static file hosting for site resources, e.g. HTML, JavaScript and CSS, means we can build entire serverless web applications. The simplicity of operating a hosted OpenWhisk environment (or rather not having to operate anything at all since it is hosted on IBM Cloud) is a great benefit compared to standing up and operating a Node.js Express or other traditional server runtime.\n\nOne of the things that helps is the option of OpenWhisk CLI *wsk* tool called \"--annotation web-export true\", which makes the code accessible from a web browser.\n\nHere are few examples on how to use OpenWhisk to build a web app:\n- [Web Actions: Serverless Web Apps with OpenWhisk](https://medium.com/openwhisk/web-actions-serverless-web-apps-with-openwhisk-f21db459f9ba).\n- [Build a user-facing OpenWhisk application with IBM Cloud Functions and Node.js](https://www.ibm.com/developerworks/cloud/library/cl-openwhisk-node-bluemix-user-facing-app/index.html)\n- [Serverless HTTP handlers with OpenWhisk](https://medium.com/openwhisk/serverless-http-handlers-with-openwhisk-90a986cc7cdd)\n\n## IoT\n\nIt is certainly possible to implement IoT applications using traditional server architectures, however in many cases the combination of different services and data bridges requires high performance and flexible pipelines, spanning from IoT devices up to cloud storage and an analytics platform. Often pre-configured bridges lack the programmability required to implement and fine-tune a particular solution architecture. Given the huge variety of possible pipelines and the lack of standardization around data fusion in general and in IoT in particular, there are many cases where the pipeline requires custom data transformation (for format conversion, filtering, augmentation, etc). OpenWhisk is an excellent tool to implement such a transformation, in a ‘serverless’ manner, where the custom logic is hosted on a fully managed and elastic cloud platform.\n\nInternet of Things scenarios are often inherently sensor-driven. For example, an action in OpenWhisk might be triggered if there is a need to react to a sensor that is exceeding a particular temperature. IoT interactions are usually stateless with potential for very high level of load in case of major events (natural disasters, significant weather events, traffic jams, etc.) This creates a need for an elastic system where normal workload might be small, but needs to scale very quickly with predictable response time and ability to handle extremely large number of events with no prior warning to the system. It is very hard to build a system to meet these requirements using traditional server architectures as they tend to either be underpowered and unable to handle peak in traffic or be over-provisioned and extremely expensive.\n\nHere is a sample IoT application that uses OpenWhisk, NodeRed, Cognitive and other services: [Serverless transformation of IoT data-in-motion with OpenWhisk](https://medium.com/openwhisk/serverless-transformation-of-iot-data-in-motion-with-openwhisk-272e36117d6c#.akt3ocjdt).\n\n![IoT solution architecture example](images/IoT_solution_architecture_example.png)\n\n## API backend\n\nServerless computing platforms give developers a rapid way to build APIs without servers. OpenWhisk supports automatic generation of REST API for actions and it is very easy to connect your API Management tool of choice (such as [IBM API Connect](https://www-03.ibm.com/software/products/en/api-connect) or other) to these REST APIs provided by OpenWhisk. Similar to other use cases, all considerations for scalability, and other Qualities of Services (QoS) apply.\n\nHere is an example and a discussion of [using Serverless as an API backend](https://martinfowler.com/articles/serverless.html#ACoupleOfExamples).\n\n## Mobile back end\n\nMany mobile applications require server-side logic. For mobile developers that don't want to manage server-side logic and would rather focus on the app that is running on the device or browser, using OpenWhisk as the server-side back end is a good solution. In addition, the built-in support for Swift allows developers to reuse their existing iOS programming skills. Mobile applications often have unpredictable load patterns and hosted OpenWhisk solution, such as IBM Cloud Functions, can scale to meet practically any demand in workload without the need to provision resources ahead of time.\n\n## Data processing\n\nWith the amount of data now available, application development requires the ability to process new data, and potentially react to it. This requirement includes processing both structured database records as well as unstructured documents, images, or videos. OpenWhisk can be configured via system provided or custom feeds to react to changes in data and automatically execute actions on the incoming feeds of data. Actions can be programmed to process changes, transform data formats, send and receive messages, invoke other actions, update various data stores, including SQL based relational databases, in-memory data grids, NoSQL database, files, messaging brokers and variety of other systems. OpenWhisk rules and sequences provide flexibility to make changes in processing pipeline without programming - simply via configuration changes. This makes OpenWhisk based system highly agile and easily adaptable to changing requirements.\n\n## Cognitive\n\nCognitive technologies can be effectively combined with OpenWhisk to create powerful applications. For example, IBM Alchemy API and Watson Visual Recognition can be used with OpenWhisk to automatically extract useful information from videos without having to actually watch them.\n\nHere is a sample application [Dark vision](https://github.com/IBM-Cloud/openwhisk-darkvisionapp) that does just that. In this application the user uploads a video or image using the Dark Vision web application, which stores it in a Cloudant DB. Once the video is uploaded, OpenWhisk detects the new video by listening to Cloudant changes (trigger). OpenWhisk then triggers the video extractor action. During its execution, the extractor produces frames (images) and stores them in Cloudant. The frames are then processed using Watson Visual Recognition and the results are stored in the same Cloudant DB. The results can be viewed using Dark Vision web application OR an iOS application. Object Storage can be used in addition to Cloudant. When doing so, video and image metadata are stored in Cloudant and the media files are stored in Object Storage.\n"
  },
  {
    "path": "docs/warmed-containers.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Warmed Containers\n\nWarmed containers can improve their performance by skipping the container initialization step.\nIt may be beneficial to configure the number of warmed containers to keep, and the duration to keep them according to the characteristics of workloads.\n\n## Configuration\n\nThe configurations are only effective with the FPC scheduler.\nThey can be configured using the limit configurations for each namespace.\n\n```scala\ncase class UserLimits(invocationsPerMinute: Option[Int] = None,\n                      concurrentInvocations: Option[Int] = None,\n                      firesPerMinute: Option[Int] = None,\n                      allowedKinds: Option[Set[String]] = None,\n                      storeActivations: Option[Boolean] = None,\n                      warmedContainerKeepingCount: Option[Int] = None,\n                      warmedContainerKeepingTimeout: Option[String] = None)\n```\n\nSo those can be configured in the same way that operators configure the `invocationsPerMinute` limit.\n\n```json\n{\n  \"_id\": \"guest/limits\",\n  \"invocationsPerMinute\": 10,\n  \"warmedContainerKeepingCount\": 8,\n  \"warmedContainerKeepingTimeout\": \"24 hours\"\n}\n```\n\nThe namespace-specific configurations would override the default, system-wide configurations.\nIn the above example, the system will keep 8 warmed containers for 24 hours even if there is no activation at all.\n"
  },
  {
    "path": "docs/webactions.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Web Actions\n\nWeb actions are OpenWhisk actions annotated to quickly enable you to build web based applications. This allows you to program backend logic which your web application can access anonymously without requiring an OpenWhisk authentication key. It is up to the action developer to implement their own desired authentication and authorization (i.e. OAuth flow).\n\nWeb action activations will be associated with the user that created the action. This actions defers the cost of an action activation from the caller to the owner of the action.\n\nLet's take the following JavaScript action `hello.js`,\n```javascript\n$ cat hello.js\nfunction main({name}) {\n  var msg = 'you did not tell me who you are.';\n  if (name) {\n    msg = `hello ${name}!`\n  }\n  return {body: `<html><body><h3>${msg}</h3></body></html>`}\n}\n```\n\nYou may create a _web action_ `hello` in the package `demo` for the namespace `guest` using the CLI's `--web` flag with a value of `true` or `yes`:\n```bash\n$ wsk package create demo\nok: created package demo\n```\n\n```\n$ wsk action create /guest/demo/hello hello.js --web true\nok: created action /guest/demo/hello\n```\n\n```\n$ wsk action get /guest/demo/hello --url\nok: got action hello\nhttps://${APIHOST}/api/v1/web/guest/demo/hello\n```\n\nUsing the `--web` flag with a value of `true` or `yes` allows an action to be accessible via REST interface without the\nneed for credentials. To configure a web action with credentials refer to [Securing web actions](webactions.md#securing-web-actions). A web action can be invoked using a URL that is structured as follows:\n`https://{APIHOST}/api/v1/web/{QUALIFIED ACTION NAME}.{EXT}`. The fully qualified name of an action consists of three\nparts: the namespace, the package name, and the action name.\n\n*The fully qualified name of the action must include its package name, which is `default` if the action is not in a named package.*\n\nAn example is `guest/demo/hello`. The last part of the URI called the `extension` which is typically `.http` although other values are permitted as described later. The web action API path may be used with `curl` or `wget` without an API key. It may even be entered directly in your browser.\n\nTry opening [https://${APIHOST}/api/v1/web/guest/demo/hello.http?name=Jane](https://${APIHOST}/api/v1/web/guest/demo/hello.http?name=Jane) in your web browser. Or try invoking the action via `curl`:\n```\n$ curl https://${APIHOST}/api/v1/web/guest/demo/hello.http?name=Jane\n```\n\nHere is an example of a web action that performs an HTTP redirect:\n```javascript\nfunction main() {\n  return {\n    headers: { location: 'http://openwhisk.org' },\n    statusCode: 302\n  }\n}\n```\n\nOr sets a cookie:\n```javascript\nfunction main() {\n  return {\n    headers: {\n      'Set-Cookie': 'UserID=Jane; Max-Age=3600; Version=',\n      'Content-Type': 'text/html'\n    },\n    statusCode: 200,\n    body: '<html><body><h3>hello</h3></body></html>' }\n}\n```\n\nOr sets multiple cookies:\n```javascript\nfunction main() {\n  return {\n    headers: {\n      'Set-Cookie': [\n        'UserID=Jane; Max-Age=3600; Version=',\n        'SessionID=asdfgh123456; Path = /'\n      ],\n      'Content-Type': 'text/html'\n    },\n    statusCode: 200,\n    body: '<html><body><h3>hello</h3></body></html>' }\n}\n```\n\nOr returns an `image/png`:\n```javascript\nfunction main() {\n    let png = <base 64 encoded string>\n    return { headers: { 'Content-Type': 'image/png' },\n             statusCode: 200,\n             body: png };\n}\n```\n\nOr returns `application/json`:\n```javascript\nfunction main(params) {\n    return {\n        statusCode: 200,\n        headers: { 'Content-Type': 'application/json' },\n        body: params\n    };\n}\n```\n\nThe default content-type for an HTTP response is `application/json` and the body may be any allowed JSON value. The default content-type may be omitted from the headers.\n\nIt is important to be aware of the [response size limit](reference.md) for actions since a response that exceeds the predefined system limits will fail. Large objects should not be sent inline through OpenWhisk, but instead deferred to an object store, for example.\n\n## Handling HTTP requests with actions\n\nAn OpenWhisk action that is not a web action requires both authentication and must respond with a JSON object. In contrast, web actions may be invoked without authentication, and may be used to implement HTTP handlers that respond with _headers_, _statusCode_, and _body_ content of different types. The web action must still return a JSON object, but the OpenWhisk system (namely the `controller`) will treat a web action differently if its result includes one or more of the following as top level JSON properties:\n\n1. `headers`: a JSON object where the keys are header-names and the values are string, number, or boolean values for those headers (default is no headers). To send multiple values for a single header, the header's value should be a JSON array of values.\n1. `statusCode`: a valid HTTP status code (default is 200 OK if body is not empty otherwise 204 No Content).\n1. `body`: a string which is either plain text, JSON object or array, or a base64 encoded string for binary data (default is empty response).\n\nThe `body` is considered empty if it is `null`, the empty string `\"\"` or undefined.\n\nThe controller will pass along the action-specified headers, if any, to the HTTP client when terminating the request/response. Similarly the controller will respond with the given status code when present. Lastly, the body is passed along as the body of the response. If a `content-type header` is not declared in the action result’s `headers`, the body is interpreted as `application/json` for non-string values, and `text/html` otherwise. When the `content-type` is defined, the controller will determine if the response is binary data or plain text and decode the string using a base64 decoder as needed. Should the body fail to decoded correctly, an error is returned to the caller.\n\n## HTTP Context\n\nAll web actions, when invoked, receives additional HTTP request details as parameters to the action input argument. They are:\n\n1. `__ow_method` (type: string): the HTTP method of the request.\n1. `__ow_headers` (type: map string to string): the request headers.\n1. `__ow_path` (type: string): the unmatched path of the request (matching stops after consuming the action extension).\n1. `__ow_user` (type: string): the namespace identifying the OpenWhisk authenticated subject.\n1. `__ow_body` (type: string): the request body entity, as a base64 encoded string when content is binary or JSON object/array, or plain string otherwise.\n1. `__ow_query` (type: string): the query parameters from the request as an unparsed string.\n\nA request may not override any of the named `__ow_` parameters above; doing so will result in a failed request with status equal to 400 Bad Request.\n\nThe `__ow_user` is only present when the web action is [annotated to require authentication](annotations.md#annotations-specific-to-web-actions) and allows a web action to implement its own authorization policy. The `__ow_query` is available only when a web action elects to handle the [\"raw\" HTTP request](#raw-http-handling). It is a string containing the query parameters parsed from the URI (separated by `&`). The `__ow_body` property is present either when handling \"raw\" HTTP requests, or when the HTTP request entity is not a JSON object or form data. Web actions otherwise receive query and body parameters as first class properties in the action arguments with body parameters taking precedence over query parameters, which in turn take precedence over action and package parameters.\n\n## Additional features\n\nWeb actions bring some additional features that include:\n\n1. `Content extensions`: the request must specify its desired content type as one of `.json`, `.html`, `.http`, `.svg` or `.text`. This is done by adding an extension to the action name in the URI, so that an action `/guest/demo/hello` is referenced as `/guest/demo/hello.http` for example to receive an HTTP response back. For convenience, the `.http` extension is assumed when no extension is detected.\n1. `Query and body parameters as input`: the action receives query parameters as well as parameters in the request body. The precedence order for merging parameters is: package parameters, binding parameters, action parameters, query parameter, body parameters with each of these overriding any previous values in case of overlap . As an example `/guest/demo/hello.http?name=Jane` will pass the argument `{name: \"Jane\"}` to the action.\n1. `Form data`: in addition to the standard `application/json`, web actions may receive URL encoded from data `application/x-www-form-urlencoded data` as input.\n1. `Activation via multiple HTTP verbs`: a web action may be invoked via any of these HTTP methods: `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`, as well as `HEAD` and `OPTIONS`.\n1. `Non JSON body and raw HTTP entity handling`: A web action may accept an HTTP request body other than a JSON object, and may elect to always receive such values as opaque values (plain text when not binary, or base64 encoded string otherwise).\n\nThe example below briefly sketches how you might use these features in a web action. Consider an action `/guest/demo/hello` with the following body:\n```javascript\nfunction main(params) {\n    return { response: params };\n}\n```\n\nThis is an example of invoking the web action using the `.json` extension, indicating a JSON response.\n\n```bash\n$ curl https://${APIHOST}/api/v1/web/guest/demo/hello.json\n{\n  \"response\": {\n    \"__ow_method\": \"get\",\n    \"__ow_headers\": {\n      \"accept\": \"*/*\",\n      \"connection\": \"close\",\n      \"host\": \"172.17.0.1\",\n      \"user-agent\": \"curl/7.43.0\"\n    },\n    \"__ow_path\": \"\"\n  }\n}\n```\n\nYou can supply query parameters.\n\n```bash\n$ curl https://${APIHOST}/api/v1/web/guest/demo/hello.json?name=Jane\n{\n  \"response\": {\n    \"name\": \"Jane\",\n    \"__ow_method\": \"get\",\n    \"__ow_headers\": {\n      \"accept\": \"*/*\",\n      \"connection\": \"close\",\n      \"host\": \"172.17.0.1\",\n      \"user-agent\": \"curl/7.43.0\"\n    },\n    \"__ow_path\": \"\"\n  }\n}\n```\n\nYou may use form data as input.\n\n```bash\n$ curl https://${APIHOST}/api/v1/web/guest/demo/hello.json -d \"name\":\"Jane\"\n{\n  \"response\": {\n    \"name\": \"Jane\",\n    \"__ow_method\": \"post\",\n    \"__ow_headers\": {\n      \"accept\": \"*/*\",\n      \"connection\": \"close\",\n      \"content-length\": \"10\",\n      \"content-type\": \"application/x-www-form-urlencoded\",\n      \"host\": \"172.17.0.1\",\n      \"user-agent\": \"curl/7.43.0\"\n    },\n    \"__ow_path\": \"\"\n  }\n}\n```\n\nYou may also invoke the action with a JSON object.\n\n```bash\n$ curl https://${APIHOST}/api/v1/web/guest/demo/hello.json -H 'Content-Type: application/json' -d '{\"name\":\"Jane\"}'\n{\n  \"response\": {\n    \"name\": \"Jane\",\n    \"__ow_method\": \"post\",\n    \"__ow_headers\": {\n      \"accept\": \"*/*\",\n      \"connection\": \"close\",\n      \"content-length\": \"15\",\n      \"content-type\": \"application/json\",\n      \"host\": \"172.17.0.1\",\n      \"user-agent\": \"curl/7.43.0\"\n    },\n    \"__ow_path\": \"\"\n  }\n}\n```\n\nYou see above that for convenience, query parameters, form data, and JSON object body entities are all treated as dictionaries, and their values are directly accessible as action input properties. This is not the case for web actions which opt to instead handle HTTP request entities more directly, or when the web action receives an entity that is not a JSON object.\n\nHere is an example of using a \"text\" content-type with the same example shown above.\n\n```bash\n$ curl https://${APIHOST}/api/v1/web/guest/demo/hello.json -H 'Content-Type: text/plain' -d \"Jane\"\n{\n  \"response\": {\n    \"__ow_method\": \"post\",\n    \"__ow_headers\": {\n      \"accept\": \"*/*\",\n      \"connection\": \"close\",\n      \"content-length\": \"4\",\n      \"content-type\": \"text/plain\",\n      \"host\": \"172.17.0.1\",\n      \"user-agent\": \"curl/7.43.0\"\n    },\n    \"__ow_path\": \"\",\n    \"__ow_body\": \"Jane\"\n  }\n}\n```\n\n\n## Content extensions\n\nA content extension is generally required when invoking a web action; the absence of an extension assumes `.http` as the default. The fully qualified name of the action must include its package name, which is `default` if the action is not in a named package.\n\n\n## Protected parameters\n\nAction parameters are protected and treated as immutable. Parameters are automatically finalized when enabling web actions.\n\n```bash\n$ wsk action create /guest/demo/hello hello.js \\\n      --parameter name Jane \\\n      --web true\n```\n\nThe result of these changes is that the `name` is bound to `Jane` and may not be overridden by query or body parameters because of the final annotation. This secures the action against query or body parameters that try to change this value whether by accident or intentionally.\n\n## Securing web actions\n\nBy default, a web action can be invoked by anyone having the web action's invocation URL. Use the `require-whisk-auth` [web action annotation](annotations.md#annotations-specific-to-web-actions) to secure the web action. When the `require-whisk-auth` annotation is set to `true`, the action will authenticate the invocation request's Basic Authorization credentials to confirm they represent a valid OpenWhisk identity.  When set to a number or a case-sensitive string, the action's invocation request must include a `X-Require-Whisk-Auth` header having this same value. Secured web actions will return a `Not Authorized` when credential validation fails.\n\nAlternatively, use the `--web-secure` flag to automatically set the `require-whisk-auth` annotation.  When set to `true` a random number is generated as the `require-whisk-auth` annotation value. When set to `false` the `require-whisk-auth` annotation is removed.  When set to any other value, that value is used as the `require-whisk-auth` annotation value.\n\n```bash\n$ wsk action update /guest/demo/hello hello.js --web true --web-secure my-secret\n```\nor\n```bash\n$ wsk action update /guest/demo/hello hello.js --web true -a require-whisk-auth my-secret\n```\n\n```bash\n$ curl https://${APIHOST}/api/v1/web/guest/demo/hello.json?name=Jane -X GET -H \"X-Require-Whisk-Auth: my-secret\"\n```\n\nIt's important to note that the owner of the web action owns all of the web action's activations records and will incur the cost of running the action in the system regardless of how the action was invoked.\n\n## Disabling web actions\n\nTo disable a web action from being invoked via web API (`https://APIHOST/api/v1/web/`), pass a value of `false` or `no` to the `--web` flag while updating an action with the CLI.\n\n```bash\n$ wsk action update /guest/demo/hello hello.js --web false\n```\n\n## Raw HTTP handling\n\nA web action may elect to interpret and process an incoming HTTP body directly, without the promotion of a JSON object to first class properties available to the action input (e.g., `args.name` vs parsing `args.__ow_query`). This is done via a `raw-http` [annotation](annotations.md). Using the same example show earlier, but now as a \"raw\" HTTP web action receiving `name` both as a query parameters and as JSON value in the HTTP request body:\n```bash\n$ curl https://${APIHOST}/api/v1/web/guest/demo/hello.json?name=Jane -X POST -H \"Content-Type: application/json\" -d '{\"name\":\"Jane\"}'\n{\n  \"response\": {\n    \"__ow_method\": \"post\",\n    \"__ow_query\": \"name=Jane\",\n    \"__ow_body\": \"eyJuYW1lIjoiSmFuZSJ9\",\n    \"__ow_headers\": {\n      \"accept\": \"*/*\",\n      \"connection\": \"close\",\n      \"content-length\": \"15\",\n      \"content-type\": \"application/json\",\n      \"host\": \"172.17.0.1\",\n      \"user-agent\": \"curl/7.43.0\"\n    },\n    \"__ow_path\": \"\"\n  }\n}\n```\n\nOpenWhisk uses the [Pekko Http](https://pekko.apache.org/docs/pekko-http/current) framework to [determine](https://pekko.apache.org/api/pekko-http/snapshot/org/apache/pekko/http/scaladsl/model/MediaTypes$.html) which content types are binary and which are plain text.\n\n\n### Enabling raw HTTP handling\n\nRaw HTTP web actions are enabled via the `--web` flag using a value of `raw`.\n\n```bash\n$ wsk action create /guest/demo/hello hello.js --web raw\n```\n\n### Disabling raw HTTP handling\n\nDisabling raw HTTP can be accomplished by passing a value of `false` or `no` to the `--web` flag.\n\n```bash\n$ wsk update create /guest/demo/hello hello.js --web false\n```\n\n### Decoding binary body content from Base64\n\nWhen using raw HTTP handling, the `__ow_body` content will be encoded in Base64 when the request content-type is binary.\nBelow are functions demonstrating how to decode the body content in Node, Python, Swift and PHP. Simply save a method shown\nbelow to file, create a raw HTTP web action utilizing the saved artifact, and invoke the web action.\n\n#### Node\n\n```javascript\nfunction main(args) {\n    decoded = new Buffer(args.__ow_body, 'base64').toString('utf-8')\n    return {body: decoded}\n}\n```\n\n#### Python\n\n```python\ndef main(args):\n    try:\n        decoded = args['__ow_body'].decode('base64').strip()\n        return {\"body\": decoded}\n    except:\n        return {\"body\": \"Could not decode body from Base64.\"}\n```\n\n#### Swift\n\n```swift\nextension String {\n    func base64Decode() -> String? {\n        guard let data = Data(base64Encoded: self) else {\n            return nil\n        }\n\n        return String(data: data, encoding: .utf8)\n    }\n}\n\nfunc main(args: [String:Any]) -> [String:Any] {\n    if let body = args[\"__ow_body\"] as? String {\n        if let decoded = body.base64Decode() {\n            return [ \"body\" : decoded ]\n        }\n    }\n\n    return [\"body\": \"Could not decode body from Base64.\"]\n}\n```\n\n#### PHP\n\n```php\n<?php\n\nfunction main(array $args) : array\n{\n    $decoded = base64_decode($args['__ow_body']);\n    return [\"body\" => $decoded];\n}\n```\n\nAs an example, save the Node function as `decode.js` and execute the following commands:\n```bash\n$ wsk action create decode decode.js --web raw\nok: created action decode\n$ curl -k -H \"content-type: application\" -X POST -d \"Decoded body\" https://${APIHOST}/api/v1/web/guest/default/decodeNode.json\n{\n  \"body\": \"Decoded body\"\n}\n```\n## Options Requests\n\nBy default, an OPTIONS request made to a web action will result in CORS headers being automatically added to the\nresponse headers. These headers allow all origins and the options, get, delete, post, put, head, and patch HTTP verbs.\nIn addition, the header `Access-Control-Request-Headers` is echoed back as the header `Access-Control-Allow-Headers`\nif it is present in the HTTP request. Otherwise, a default value is generated as shown below.\n\n```\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH\nAccess-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, User-Agent\n```\n\nAlternatively, OPTIONS requests can be handled manually by a web action. To enable this option add a\n`web-custom-options` annotation with a value of `true` to a web action. When this feature is enabled, CORS headers will\nnot automatically be added to the request response. Instead, it is the developer's responsibility to append their\ndesired headers programmatically. Below is an example of creating custom responses to OPTIONS requests.\n\n```\nfunction main(params) {\n  if (params.__ow_method == \"options\") {\n    return {\n      headers: {\n        'Access-Control-Allow-Methods': 'OPTIONS, GET',\n        'Access-Control-Allow-Origin': 'example.com'\n      },\n      statusCode: 200\n    }\n  }\n}\n```\n\nSave the above function to `custom-options.js` and execute the following commands:\n\n```\n$ wsk action create custom-option custom-options.js --web true -a web-custom-options true\n$ curl https://${APIHOST}/api/v1/web/guest/default/custom-options.http -kvX OPTIONS\n< HTTP/1.1 200 OK\n< Server: nginx/1.11.13\n< Content-Length: 0\n< Connection: keep-alive\n< Access-Control-Allow-Methods: OPTIONS, GET\n< Access-Control-Allow-Origin: example.com\n```\n\n## Web Actions in Shared Packages\n\nA web action in a shared (i.e., public) package is accessible as a web action either directly via the package's fully\nqualified name, or via a package binding. It is important to note that a web action in a public package will be\naccessible for all bindings of the package even if the binding is private. This is because the web action annotation\nis carried on the action and cannot be overridden. If you do not wish to expose a web action through your package\nbindings, then you should clone-and-own the package instead.\n\nAction parameters are inherited from its package, and the binding if there is one. You can make package parameters\n[immutable](./annotations.md#protected-parameters) by defining their values through a package binding.\n\n## Error Handling\n\nWhen an OpenWhisk action fails, there are two different failure modes. The first is known as an _application error_ and is analogous to a caught exception: the action returns a JSON object containing a top level `error` property. The second is a _developer error_ which occurs when the action fails catastrophically and does not produce a response (this is similar to an uncaught exception). For web actions, the controller handles application errors as follows:\n\n1. The controller projects an `error` property from the response object.\n1. The controller applies the content handling implied by the action extension to the value of the `error` property.\n\nDevelopers should be aware of how web actions might be used and generate error responses accordingly. For example, a web action that is used with the `.http` extension\nshould return an HTTP response, for example: `{error: { statusCode: 400 }`. Failing to do so will in a mismatch between the implied content-type from the extension and the action content-type in the error response. Special consideration must be given to web actions that are sequences, so that components that make up a sequence can generate adequate errors when necessary.\n\n## Vanity URL\n\nWeb actions may be accessed via an alternate URL which treats the OpenWhisk namespace as a subdomain to the API host. This is suitable for developing web actions that use cookies or local storage so that data is not inadvertently made visible to other web actions in other namespaces. The namespaces must match the regular expression `[a-zA-Z0-9-]+` (and should be 63 characters or fewer) for the edge router to rewrite the subdomain to the corresponding URI. For a conforming namespace, the URL `https://guest.openwhisk-host/public/index.html` becomes a alias for `https://openwhisk-host/api/v1/web/guest/public/index.html`.\n\nFor added convenience, and to provide the equivalent of an `index.html`, the edge router will also proxy `https://guest.openwhisk-host` to `https://openwhisk-host/api/v1/web/guest/public/index.html` where `/guest/public/index.html` (i.e., action is called `index` in a package called `public`) is a web action that responds with HTML content. If the action does not exist, the API host will respond with 404 Not Found.\n\nFor a local deployment, you will need to provide name resolution for the vanity URL to work. The easiest solution is to add an entry in `/etc/host` for `<namespace>.openwhisk-host`, as in:\n```bash\n127.0.0.1  guest.openwhisk-host\n```\nor using a name resolver in combination with `curl` for example, as in:\n```bash\n$ curl -k https://guest.openwhisk-host --resolve guest.openwhisk-host:443:127.0.0.1\n```\n\nYou also need to generate an edge router configuration (and SSL certificate) that uses the proper hostname. This may be done by modifying a proper host name (see [global environment variables](../ansible/group_vars/all#L18)) and running the [`setup.yml`](../ansible/setup.yml) and [`edge.yml`](../ansible/edge.yml) playbooks.\n"
  },
  {
    "path": "docs/yarn.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# YARN Support\n\nThe `YARNContainerFactory` enables launching action containers within a YARN cluster. It does not affect the deployment of OpenWhisk components (invoker, controller).\n\n## Enable\n\nTo enable YARNContainerFactory, use the following TypeSafe Config properties\n\n| property | required | details | example |\n| --- | --- | --- | --- |\n| `whisk.spi.ContainerFactoryProvider` | required | enable the YARNContainerFactory | org.apache.openwhisk.core.yarn.YARNContainerFactoryProvider |\n| `whisk.yarn.masterUrl` | required | YARN Resource Manager endpoint to be accessed from the invoker |  http://localhost:8088 |\n| `whisk.yarn.yarnLinkLogMessage` | optional (default true) | Display a log message with a link to YARN when using the default LogStore (or no log message) |  true |\n| `whisk.yarn.serviceName` | optional (default openwhisk) | Name of the YARN Service created by the invoker. The invoker number will be appended. |  openwhisk-action-service |\n| `whisk.yarn.authType` | optional (default simple) | Authentication type for YARN |  simple or kerberos |\n| `whisk.yarn.kerberosPrincipal` | optional (default \"\") | Kerberos principal to use for the YARN service. Note: must include a hostname |  user1/hostA@REALM |\n| `whisk.yarn.kerberosKeytabURI` | optional (default \"\") | Location of keytab accessible by all node managers |  hdfs:/user/user1/user1_hostA.keytab |\n| `whisk.yarn.queue` | optional (default default) | Name of the YARN queue where the service will be created |  default |\n| `whisk.yarn.memory` | optional (default 256) | Memory used by each YARN container |  256 |\n| `whisk.yarn.cpus` | optional (default 1) | CPUs used by each YARN container |  1 |\n\nTo set these properties for your invoker, set the corresponding environment variables e.g.,\n```properties\nCONFIG_whisk_spi_ContainerFactoryProvider=org.apache.openwhisk.core.yarn.YARNContainerFactoryProvider\nCONFIG_whisk_yarn_masterUrl=http://localhost:8088\nCONFIG_whisk_yarn_yarnLinkLogMessage=true\nCONFIG_whisk_yarn_serviceName=openwhisk-action-service\nCONFIG_whisk_yarn_authType=simple\n\nCONFIG_whisk_yarn_queue=default\nCONFIG_whisk_yarn_memory=256\nCONFIG_whisk_yarn_cpus=1\n```\n\n## HA\nHA is supported. Each invoker will create its own YARN service with its invoker number appended to the configured service name (e.g. openwhisk-action-service-0).\n\n## Security\nBy default, OpenWhisk does not authenticate when communicating with YARN. Optionally, Kerberos/SPNEGO authentication can be used via JaaS with a few steps:\n* Set whisk.yarn.authType to \"kerberos\"\n* Set the kerberosPrincipal and kerberosKeytabURI properties. These are used by the YARN service.\n* Mount krb5.conf, login.conf, and keytab files into the invoker's docker container. For example:\n    * -v \"/etc/krb5.conf:/etc/krb5.conf\"\n    * -v \"/home/user1/login.conf:/login.conf\"\n    * -v \"/home/user1/user1.keytab:/user1.keytab\"\n* Run the invoker with the following java settings (e.g. via the INVOKER_OPTS environment variable):\n    * -Djava.security.auth.login.config={Path to login.conf file}\n    * -Djava.security.krb5.conf={Path to krb5.conf file}\n\nExample login.conf:\n```\ncom.sun.security.jgss.initiate {\n     com.sun.security.auth.module.Krb5LoginModule required\n     useKeyTab=true\n     storeKey=true\n     doNotPrompt=true\n     keyTab=\"~/user1_hostA.keytab\"\n     principal=\"user1/hostA@REALM\";\n };\n```\n\n## Known Issues\n\n* Logs are not collected from action containers.\n\n  For now, the YARN public URL will be included in the logs retrieved via the wsk CLI. Once log retrieval from external sources is enabled, logs from yarn containers would have to be routed to the external source, and then retrieved from that source.\n"
  },
  {
    "path": "gradle/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Gradle\n\nGradle is used to build OpenWhisk. It does not need to be pre-installed as it installs itself using the [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html). To use it without installing, simply invoke the `gradlew` command at the root of the repository. You can also install `gradle` via [`apt`](http://linuxg.net/how-to-install-gradle-2-1-on-ubuntu-14-10-ubuntu-14-04-ubuntu-12-04-and-derivatives/) on Ubuntu or [`brew`](http://www.brewformulas.org/Gradle) on Mac. In the following we use `gradle` and `gradlew` as synonymous.\n\n## Usage\n\nIn general, project level properties are set via `-P{propertyName}={propertyValue}`. A task is called via `gradle {taskName}` and a subproject task is called via `gradle :path:to:subproject:{taskName}`. To run tasks in parallel, use the `--parallel` flag (**Note:** It's an incubating feature and might break stuff).\n\n### Build\n\nTo build all Docker images use `gradle distDocker` at the top level project, to build a specific component use `gradle :core:controller:distDocker`.\n\nProject level options that can be used on `distDocker`:\n\n- `dockerImageName` (*required*): The name of the image to build (e.g. whisk/controller)\n- `dockerHost` (*optional*): The docker host to run commands on, default behaviour is docker's own `DOCKER_HOST` environment variable\n- `dockerRegistry` (*optional*): The registry to push to\n- `dockerImageTag` (*optional*, default 'latest'): The tag for the image\n- `dockerTimeout` (*optional*, default 240): Timeout for docker operations in seconds\n- `dockerRetries` (*optional*, default 3): How many times to retry docker operations\n- `dockerBinary` (*optional*, default `docker`): The binary to execute docker commands\n\n### Test\n\nTo run tests one uses the `test` task. OpenWhisk consolidates tests into a single `tests` project. Hence the command to run all tests is `gradle :tests:test`.\n\nIt is possible to run specific tests using [Gradle testfilters](https://docs.gradle.org/current/userguide/java_plugin.html#test_filtering). For example `gradle :tests:test --tests \"your.package.name.TestClass.evenMethodName\"`. Wildcard `*` may be used anywhere.\n\n## Build your own `build.gradle`\nIn Gradle, most of the tasks we use are default tasks provided by plugins in Gradle. The [`scala` Plugin](https://docs.gradle.org/current/userguide/scala_plugin.html) for example includes tasks, that are needed to build Scala projects. Moreover, Gradle is aware of *Applications*. The [`application` Plugin](https://docs.gradle.org/current/userguide/application_plugin.html) provides tasks that are required to distribute a self-contained application. When `application` and `scala` are used in conjunction, they hook into each other and provide the tasks needed to distribute a Scala application. `distTar` for example compiles the Scala code, creates a jar containing the compiled classes and resources and creates a Tarball including that jar and all of its dependencies (defined in the dependencies section of `build.gradle`). It also creates a start-script which correctly sets the classpath for all those dependencies and starts the app.\n\nIn OpenWhisk, we want to distribute our application via Docker images. Hence we wrote a \"plugin\" that creates the task `distDocker`. That task will build an image from the `Dockerfile` that is located next to the `build.gradle` it is called from, for example Controller's `Dockerfile` and `build.gradle` are both located at `core/controller`.\n\nIf you want to create a new `build.gradle` for your component, simply put the `Dockerfile` right next to it and include `docker.gradle` by using\n\n```\next.dockerImageName = 'openwwhisk/{IMAGENAME}'\napply from: 'path/to/docker.gradle'\n```\n\nIf your component needs to be build before you can build the image, make `distDocker` depend on any task needed to run before it, for example:\n\n```\ndistDocker.dependsOn ':common:scala:distDocker', 'distTar'\n```\n"
  },
  {
    "path": "gradle/docker.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport groovy.time.*\n\n/**\n * Utility to build docker images based in gradle projects\n *\n * This extends gradle's 'application' plugin logic with a 'distDocker' task which builds\n * a docker image from the Dockerfile of the project that applies this file. The image\n * is automatically tagged and pushed if a tag and/or a registry is given.\n *\n * Parameters that can be set on project level:\n * - dockerImageName (required): The name of the image to build (e.g. controller)\n * - dockerRegistry (optional): The registry to push to\n * - dockerImageTag (optional, default 'latest'): The tag for the image\n * - dockerImagePrefix (optional, default 'whisk'): The prefix for the image,\n *       'controller' becomes 'whisk/controller' per default\n * - dockerTimeout (optional, default 840): Timeout for docker operations in seconds\n * - dockerRetries (optional, default 3): How many times to retry docker operations\n * - dockerBinary (optional, default 'docker'): The binary to execute docker commands\n * - dockerBuildArgs (options, default ''): Project specific custom docker build arguments\n * - dockerHost (optional): The docker host to run commands on, default behaviour is\n *       docker's own DOCKER_HOST environment variable\n */\n\next {\n    dockerRegistry = project.hasProperty('dockerRegistry') ? dockerRegistry + '/' : ''\n    dockerImageTag = project.hasProperty('dockerImageTag') ? dockerImageTag : 'latest'\n    dockerImagePrefix = project.hasProperty('dockerImagePrefix') ? dockerImagePrefix : 'whisk'\n    dockerTimeout = project.hasProperty('dockerTimeout') ? dockerTimeout.toInteger() : 840\n    dockerRetries = project.hasProperty('dockerRetries') ? dockerRetries.toInteger() : 3\n    dockerBinary = project.hasProperty('dockerBinary') ? [dockerBinary] : ['docker']\n    dockerBuildArg = ['build']\n    dockerDockerfileSuffix = project.hasProperty('dockerDockerfileSuffix') ? dockerDockerfileSuffix : \"\"\n    dockerMultiArchBuild = project.hasProperty('dockerMultiArchBuild') ? dockerMultiArchBuild.toBoolean() : false\n}\next.dockerTaggedImageName = dockerRegistry + dockerImagePrefix + '/' + dockerImageName + ':' + dockerImageTag\next.scalaBaseImageName = dockerRegistry + dockerImagePrefix + '/scala:' + dockerImageTag\n\nif(project.hasProperty('dockerHost')) {\n    dockerBinary += ['--host', project.dockerHost]\n}\n\nif(project.hasProperty('dockerBuildArgs')) {\n    dockerBuildArgs.each { arg  ->\n        dockerBuildArg += ['--build-arg', arg]\n    }\n    if( dockerMultiArchBuild ){\n        dockerBuildArg += ['--build-arg','BASE='+scalaBaseImageName]\n    }\n}\n\nif( !project.hasProperty('dockerBuildArgs') && dockerMultiArchBuild ) {\n    dockerBuildArg += ['--build-arg','BASE='+scalaBaseImageName]\n}\n\ndef builDockerCommand(dockerFile) {\n    def cmd = dockerBinary\n\n    if(dockerMultiArchBuild) {\n        cmd += ['buildx']\n    }\n\n    cmd +=  dockerBuildArg + ['-f', dockerFile] + ['-t', dockerImageName, project.buildscript.sourceFile.getParentFile().getAbsolutePath()]\n\n    if(dockerMultiArchBuild) {\n        cmd += ['--load']\n    }\n\n    return cmd\n}\n\ntask distDocker {\n    doLast {\n        def start = new Date()\n        String dockerFileDir = project.buildscript.sourceFile.getParentFile().getAbsolutePath()\n        String dockerFile = dockerFileDir + \"/Dockerfile\" + dockerDockerfileSuffix\n        if (!new File(dockerFile).exists()){\n            println(\"Using default Dockerfile since '${dockerFile}' does not exist\")\n            dockerFile = dockerFileDir + \"/Dockerfile\"\n        }\n\n        def cmd = builDockerCommand(dockerFile)\n        retry(cmd, dockerRetries, dockerTimeout)\n\n        println(\"Building '${dockerImageName}' took ${TimeCategory.minus(new Date(), start)}\")\n    }\n}\n\ntask distDockerCoverage() {\n    doLast {\n        def start = new Date()\n        //Copy the scoverage runtime jars\n        copy {from configurations.scoverage - configurations.implementationResolvable; into \"build/tmp/docker-coverage/ext-lib\"}\n        //Copy the scoverage prepared jars\n        coverageDirs.each {dir ->\n            copy {from file(dir); into \"build/tmp/docker-coverage/classes\"}\n        }\n\n        def buildArgs = [\n                \"OW_ROOT_DIR=${project.rootProject.projectDir.absolutePath}\"\n        ]\n        def dockerImageNameOrig = dockerImageName\n        dockerImageName = \"$dockerImageName-cov\"\n\n        //Use absolute paths for dockerFile and build directory\n        String dockerFileDir = project.buildscript.sourceFile.getParentFile().getAbsolutePath()\n        String dockerFile = \"$dockerFileDir/Dockerfile.cov\"\n\n        def cmd = dockerBinary + prepareBuildArgs(buildArgs) + ['-f', dockerFile, '-t', dockerImageName, dockerFileDir]\n        retry(cmd, dockerRetries, dockerTimeout)\n        println(\"Building '${dockerImageName}' took ${TimeCategory.minus(new Date(), start)}\")\n\n        //Replace the original image with coverage one\n        project.ext.dockerTaggedImageName = dockerImagePrefix + '/' + dockerImageNameOrig + ':' + \"cov\"\n    }\n    finalizedBy('tagImage')\n}\n\ndef prepareBuildArgs(List buildArgs) {\n    def result = ['build']\n    if(project.hasProperty('dockerBuildArgs')) {\n        buildArgs.addAll(dockerBuildArgs)\n    }\n    buildArgs.each {arg ->\n        result += ['--build-arg', arg]\n    }\n    result\n}\n\ntask tagImage {\n    doLast {\n        def versionString = (dockerBinary + ['-v']).execute().text\n        def matched = (versionString =~ /^(\\S+) version (\\d+)\\.(\\d+)\\.(\\d+)/)\n\n        def runner = matched[0][1]\n        def major = matched[0][2] as int\n        def minor = matched[0][3] as int\n\n        def dockerCmd = ['tag']\n        if(runner == 'Docker' && major == 1 && minor < 12) {\n            dockerCmd += ['-f']\n        }\n        retry(dockerBinary + dockerCmd + [dockerImageName, dockerTaggedImageName], dockerRetries, dockerTimeout)\n    }\n}\n\ntask pushImage {\n    doLast {\n        def cmd = dockerBinary + ['push', dockerTaggedImageName]\n        retry(cmd, dockerRetries, dockerTimeout)\n    }\n}\n\npushImage.dependsOn tagImage\npushImage.onlyIf { dockerRegistry != '' }\ndistDocker.finalizedBy pushImage\n\ndef retry(cmd, retries, timeout) {\n    println(\"${new Date()}: Executing '${cmd.join(\" \")}'\")\n    def proc = cmd.execute()\n    proc.consumeProcessOutput(System.out, System.err)\n    proc.waitForOrKill(timeout * 1000)\n    if(proc.exitValue() != 0) {\n        def message = \"${new Date()}: Command '${cmd.join(\" \")}' failed with exitCode ${proc.exitValue()}\"\n        if(proc.exitValue() == 143) { // 143 means the process was killed (SIGTERM signal)\n            message = \"${new Date()}: Command '${cmd.join(\" \")}' was killed after ${timeout} seconds\"\n        }\n\n        if(retries > 1) {\n            println(\"${message}, ${retries-1} retries left, retrying...\")\n            retry(cmd, retries-1, timeout)\n        }\n        else {\n            println(\"${message}, no more retries left, aborting...\")\n            throw new GradleException(message)\n        }\n    }\n}\n\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.6.2-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "licenses/LICENSE-spray.txt",
    "content": "This software is licensed under the Apache 2 license, quoted below.\n\nCopyright © 2011-2015 the spray project <http://spray.io>\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\n    [http://www.apache.org/licenses/LICENSE-2.0]\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n"
  },
  {
    "path": "proposals/POEM-1-proposal-for-openwhisk-enhancements.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Title\nProcess for introducing an OpenWhisk Enhancement (POEM)\n\n## Status\n* Current state: Completed\n* Author: @style95\n\n## Summary\n\nIntroduce and document a process for suggesting and implementing a substantive OpenWhisk Enhancement (POEM).\nA developer or group of developers working together to propose and implement a major new feature or functionality, a new subsystem, or a breaking change should follow the process described herein and open a proposal for consideration under this directory.\n\n## Motivation\n\nAs the project grows, more and more issues are getting complex and require multiple parties and an extended period of time to develop them.\nWe can incubate, manage, and elaborate new ideas in OpenWhisk with a standard way and a well-tracked artifact.\nThe goals are to enhance the discoverability of proposals, and to help community members who want to get involved in the project.\n\n## Proposed changes\n\n### Procedures\n1. Create a pull request to describe your proposal with [this template](./POEM-N-template.md). The initial state of a proposal should be _\"Draft\"_.\n2. [Create a corresponding issue](https://github.com/apache/openwhisk/issues/new?template=proposal.md) to propose a new change based on [this template](../.github/ISSUE_TEMPLATE/proposal.md). It is mainly used to track discussion history.\n3. Discuss the proposals using to form a consensus and update your proposal based on comments as needed. It is important to be inclusive, and to notify the OpenWhisk community of meaningful changes using the Apache [`dev` list](https://openwhisk.apache.org/community.html) for this project. Other forms of communication such as Slack are OK but any meaningful results should be documented in issues and the `dev` list.\n4. When members form a rough consensus for the proposal. The proposal owner can request a vote via the dev mailing list.\n5. The voting process follows the [Apache Voting guideline](https://www.apache.org/foundation/voting.html). The PR can be merged with the _\"In-progress\"_ state if the voting is successfully closed without any veto.\n6. The implementation begins as the proposal is filed into the repo and any volunteer can join the implementation ideally.\n7. If the proposal is not accepted or no consensus is formed, the PR is merged with the state, _\"Abandoned\"_.\n8. The proposal state is changed to \"Completed\"_ and any corresponding issues are closed once the implementation is compete, and the code is merged into the master branch.\n\n### Note\n* Committers and the PMC are supposed to label issues with an appropriate label to track issue and pull request status.\n* There are 4 labels(`draft`, `in-progress`, `completed`, and `abandoned`) to specify the state of a proposal and one special label (`proposal`) to differentiate proposals from other issues.\n\n### Proposal Lifecycle\nA proposal may be in one of the following states:\n* **Draft**: A new enhancement is proposed and it is under discussion.\n* **In-progress**: A consensus for the proposal is formed and implementation is in progress.\n* **Completed**: Implementation is finished and the change is included in the master branch.\n* **Abandoned**: A proposal is not accepted for some reason such as ”no consensus is formed”.\n\n## Issue\n\nAbandoned proposals are filed in this directory for archival.\nThis is to keep and track all proposals at any stage in one place.\nA new idea can be derived from the abandoned one.\n"
  },
  {
    "path": "proposals/POEM-2-function-pulling-container-scheduler.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Title\nFunction Pulling Container Scheduler (FPCScheduler)\n\n## Status\n* Current state: In-progress\n* Author(s): @style95, @ningyougang, @keonhee, @jiangpengcheng, @upgle\n\n## Summary and Motivation\n\nThis POEM proposes a new scheduler for OpenWhisk, FPCScheduler.\nPreviously we revealed [performance issues](https://cwiki.apache.org/confluence/display/OPENWHISK/Autonomous+Container+Scheduling+v1) in OpenWhisk.\nThere are many reasons for them, but we can summarize them into the following problems.\n\n1. Multiple actions share the same `homeInvoker`.\n2. Current scheduler does not consider the time taken for container operations (create/delete, pause/unpause).\n3. Resources are evenly divided by the number of controllers.\n\nFirst, multiple actions share the same `homeInvoker`. This is because the `homeInvoker` is statically decided by a hash function.\nWhile scheduling, resource status in the invoker side is not considered and this introduces busy hotspot invokers while the others are idle.\nThis is generally a good idea to increase the probability of container reuse, but it leads to performance degradation because of slow container operation.\nWhen all resources in an invoker are taken by one action (busy/warm), an activation for another action will remove one of the existing containers and create a new one for it.\nIf actions run for a long time this would be a good approach in terms of bin-packing. But when actions run for short time (most of the serverless cases),\nthe heuristic severely increases the response time because container operations take one to two orders of magnitude longer time than invocation and this results in poor performance.\n\nSecond, since controllers schedule activations to invokers in a distributed environment, the invokers in the system are partitioned and assigned to specific controllers. This means that any one controller is assigned a fraction of the available resources in the system.\nIt's not feasible to collect the status of all invokers so that scheduling decisions have a global view.\nAs a result, controllers may throttle activations even if cluster-wide invocations are below limits (and when there are available resources and system capacity).\n\nWe propose FPCScheduler to address the above issues.\nThe scheduler differs from the existing scheduler(`ShardingPoolBalancer`) in the following ways:\n- It schedules containers rather than function requests. Each container will pull activations requests from the given action queue continuously.\n- Whenever one execution is over, it fetches the next activation requests and invokes it repeatedly. In this way, we can maximize container reuse.\n- An added benefit is that schedulers don't need to track or consider the location of existing containers. Schedulers decide where and when to create more containers.\n- The scheduler can create more containers on invokers with enough resources. This enables distributed scheduling with all invoker resources among multiple schedulers.\n\nControllers no longer schedule activation requests. Instead, controllers create action queues and forward activation requests to these queues.\nEach action queue is a dedicated queue for the given action and dynamically created/deleted by the FPCSchedulers. Each action has its own queue, so there is no interference among actions because of a shared queue.\n\n[ETCD](https://github.com/etcd-io/etcd), a distributed and reliable key-value store is used for a transaction, health-check, cluster information sharing, etc.\nEach scheduler performs a transaction via ETCD when scheduling. The health status of each component(scheduler, invoker) is managed by a [lease](https://help.compose.com/docs/etcd-using-etcd3-features#leases) in ETCD.\nWhenever a component is failed, it no longer sends keepalive requests, and its health data is removed via a lease timing out.\nCluster-wide information such as scheduler endpoints, queue endpoints, containers in a namespace, and throttling is stored in ETCD and referenced by corresponding components.\nControllers throttle namespaces based on the throttling data in ETCD. So all controllers share the same view against the resources and manage them in the same way.\n\nOne more benefit of having our own component for routing rather than utilizing open-source components such as Kafka is we can extend and implement any routing logic.\nWe would want various routing policies at some point. For example, when we utilize multiple versions of an action in-flight in the future we might want to control the traffic ratio between the two versions,\nwe can route activations only to invokers with a specific resource, we may want to have dedicated invokers for some namespaces and so on. And this POEM could be a baseline for such extensions.\n\n## Proposed changes: Architecture Diagram (optional) and Design\nThe design document along architecture diagram is already shared on the [OpenWhisk Wiki](https://cwiki.apache.org/confluence/display/OPENWHISK/Apache+OpenWhisk+Project+Wiki?src=sidebar)\n\n* [Design Consideration](https://cwiki.apache.org/confluence/display/OPENWHISK/Design+consideration?src=contextnavpagetreemode)\n* [Architecture](https://cwiki.apache.org/confluence/display/OPENWHISK/System+Architecture)\n* [Component Design](https://cwiki.apache.org/confluence/display/OPENWHISK/Component+Design)\n\n### Implementation details\n\nFor the record, we (NAVER) are already operating OpenWhisk with this scheduler in a production environment.\nWe want to contribute the scheduler and hope it evolves with the community.\n\nThere are many [new components](https://cwiki.apache.org/confluence/display/OPENWHISK/Component+Design).\n\n#### Common components\nWe store data in ETCD, there are many relevant components such as `EtcdClient`, `LeaseKeepAliveService`, `WatcherService`, `DataManagementService`, etc.\n\n* `EtcdClient`: An ETCD client offering basic CRUD operations.\n* `DataManagementService`: In charge of storing/recovering data in ETCD. It internally utilizes `LeaseKeepAliveService`, and `WatcherService`.\n* `LeaseKeepAliveService`: In charge of keeping the given lease alive.\n* `WatcherService`: Watches the given keys and receives events for the keys.\n\n#### Scheduler components\n\nIn an abstract view, schedulers provide queueing, container scheduling, and activation routing.\n\n* `QueueManager`: The main entry point for queue creation request. It has references to all queues.\n* `MemoryQueue`: Dynamically created/deleted for each action. It watches the incoming/outgoing requests and triggers container creation.\n* `ContainerManager`: Schedule container creation requests to appropriate invokers.\n* `ActivationServiceImpl`: Provide API for containers to fetch activations via Pekko-grpc. It works in a long-poll way to avoid busy-waiting.\n\n#### Controller components\n* `FPCPoolBalancer`: Create queues if not exist, and forward messages to them.\n* `FPCEntitlementProvider`: Throttle activations based on throttling information in ETCD.\n\n#### Invoker components\n\n* `FunctionPullingContainerPool`: A container pool for function pulling container. It handles the container creation requests.\n* `FunctionPullingContainerProxy`: A proxy for a container. It repeatedly fetches activations and invokes them.\n* `ActivationClientProxy`: The Pekko-grpc client. It communicates with `ActivationServiceImpl` in schedulers.\n* `InvokerHealthManager`: Manages the health and resources information of invokers. The data is stored in ETCD. If an invoker becomes unhealthy, it invokes health activations.\n\n## Future work\n\n#### Persistence\n\nWe reached a conclusion that the persistence of activation requests is not that mandatory requirement along with the at-most-once nature and circuit-breaking of OpenWhisk.\nBut if it is desired, we can implement a persistent queue rather than an in-memory queue.\n\n#### Multiple partitions\n\nCurrently, a queue only has one partition. Since there can be multiple queues for each action, cluster-wide RPS(request per second) increases linearly.\nIf high RPS for one action is required, we can implement partitioning in queues to introduce parallel processing like what Kafka does.\nBut we already confirmed 100K RPS with 10 queues and 10 commodity invokers. And the performance would linearly increase with more invokers and queues.\n\n## Integration and Migration plan\n\nSince they are all new components and can coexist with existing components using SPI(Service Provider Interface), there would be no breaking change.\nWe would incrementally merge PRs into the master branch.\n"
  },
  {
    "path": "proposals/POEM-3-action-limit-for-namespace.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Title\nProviding action limits for each namespace\n\n## Status\n* Current state: In-progress\n* Author(s): @upgle\n\n## Summary and Motivation\n\nThis POEM proposes a new feature that allows administrators to set action limits (memory, timeout, log, and concurrency) for each namespace.\n\nSometimes some users want to make an action with more memory and longer duration. But, OpenWhisk only has a system limit for action shared by all namespaces.\nThere is no way to adjust the action limit for a few users, and changing the action limit setting will affect all users.\n\nIn some private environments, you can operate OpenWhisk more flexibly by providing different action limits.\n(For example, providing high memory only to some users.)\n\n```\n          256M                               512M\n\n            │     namespace default limit      │\n            ▼                                  ▼\n ┌──────────┬──────────────────────────────────┬────────────┬────────────────────────┐\n │          │┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼┼│----------► │                        │\n └──────────┴──────────────────────────────────┴────────────┴────────────────────────┘\n ▲                                                          ▲                        ▲\n │ system limit                                             │ namespace limit        │ system limit\n\n128M                                                      1024M                    2048M\n```\n\n## Proposed changes\n\n### 3 types of action limits\n\nThere was only a system limit shared by all namespaces, but two more concepts for namespace limits are added.\n\n- (1) system limit: It can never be exceeded under any circumstances.\n- (2) namespace default limit: It can be used if a limit has not been set for a namespace.\n- (3) namespace limit: It can be set by a system administrator for a namespace and cannot exceed the range of the system limit.\n\n### Limit configs for namespace\n\n- The maxParameterSize, maxPayloadSize and truncationSize values are treated as `ByteSize` string. (e.g. 1 MB, 512 KB...)\n\nThe following settings are new:\n\nconfig key             | Type                         | description\n---------------------- | ---------------------------- | ---------------\nminActionMemory        | integer (unit: MB)           | minimum action memory size for namespace\nmaxActionMemory        | integer (unit: MB)           | maximum action memory size for namespace\nminActionLogs          | integer (unit: MB)           | minimum activation log size for namespace\nmaxActionLogs          | integer (unit: MB)           | maximum activation log size for namespace\nminActionTimeout       | integer (unit: milliseconds) | minimum action time limit for namespace\nmaxActionTimeout       | integer (unit: milliseconds) | maximum action time limit for namespace\nminActionConcurrency   | integer                      | minimum action concurrency limit for namespace\nmaxActionConcurrency   | integer                      | maximum action concurrency limit for namespace\nmaxParameterSize       | string  (format: ByteSize)   | maximum parameter size for namespace\nmaxPayloadSize         | string  (format: ByteSize)   | maximum payload size for namespace\ntruncationSize         | string  (format: ByteSize)   | activation truncation size for namespace\n\n\n### Limit document for CouchDB\n\nYou can set namespace limits with `{namespace}/limits` document just like any other existing settings (e.g., invocationsPerMinute, concurrentInvocations).\n\n```json\n{\n  \"concurrentInvocations\": 100,\n  \"invocationsPerMinute\": 100,\n  \"firesPerMinute\": 100,\n  \"maxActionMemory\": 1024,\n  \"minActionMemory\": 128,\n  \"maxActionConcurrency\": 400,\n  \"minActionConcurrency\": 1,\n  \"maxActionLogs\": 128,\n  \"minActionLogs\": 0,\n  \"maxParameterSize\": \"1048576 B\"\n}\n```\n\n#### Applying namespace limit\n- Because there is no administrator API, you must modify the DB directly or use the wskadmin tool.\n- There is plan to provide the feauture to change namespace limits in wskadmin.\n\n### Namespace Limit API\n\nUser can get the applied action limits of the namespace by the namespace limit API.\nIf the namespace's action limit is not set, the default namespace limit value will be returned.\n\n> GET /api/v1/namespaces/_/limits\n\n```json\n{\n  \"concurrentInvocations\": 30,\n  \"firesPerMinute\": 60,\n  \"invocationsPerMinute\": 60,\n  \"maxActionConcurrency\": 500,\n  \"maxActionLogs\": 0,\n  \"maxActionMemory\": 512,\n  \"maxActionTimeout\": 300000,\n  \"maxParameterSize\": \"1048576 B\",\n  \"minActionConcurrency\": 1,\n  \"minActionLogs\": 0,\n  \"minActionMemory\": 128,\n  \"minActionTimeout\": 100\n}\n```\n\n### System API (URI path: /)\n\nA namespace default limit information is additionally provided separately from the previously provided system limit information.\n\n- default_max_action_duration\n- default_max_action_logs\n- default_max_action_memory\n- default_min_action_duration\n- default_min_action_logs\n- default_min_action_memory\n\n#### Preview\n\n> GET /\n\n```json\n{\n  \"api_paths\": [\n    \"/api/v1\"\n  ],\n  \"description\": \"OpenWhisk\",\n  \"limits\" : {\n    \"actions_per_minute\": 60,\n    \"concurrent_actions\": 30,\n    \"default_max_action_duration\": 300000,\n    \"default_max_action_logs\": 0,\n    \"default_max_action_memory\": 536870912,\n    \"default_min_action_duration\": 100,\n    \"default_min_action_logs\": 0,\n    \"default_min_action_memory\": 134217728,\n    \"max_action_duration\": 300000,\n    \"max_action_logs\": 0,\n    \"max_action_memory\": 536870912,\n    \"min_action_duration\": 100,\n    \"min_action_logs\": 0,\n    \"min_action_memory\": 134217728,\n    \"sequence_length\": 50,\n    \"triggers_per_minute\": 60\n  }\n}\n```\n\n### Backward compatibility\n\nFor backward compatibility, if there is no namespace default limit setting, it is replaced with a system limit.\n\nAs the namespace default limit is the same as the system limit, so the administrator cannot set the namespace limit, and the user can create actions with resources (memory, logs, timeout...) up to the system limit as before.\n\n\n### Namespace limit validation\n\nPreviously, system limits were validated when deserializing the `ActionLimits` object from the user request.\n\nHowever, at the time of deserialization of the user requests, the namespace's action limit cannot be known and the limit value cannot be included in an error message, so the validation must be performed after deserialization.\nTherefore, the code to perform this validation has been added to the controller, scheduler, and invoker.\n\n#### 1. Validate action limits when the action is created in the controller\n\nWhen an action is created in the controller, make sure that the action limits do not exceed the system limits and namespace limits.\n\nIf the namespace limits or system limits are exceeded, the namespace limit value must be returned as an error message in the response body.\n\n```\n                                                 ┌───────────────┐\n                                                 │               │\n                                                 │   AuthStore   │\n                                                 │               │\n                                                 └───────┬───────┘\n                                                         │\n                                                 ┌───────┴───────┐\n                                                 │               │\n                                                 │   Identity    │ UserLimits\n                                                 │               │ (maxActionMemory = 512M)\n  Create action     ┌───────────────────┐        └───────────────┘\n (memory = 1024M)   │                   │                ▲\n──────────────────► │                   │                │\n                    │    Controller     ├────────────────┘\n◄────────X───────── │                   │   Validate namespace limit\n   Reject request   │                   │\n   (1024M > 512M)   └───────────────────┘\n```\n\n\n#### 2. Validate action limits when the action is executed in the invoker\n\nWhen the action is executed, the invoker must checks whether the action limit exceeds the system limit and namespace limits.\nIf the limit of the action to be executed exceeds the limit, an application error with `Messages.actionLimitExceeded` message is returned and invocation is aborted.\n\n```scala\ncase _: ActionLimitsException =>\n  ActivationResponse.applicationError(Messages.actionLimitExceeded)\n```\n\n```\n                                                             ┌───────────────┐\n                                                             │               │\n                                                             │   Identity    │  UserLimits\n                                                             │               │  (maxActionMemory = 512M)\n                                                             └───────────────┘\n                                                                  ▲\n                                                                  │  Validate namespace limit\n                                                                  │\n  Invoke action     ┌───────────────────┐     Activation     ┌────┴──────────────┐\n (memory = 1024M)   │                   │       Message      │                   │\n──────────────────► │                   │ ─────────────────► │                   │\n                    │    Controller     │                    │      Invoker      │\n◄────────X───────── │                   │ ◄────────X──────── │                   │\n   Reject request   │                   │        Reject      │                   │\n                    └───────────────────┘      Invocation    └───────────────────┘\n                                             (1024M > 512M)\n```\n\n#### 3. Validate action limits when the action is executed in the invoker with the scheduler\n\nThe invoker that works with the scheduler should check namespace limits when creating containers and handling activations.\n\n- When creating a container, if the requested resource of the action exceeds the namespace limit, creation is rejected and the queue is removed.\n- when processing an activation message, if the action exceeds the namespace limit, the activation is rejected.\n\n\n```\n                                                         ┌───────────────┐\n                                                         │               │\n                                                         │   Identity    │ UserLimits\n                                                         │               │ (maxActionMemory = 512M)\n                                                         └───────────────┘\n                                                                 ▲\n                                        Invoker                  │\n                                       ┌─────────────────────────┼─┐\n┌─────────────┐   ContainerCreation    │                         │ │\n│             │        Message         │  ┌────────────────────┐ │ │\n│             │ ───────────────────────┼─►│  ContainerMessage  │ │ │\n│             │                        │  │     Consumer       ├─┤ │ Validate namespace limit\n│             │ ◄───────────X──────────┼─ └────────────────────┘ │ │\n│  Scheduler  │     Reject creating    │                         │ │\n│             │        container       │  ┌────────────────────┐ │ │\n│             │                        │  │  FunctionPulling   │ │ │\n│             │ ◄──────────────────────┼──┤  ContainerProxy    ├─┘ │\n│             │      Fetch activation  │  └──────────────┬─────┘   │\n└─────────────┘                        │                 │         │\n                                       └─────────────────┼─────────┘\n                    Kafka                                │\n                   ┌───────────────┐                     │\n                   ├───────────────┤                     │\n                   │ Completed0    │ ◄─────────X─────────┘\n                   ├───────────────┤   Activation Response\n                   └───────────────┘    (Reject 1024>512M)\n```\n\n\n\n### Handling invalid namespace limits\n\nBecause there is no admin API to handle namespace limits, the CouchDB document may have namespace limit values that exceed the system limits.\nBut, If there is a namespace limit that exceeds the system limit, the namespace limit is lowered to the system limit.\n"
  },
  {
    "path": "proposals/POEM-4-action-concurrency-limit-within-namespace.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Title\nUser Defined Action Level Concurrency Limits Within Confines of Global Namespace Limit\n\n## Status\n* Current state: In-progress\n* Author(s): @bdoyle0182 (Github ID)\n\n## Summary and Motivation\n\nCurrently, openwhisk has a single concurrency limit for managing auto scaling within a namespace. This limit for each namespace is managed\nrightly by system administrators to maintain a good balance between the namespaces of the system and the total system's resources.\n\nHowever, this does not allow for the user to control how their applications scale within the namespace that they are operating. There is no\nfairness across functions within a namespace. The semantics of a namespace can vary heavily depending on how openwhisk is being used. A namespace\ncould represent an organization for public cloud, a group within an organization, an application of functions, a logical grouping of applications\n(for example putting all of your interactions with slack in one namespace).\n\nThe problem is that a single function can runaway and end up using all of the namespace's resources. It shouldn't be on the system administrators\nto provide this fairness as it's dependent on the application and what the user wants. They may want the existing behavior to allow any action\nto scale up to the total namespace's resources, they may want to restrict one less prioritized function scale up to a smaller threshold so it can't eat\nthe entire namespace's resources but still allow other high priority functions access to the entire namespace's resources, or they may want to provide\nlimits to all of their actions that add up to their namespace limit which will guarantee each action in their namespace can have up to their defined\naction concurrency limits similar to other FaaS providers concept of reserved concurrency for actions.\n\nWith the major revision to how Openwhisk processes activations with the new scheduler, such a feature becomes extremely easy to implement by just adding\na single new limit that users can configure on their action document.\n\n## Proposed changes: Architecture Diagram (optional), and Design\n\nAdd a optional `maxContainerConcurrency` limit field to action documents in the limits section. This limit will be used in the scheduler when deciding\nif there is capacity for the action to scale up more containers. Previously, the scheduler was completely naive of functions across a namespace when provisioning\nmore containers, but if this limit is defined the scheduler will only allow to provision containers up to the defined action limit (which must be less than or equal to the namespace limit).\n\n### Implementation details\n\nA working PR of this POEM is already done in which implementation details can be reviewed but I will describe implementation considerations here. Once the POEM is approved,\nI will add any feedback from the POEM, tests, and documentation.\n\n- The scheduler decision maker uses the min of action container concurrency limit and the namespace concurrency limit. If the action limit is less than the namespace\nlimit, it will check both that the action hasn't used up all its capacity and that the namespace still has capacity if the action does still have capacity.\n- The new limit `maxContainerConcurrency` on the action document is an optional field. If the field does not exist, the action limit used by the system is\nthe namespace limit making this an optional feature.\n- The one thing not yet included in the implementation param is a parameter on the create action api which will allow the user to delete the limit field so that\nthe action will rely on the namespace limit again.\n- When creating an action, the api will validate that your action container concurrency limit is less than or equal to the namespace concurrency limit. If it is greater,\nthe upload will fail with a BadRequest and error message that the limit must be less than the namespace limit with the namespace limit value included in the message.\n- If the system admin lowers a namespace's concurrency limit below an amount that an existing action document has already configured, it will not break the action.\nSince the scheduler just decides what the limit is to use to determine capacity based on the min of the namespace and action limit, it will therefore just use\nthe namespace limit as the capacity limit. Therefore, there is no action required or side effects or coordination required from the system admin wanting to lower the namespace limit.\nHowever, if the user wants to redeploy the same function with the same limit that is now over the namespace limit; the api will now reject the action upload until the action limit\nis lowered below the new namespace limit.\n- A user may want to update their action to go back to just relying on the namespace limit. Since updates to action documents copy over limits in the update even if not\nsupplied on the request object, a boolean param will need to be added to the create action api so that the field is not copied in the update. **This is the one thing I still\nneed to add in the src code of the implementation PR.**\n- In the scheduler, if the action limit is hit and new containers cannot be provisioned for the action but there is still capacity available for the namespace, namespace throttling\nwill not be turned on. The action queue will rely on action throttling if the queue grows too large if this case is hit. Namespace throttling will still be turned on if\nthe total containers hits the namespace limit.\n\n## Integration and Migration plan (optional)\n\nThe feature is fully backwards compatible with existing action documents since the new limit is an optional field. If the limit is not defined on an action document,\nthe existing behavior is used where the action can have up to the namespace concurrency limit so there is no change to behavior if the feature is not used.\nIf using the old scheduler and the limit is defined on the action document, the limit just won't do anything until migrated to the new scheduler.\n"
  },
  {
    "path": "proposals/POEM-N-template.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nThis is a template for a filed proposal.\nCreate a pull request using this template.\n\n# Title\nThis is the title of the POEM.\nA good simple title would help to describe the purpose of a proposal.\n# Your Poem\n\nRoses are red.\nViolets are blue.\nYour ideas are beautiful.\nWe welcome you.\n## Status\n* Current state: (Draft | In-progress | Completed | Abandoned)\n* Author(s): @author (Github ID)\n\n## Summary and Motivation\n\nThis section summarize the proposal.\nA brief description, proposed changes, and effects are expected to be included.\nYou should cover the \"what\" and the \"why\" and briefly the \"how\".\n\n## Proposed changes: Architecture Diagram (optional), and Design\nThis section may include large subsections, diagrams, links to references, and so on.\n\nIt is recommended to include an architectural diagram.\nA link to an external resource is enough.\n\n### Implementation details\n\nThis section describe how to implement the proposal.\n\n## Issue (optional)\n\nAny issue(compatibility, drawbacks, etc) should be describe here.\n\n## Integration and Migration plan (optional)\n\nIf a proposal contains any breaking changes, it is required to include a plan for integration and migration.\n"
  },
  {
    "path": "proposals/POEM-support-array-result.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# Title\n\nCurrently, OpenWhisk supports returning a JSON object only, e.g.\n```shell\n# wsk action invoke hello -r\n{\n    \"greeting\": \"Hello stranger!\"\n}\n```\nIt is necessary to support returning an array too as an array is also a proper JSON object, e.g.\n```shell\n# wsk action invoke hello-array -r\n[\n    \"a\",\n    \"b\"\n]\n```\nThe sequence action should be considered support as well.\n\n# Status\n* Current state: In-progress\n* Author(s): @ningyougang\n\n# Summary and Motivation\n\nThis POEM proposes a new feature that allows user to write their own action which supports an array result.\nSo actions would be able to return a JSON object or an array.\n\n# Proposed changes\n## Openwhisk main repo\nMake controller and invoker support both a JSON object and an array result.\n\n## Runtime repos\nAll runtime images should support an array result. e.g.\n\n* nodejs (supports by default)\n* go\n* java\n* python\n* php\n* shell\n* docker\n* ruby\n* dotnet\n* rust\n* swift\n* deno\n* ballerina\n\n## Openwhisk-cli repo\n* The `wsk` CLI needs to support parsing an array result when executing actions.\n* The `wsk` CLI needs to support parsing an array result when getting activations.\n"
  },
  {
    "path": "proposals/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# How to submit a Proposal for an OpenWhisk Enhancement (POEM)\n\nThis directory contains Proposals for OpenWhisk Enhancements (POEM). Proposed changes, new features and ideas are documented using a process outlined in this document. If you're a contributor interested in out of the proposals, you are encouraged to reach out to the proposal authors. This is a welcoming community and we look forward to your contributions.\nThis directory contains approved proposals. The one who wants to include any major new feature, subsystem, piece of functionality and any breaking changes is supposed to open a proposal.\n\nYou can find the full details in [POEM-1](./POEM-1-proposal-for-openwhisk-enhancements.md).\n\n## Quickstart to propose a new change.\n1. Copy the [POEM template](./POEM-N-template.md) and open a pull request.\n2. [Create a corresponding issue](https://github.com/apache/openwhisk/issues/new?template=proposal.md) with the prefix, `[Proposal]` in the title.\n3. Follow the process outlined in the [POEM-1](./POEM-1-proposal-for-openwhisk-enhancements.md).\n\n## When should I open a proposal?\nCreate an issue when you propose:\n * any major new feature, subsystem, or piece of functionality\n * a big change which requires multiple PRs to complete it\n * a breaking change that impacts public interfaces, data structures, or core execution paths.\n\nIf you are not sure, ask to committers or the [PMC] of OpenWhisk.\n\nThe POEM process was inspired by KEP( Kubernetes Enhancement Proposals), KIP (Kafka Improvement Proposal), and Cordova-discuss.\n"
  },
  {
    "path": "settings.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'com.gradle.develocity' version '3.18.2'\n    id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.0.2'\n}\n\ndef isGithubActions = System.getenv('GITHUB_ACTIONS') != null\ndef isJenkins = System.getenv('JENKINS_URL') != null\ndef isCI = isGithubActions || isJenkins\n\ndevelocity {\n    server = \"https://develocity.apache.org\"\n    projectId = \"openwhisk\"\n    buildScan {\n        uploadInBackground = !isCI\n        publishing.onlyIf { it.isAuthenticated() }\n        obfuscation {\n            // This obfuscates the IP addresses of the build machine in the build scan.\n            // Alternatively, the build scan will provide the hostname for troubleshooting host-specific issues.\n            ipAddresses { addresses -> addresses.collect { address -> \"0.0.0.0\"} }\n        }\n    }\n}\n\nbuildCache {\n    local {\n        enabled = !isCI\n    }\n\n    remote(develocity.buildCache) {\n        enabled = false\n    }\n}\n\ninclude 'common:scala'\n\ninclude 'core:controller'\ninclude 'core:scheduler'\ninclude 'core:invoker'\ninclude 'core:cosmosdb:cache-invalidator'\ninclude 'core:standalone'\ninclude 'core:monitoring:user-events'\n\ninclude 'tests'\ninclude 'tests:performance:gatling_tests'\n\ninclude 'tools:actionProxy'\ninclude 'tools:ow-utils'\ninclude 'tools:dev'\n\ninclude 'tools:admin'\n\nrootProject.name = 'openwhisk'\n\n\ndef scalaVersion = System.getenv().getOrDefault('OW_SCALA_VERSION', '2.12')\n\nif (scalaVersion == '2.12') {\n    println(\"Build using Scala 2.12\")\n    gradle.ext.scala = [\n            version     : '2.12.10',\n            depVersion  : '2.12',\n            scoverageScalaVersion : '2.12.15',\n            scoverageVersion : '1.4.11',\n            compileFlags: ['-feature', '-unchecked', '-deprecation', '-Xfatal-warnings', '-Ywarn-unused-import']\n    ]\n} else {\n    println(\"Build using Scala 2.13\")\n    gradle.ext.scala = [\n            version     : '2.13.1',\n            depVersion  : '2.13',\n            scoverageScalaVersion : '2.13.1',\n            scoverageVersion : '1.4.11',\n            // We can't use fatal warnings yet because there are deprecated things in 2.13 that are not fixable\n            // in 2.12.\n            compileFlags: ['-feature', '-unchecked', '-deprecation']\n    ]\n}\n\ngradle.ext.scalafmt = [\n    version: '1.5.1',\n    config: new File(rootProject.projectDir, '.scalafmt.conf')\n]\n\n// Pekko versions\ngradle.ext.pekko = [version : '1.1.5']\ngradle.ext.pekko_kafka = [version : '1.1.0']\ngradle.ext.pekko_http = [version : '1.1.0']\ngradle.ext.pekko_management = [version : '1.1.1']\ngradle.ext.pekko_grpc = [version : '1.1.1']\ngradle.ext.grpc = [version : '1.75.0']\n\ngradle.ext.curator = [version : '5.7.0']\ngradle.ext.kube_client = [version: '4.10.3']\n\ngradle.ext.netty = [version : '4.1.128.Final']\n"
  },
  {
    "path": "tests/.pydevproject",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<?eclipse-pydev version=\"1.0\"?><pydev_project>\n<pydev_property name=\"org.python.pydev.PYTHON_PROJECT_INTERPRETER\">Default</pydev_property>\n<pydev_property name=\"org.python.pydev.PYTHON_PROJECT_VERSION\">python 2.7</pydev_property>\n</pydev_project>\n"
  },
  {
    "path": "tests/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Running Tests\n\nThis module hosts all the unit and integration test for this repo. Command examples given below are meant to be executed\nfrom project root.\n\nTo run all tests\n\n    $ ./gradlew tests:test\n\nThis requires the OpenWhisk system to be setup and running locally.\n\n## Running Unit Tests\n\nTo just run the unit tests\n\n    $ ansible-playbook -i ansible/environments/local ansible/properties.yml\n    $ ./gradlew tests:testUnit\n\n## Running System Basic Tests\n\nTo just run system basic test against an existing running setup you can pass on the server details and auth via system properties\n\n    $ ./gradlew :tests:testSystemBasic -Dwhisk.auth=\"23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\" -Dwhisk.server=https://localhost -Dopenwhisk.home=`pwd`\n\nHere\n\n* `whisk.auth` - Auth key for a test user account. For a setup using default credentials it can be `guest` key. (env `WHISK_AUTH`)\n* `whisk.server` - Edge Host Url of the OpenWhisk setup. (env `WHISK_SERVER`)\n* `opnewhisk.home` - Base directory of your OpenWhisk source tree. (env `OPENWHISK_HOME`)\n\nIf required you can relax the SSL check by passing `-Dwhisk.ssl.relax=true`. All these properties can also be provided via env variables.\n"
  },
  {
    "path": "tests/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport org.scoverage.ScoverageReport\nimport static groovy.json.JsonOutput.*\n\nplugins {\n    id 'eclipse'\n    id 'maven-publish'\n    id 'org.hidetake.swagger.generator' version '2.19.2'\n    id 'org.scoverage'\n    id 'scala'\n}\n\ncompileTestScala.options.encoding = 'UTF-8'\nproject.archivesBaseName = \"openwhisk-tests\"\n\nscoverage {\n    scoverageVersion.set(\"${gradle.scala.scoverageVersion}\")\n    scoverageScalaVersion.set(\"${gradle.scala.scoverageScalaVersion}\")\n}\n\ndef leanExcludes = [\n    '**/MaxActionDurationTests*',\n    'invokerShoot/**'\n]\n\ndef projectsWithCoverage = [\n    ':common:scala',\n    ':core:controller',\n    ':core:scheduler',\n    ':core:invoker',\n    ':tools:admin',\n    ':core:cosmosdb:cache-invalidator'\n]\n\ndef systemIncludes = [\n            \"org/apache/openwhisk/core/limits/**\",\n            \"org/apache/openwhisk/core/admin/**\",\n            \"org/apache/openwhisk/core/cli/test/**\",\n            \"org/apache/openwhisk/core/apigw/actions/test/**\",\n            \"org/apache/openwhisk/core/database/test/*CacheConcurrencyTests*\",\n            \"org/apache/openwhisk/core/controller/test/*ControllerApiTests*\",\n            \"org/apache/openwhisk/core/scheduler/queue/test/ElasticSearchDurationCheck*\",\n            \"apigw/healthtests/**\",\n            \"ha/**\",\n            \"services/**\",\n            \"system/basic/**\",\n            \"system/rest/**\",\n]\n\next.testSets = [\n    \"REQUIRE_ONLY_DB\" : [\n        \"includes\" : [\n            \"org/apache/openwhisk/**\"\n        ],\n        \"excludes\" : [\n            \"org/apache/openwhisk/core/admin/**\",\n            \"org/apache/openwhisk/core/apigw/actions/test/**\",\n            \"org/apache/openwhisk/standalone/**\",\n            \"org/apache/openwhisk/core/cli/test/**\",\n            \"org/apache/openwhisk/core/limits/**\",\n            \"org/apache/openwhisk/core/scheduler/**\",\n            \"org/apache/openwhisk/core/invoker/test/*InvokerBootUpTests*\",\n            \"org/apache/openwhisk/core/scheduler/queue/test/ElasticSearchDurationCheck*\",\n            \"org/apache/openwhisk/common/etcd/**\",\n            \"**/*CacheConcurrencyTests*\",\n            \"**/*ControllerApiTests*\",\n            \"org/apache/openwhisk/testEntities/**\",\n            \"invokerShoot/**\"\n        ]\n    ],\n    \"REQUIRE_SYSTEM\" : [\n        \"includes\" : systemIncludes,\n        \"excludes\": [\n            \"org/apache/openwhisk/core/loadBalancer/test/*FPCPoolBalancerTests*\",\n            \"system/basic/WskMultiRuntimeTests*\",\n            'invokerShoot/**'\n        ]\n    ],\n    \"REQUIRE_SCHEDULER\" : [\n        \"includes\" : [\n            \"org/apache/openwhisk/common/etcd/**\",\n            \"org/apache/openwhisk/core/containerpool/v2/test/**\",\n            \"org/apache/openwhisk/core/scheduler/**\",\n            \"org/apache/openwhisk/core/invoker/test/*InvokerBootUpTests*\",\n            \"org/apache/openwhisk/core/service/**\",\n        ]\n    ],\n    \"REQUIRE_MULTI_RUNTIME\" : [\n        \"includes\" : [\n            \"system/basic/*MultiRuntimeTests*\",\n            \"system/basic/*UnicodeTests*\",\n            \"limits/**\"\n        ]\n    ],\n    \"LEAN\" : [\n        \"excludes\" : leanExcludes\n    ],\n    \"REQUIRE_SYSTEM_BASIC\" : [\n        \"includes\" : [\n            \"system/basic/**\"\n        ]\n    ],\n    \"REQUIRE_LEAN_SYSTEM\" : [\n        \"includes\" : systemIncludes,\n\n        // Tests suits below require Kafka so they are excluded for Lean System tests\n        \"excludes\" : [\n            \"**/*KafkaConnectorTests*\",\n            \"system/basic/WskMultiRuntimeTests*\",\n            \"invokerShoot/**\"\n        ]\n    ],\n    \"REQUIRE_STANDALONE\" : [\n        \"includes\" : [\n            \"org/apache/openwhisk/standalone/**\"\n        ]\n    ]\n]\n\ntestSets.each {setName, patterns ->\n    def excludes = patterns[\"excludes\"] ?: new HashSet<>()\n    excludes.addAll(leanExcludes)\n    patterns[\"excludes\"] = excludes\n}\n\n//The value can be specified either via env variable\n// ORG_GRADLE_PROJECT_testSetName\n//Or via property -PtestSetName\nif (!project.hasProperty(\"testSetName\")) {\n    ext.testSetName = \"LEAN\"\n}\n\ndef getPattern(String name, String type) {\n    def patterns = testSets[name]\n    assert patterns : \"No pattern found for $name\"\n    return patterns[type] ?: []\n}\n\ndef logTestSetInfo(){\n    println \"Using testSet $testSetName - ${prettyPrint(toJson(testSets[testSetName]))}\"\n}\n\ntest {\n    // Use command line option \"-D testResultsDirName=<name>\" to change the default test results directory (\"test-results\").\n    // Specify a path relative to the build directory or an absolute path.\n    testResultsDirName = System.getProperty('testResultsDirName', testResultsDirName)\n\n    exclude 'invokerShoot/**'\n}\n\ntask testShootInvoker(type: Test) {\n    include 'invokerShoot/**'\n}\n\ntask testLean(type: Test) {\n    doFirst {\n        logTestSetInfo()\n    }\n    exclude getPattern(testSetName, \"excludes\")\n    include getPattern(testSetName, \"includes\")\n}\n\ntask testSystemBasic(type: Test) {\n    exclude getPattern(\"REQUIRE_SYSTEM_BASIC\", \"excludes\")\n    include getPattern(\"REQUIRE_SYSTEM_BASIC\", \"includes\")\n}\n\ntask testSystemKCF(type: Test) {\n    exclude getPattern(\"REQUIRE_SYSTEM_BASIC\", \"excludes\")\n    include getPattern(\"REQUIRE_SYSTEM_BASIC\", \"includes\")\n\n    // KubernetesContainerFactory's logs API is not reliable and mixes stdout/stderr into a single stream\n    exclude \"**/*ActivationLogsTests*\"\n\n    // These tests fail almost all the time in openwhisk-deploy-kube TravisCI.  Disabling to unblock PRs.\n    exclude \"**/*WskPackageTests*\"\n    exclude \"**/*WskSequenceTests*\"\n}\n\ntask testUnit(type: Test) {\n    systemProperty(\"whisk.spi.ArtifactStoreProvider\", \"org.apache.openwhisk.core.database.memory.MemoryArtifactStoreProvider\")\n    exclude getPattern(\"REQUIRE_ONLY_DB\", \"excludes\")\n    include getPattern(\"REQUIRE_ONLY_DB\", \"includes\")\n\n    //Test below have direct dependency on CouchDB running\n    def couchDbExcludes = [\n        \"**/*NamespaceBlacklistTests*\",\n        \"**/*CleanUpActivationsTest*\",\n        \"**/*CouchDbRestClientTests*\",\n        \"**/*RemoveLogsTests*\",\n        \"**/*ReplicatorTest*\",\n        \"**/*CouchDBArtifactStoreTests*\",\n        \"invokerShoot/**\"\n    ]\n\n    exclude couchDbExcludes\n}\n\ndependencies {\n    implementation \"org.scala-lang:scala-library:${gradle.scala.version}\"\n    implementation \"org.apache.commons:commons-lang3:3.18.0\"\n    implementation \"org.apache.httpcomponents:httpclient:4.5.14:tests\"\n    implementation \"org.apache.httpcomponents:httpmime:4.3.6\"\n    implementation \"junit:junit:4.11\"\n    implementation \"io.rest-assured:rest-assured:4.5.1\"\n    implementation \"org.scalatest:scalatest_${gradle.scala.depVersion}:3.2.14\"\n    implementation \"org.scalatestplus:junit-4-13_${gradle.scala.depVersion}:3.2.14.0\"\n    implementation \"org.scalatestplus:mockito-4-6_${gradle.scala.depVersion}:3.2.14.0\"\n    implementation \"org.apache.pekko:pekko-testkit_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    implementation \"com.google.code.gson:gson:2.10.1\"\n    implementation \"org.scalamock:scalamock_${gradle.scala.depVersion}:4.4.0\"\n    implementation \"org.apache.pekko:pekko-http-testkit_${gradle.scala.depVersion}:${gradle.pekko_http.version}\"\n    implementation \"com.github.java-json-tools:json-schema-validator:2.2.8\"\n    implementation \"org.mockito:mockito-core:2.27.0\"\n    implementation \"io.opentracing:opentracing-mock:0.31.0\"\n    implementation (\"org.apache.curator:curator-test:${gradle.curator.version}\") {\n        exclude group: 'log4j'\n    }\n    implementation \"com.atlassian.oai:swagger-request-validator-core:1.4.5\"\n    constraints {\n        implementation(\"com.atlassian.oai:swagger-request-validator-core:1.4.5\")\n        implementation(\"org.slf4j:slf4j-ext:1.7.36\") {\n            because 'swagger-request-validator-core cannot be upgraded to 2.x where vuln is remediated'\n        }\n    }\n    implementation \"io.github.embeddedkafka:embedded-kafka_${gradle.scala.depVersion}:3.9.1\"\n    constraints {\n        implementation(\"io.github.embeddedkafka:embedded-kafka_${gradle.scala.depVersion}:3.9.1\")\n        implementation(\"org.apache.kafka:kafka-clients:3.9.1\")\n    }\n    implementation (\"org.apache.zookeeper:zookeeper:3.9.3\") {\n        exclude group: 'org.slf4j'\n        exclude group: 'log4j'\n        exclude group: 'jline'\n    }\n    implementation \"org.apache.pekko:pekko-connectors-kafka-testkit_${gradle.scala.depVersion}:${gradle.pekko_kafka.version}\"\n    implementation \"org.apache.pekko:pekko-stream-testkit_${gradle.scala.depVersion}:${gradle.pekko.version}\"\n    implementation \"io.fabric8:kubernetes-server-mock:${gradle.kube_client.version}\"\n    implementation \"org.rogach:scallop_${gradle.scala.depVersion}:3.3.2\"\n\n    implementation \"com.amazonaws:aws-java-sdk-s3:1.12.395\"\n    implementation \"com.microsoft.azure:azure-cosmos:3.7.6\"\n    implementation 'org.testcontainers:elasticsearch:1.17.6'\n    implementation 'org.testcontainers:mongodb:1.17.1'\n    implementation project(':common:scala')\n    implementation project(':core:controller')\n    implementation project(':core:scheduler')\n    implementation project(':core:invoker')\n    implementation project(':core:cosmosdb:cache-invalidator')\n    implementation project(':core:monitoring:user-events')\n    implementation project(':tools:admin')\n\n    swaggerCodegen 'io.swagger:swagger-codegen-cli:2.4.29'\n}\n\ndef keystorePath = new File(sourceSets.test.scala.outputDir, 'keystore')\ntask deleteKeystore(type: Delete) {\n    delete keystorePath\n}\ntask createKeystore(dependsOn: deleteKeystore) {\n    doLast {\n        def propsFile = file('../whisk.properties')\n        if (propsFile.exists()) {\n            Properties props = new Properties()\n            props.load(new FileInputStream(propsFile))\n            keystorePath.parentFile.mkdirs()\n            def cmd = ['keytool', '-import', '-alias', 'Whisk', '-noprompt', '-trustcacerts', '-file', file(props['whisk.ssl.cert']), '-keystore', keystorePath, '-storepass', 'openwhisk']\n            cmd.execute().waitForProcessOutput(System.out, System.err)\n        }\n    }\n}\n\ntask buildArtifacts(type:Exec) {\n  workingDir 'dat/actions'\n  commandLine './build.sh'\n}\n\ntasks.withType(Test) {\n    dependsOn createKeystore\n    dependsOn buildArtifacts\n}\ncreateKeystore.mustRunAfter(testClasses)\nbuildArtifacts.mustRunAfter(testClasses)\n\ngradle.projectsEvaluated {\n    task testCoverageLean(type: Test) {\n        doFirst {\n            logTestSetInfo()\n        }\n        classpath = getScoverageClasspath(project, projectsWithCoverage)\n        exclude getPattern(testSetName, \"excludes\")\n        include getPattern(testSetName, \"includes\")\n    }\n\n    task testCoverage(type: Test) {\n        classpath = getScoverageClasspath(project, projectsWithCoverage)\n    }\n\n    tasks.withType(ScalaCompile) {\n        configure(scalaCompileOptions.forkOptions) {\n            memoryMaximumSize = '1g'\n        }\n    }\n\n    tasks.withType(Test) {\n        systemProperties(System.getProperties())\n        systemProperty(\"whisk.server.jar\", getStandaloneJarFilePath())\n\n        testLogging {\n            events \"passed\", \"skipped\", \"failed\"\n            showStandardStreams = true\n            exceptionFormat = 'full'\n        }\n        maxHeapSize = \"1024m\" //Gradle 5.5 defaults to 512MB which is low\n        outputs.upToDateWhen { false } // force tests to run every time\n    }\n    /**\n     * Task to generate coverage xml report. Requires the\n     * tests to be executed prior to its invocation\n     */\n    task reportCoverage(type: ScoverageReport) {\n        def dependentTasks = []\n        dependentTasks << copyMeasurementFiles\n        projectsWithCoverage.forEach {\n            dependentTasks << it + ':reportScoverage'\n            dependentTasks << it + ':processScoverageResources'\n        }\n        dependentTasks << 'compileScoverageScala'\n        dependsOn(dependentTasks)\n\n        //Need to recreate the logic from\n        //https://github.com/scoverage/gradle-scoverage/blob/924bf49a8f981f119d0604b44a782f3f8eecb359/src/main/groovy/org/scoverage/ScoveragePlugin.groovy#L137\n        //default tasks retrigger the tests. As ours is a multi module integration\n        //test we have to adapt the classpath and hence cannot use default reportXXXScoverage tasks\n        runner = new org.scoverage.ScoverageRunner(project.configurations.scoverage)\n        reportDir = reportTestScoverage.reportDir\n        sources = reportTestScoverage.sources\n        sourceEncoding = 'UTF-8'\n        dataDir = reportTestScoverage.dataDir\n        coverageOutputCobertura = reportTestScoverage.coverageOutputCobertura\n        coverageOutputXML = reportTestScoverage.coverageOutputXML\n        coverageOutputHTML = reportTestScoverage.coverageOutputHTML\n        coverageDebug = reportTestScoverage.coverageDebug\n    }\n}\n\ntask copyMeasurementFiles() {\n    doLast{\n        Project common = project(\":common:scala\")\n        Project controller = project(\":core:controller\")\n        Project scheduler = project(\":core:scheduler\")\n        Project invoker = project(\":core:invoker\")\n\n        Properties wskProps = loadWhiskProps()\n        String covLogsDir = wskProps.getProperty('whisk.coverage.logs.dir')\n        assert covLogsDir : \"Did not find coverage logs property 'whisk.coverage.logs.dir' in whisk props\"\n\n        File covLogs = new File(covLogsDir)\n\n        copyAndRenameMeasurementFile(covLogs, 'controller', \"common\", common)\n        copyAndRenameMeasurementFile(covLogs, 'controller', \"controller\", controller)\n        copyAndRenameMeasurementFile(covLogs, 'scheduler', \"common\", common)\n        copyAndRenameMeasurementFile(covLogs, 'scheduler', \"scheduler\", scheduler)\n        copyAndRenameMeasurementFile(covLogs, 'invoker', \"common\", common)\n        copyAndRenameMeasurementFile(covLogs, 'invoker', \"invoker\", invoker)\n    }\n}\n\n\n/**\n * Scoverage measurement files are named like scoverage.measurements.xxx. Where xxx is thread id. While\n * consolidating the files between container run and normal test run we need to rename the files generated by\n * container run so that the file name becomes unique\n */\ndef copyAndRenameMeasurementFile(File covLogDir, String containerName, String moduleName, Project dest){\n    File dir = new File(new File(covLogDir, containerName), moduleName)\n    if (!dir.exists()) {\n        println \"Coverage logs directory ${dir.absolutePath} does not exist. Skipping measurement file collection\"\n        return\n    }\n    copy{\n        from(dir)\n        into(\"${dest.buildDir.absolutePath}/scoverage\")\n        rename {it+\".$containerName-container\"}\n    }\n}\n\ndef loadWhiskProps(){\n    Properties p = new Properties()\n    file('../whisk.properties').withInputStream {is ->\n        p.load(is)\n    }\n    p\n}\n\n/**\n * Prepares the classpath which refer to scoverage instrumented classes from\n * dependent projects \"before\" the non instrumented classes\n */\ndef getScoverageClasspath(Project project, List<String> projectNames) {\n    def combinedClasspath = projectNames.inject(project.files([])) { result, name ->\n        def cp = project.project(name).sourceSets.scoverage.runtimeClasspath\n        result + cp.filter {it.name.contains('scoverage')}\n    }\n\n    combinedClasspath + sourceSets.test.runtimeClasspath\n}\n\nswaggerSources {\n    java {\n        inputFile = file(\"$projectDir/../core/controller/src/main/resources/apiv1swagger.json\")\n        code {\n            language = 'java'\n            configFile = file('src/test/resources/swagger-config.json')\n            templateDir = file('src/test/resources/templates')\n            dependsOn validation\n        }\n    }\n}\n\ntask testSwaggerCodegen(type: GradleBuild) {\n    dependsOn swaggerSources.java.code\n    buildFile = \"${buildDir}/swagger-code-java/build.gradle\"\n    tasks = ['build']\n}\n\ndef getStandaloneJarFilePath(){\n    project(\":core:standalone\").tasks.named(\"bootJar\").get().archiveFile.get().asFile\n}\n"
  },
  {
    "path": "tests/dat/actions/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n### Building test artifacts\n\nSome test artifacts should be build ahead of actually running the tests.\nThese test artifacts compile source code into binaries, generate JAR files,\ninstall modules and package functions into zip files.\n\nA `build.sh` script is available as a convenience and will run ahead of tests\nwhen using `gradle`. However this script will not work on Windows as it currently\nassumes Bash is available.\n\nSome artifacts are not build because they require additional tooling.\nSpecifically, these are the unicode tests for the Ballerina and .NET runtimes.\nThese unicode tests are checked however in their corresponding runtime build.\n\nFor Java artifacts, Java 8 is required. If not found in your path, the build\nscript will skip those artifacts and corresponding tests will also be skipped.\n\n"
  },
  {
    "path": "tests/dat/actions/applicationError.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(args) {\n    return { error: \"This error thrown on purpose by the action.\"};\n}\n"
  },
  {
    "path": "tests/dat/actions/argCheck.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(params) {\n    return {payload: params.payload};\n}\n"
  },
  {
    "path": "tests/dat/actions/argsPrint.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(params) {\n    var param1 = params.param1 || '';\n    var param2 = params.param2 || '';\n    return {param1: param1, param2: param2};\n}\n"
  },
  {
    "path": "tests/dat/actions/asyncError.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(params) {\n    return Promise.reject({msg: 'failed activation on purpose'});\n}\n"
  },
  {
    "path": "tests/dat/actions/blackbox/exec",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\necho 'This is an example zip used with the docker skeleton action.'\necho '{ \"msg\": \"hello zip\" }'\n"
  },
  {
    "path": "tests/dat/actions/build.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nif [ -f \".built\" ]; then\n  echo \"Test zip artifacts already built, skipping\"\n  exit 0\nfi\n\n# need java 8 to build java actions since that's the version of the runtime currently\njv=$(java -version 2>&1 | head -1 | awk -F'\"' '{print $2}')\nif [[ $jv == 1.8.* ]]; then\n  echo \"java version is $jv (ok)\"\n  (cd unicode.tests/src/java/unicode && ../../../../../../../gradlew build && cp build/libs/unicode-1.0.jar ../../../java-8.bin)\n  (cd src/java/sleep && ../../../../../../gradlew build && cp build/libs/sleep-1.0.jar ../../../sleep.jar)\nelse\n  echo \"java version is $jv (not ok)\"\n  echo \"skipping java actions\"\nfi\n\n(cd blackbox && zip ../blackbox.zip exec)\n(cd python-zip && zip ../python.zip -r .)\n(cd zippedaction && npm install && zip ../zippedaction.zip -r .)\n\ntouch .built\n"
  },
  {
    "path": "tests/dat/actions/cat.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Equivalent to unix cat command.\n * Return all the lines in an array. All other fields in the input message are stripped.\n * @param lines An array of strings.\n */\nfunction main(msg) {\n    var lines = msg.lines || [];\n    var retn = {lines: lines, payload: lines.join(\"\\n\")};\n    console.log('cat: returning ' + JSON.stringify(retn));\n    return retn;\n}\n"
  },
  {
    "path": "tests/dat/actions/concurrent.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nlet counter = 0;\nlet requestCount = undefined;\nlet interval = 100;\nfunction main(args) {\n    //args.warm == 1 => indicates a warmup activation (to avoid multiple containers when concurrent activations arrive at prewarm state)\n    //args.requestCount == n => indicates number of activations needed to arrive before returning results to ANY of the pending activations\n    if (args.warm == 1) {\n        return {warm: 1};\n    } else {\n        requestCount = requestCount || args.requestCount;\n        counter++;\n        console.log(\"counter: \"+counter + \" requestCount: \"+requestCount);\n        return new Promise(function(resolve, reject) {\n            setTimeout(function() {\n                checkRequests(args, resolve, reject);\n            }, interval);\n        });\n    }\n\n}\nfunction checkRequests(args, resolve, reject, elapsed) {\n    let elapsedTime = elapsed||0;\n    if (counter == requestCount) {\n        const result = {msg: \"Received \" + counter + \" activations.\"};\n        console.log(result.msg);\n        resolve(result);\n    } else {\n        if (elapsedTime > 30000) {\n            reject(\"did not receive \"+requestCount+\" activations within 30s\");\n        } else {\n            setTimeout(function() {\n                checkRequests(args, resolve, reject, elapsedTime+interval);\n            }, interval);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/concurrentFail1.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nlet count=0;\nfunction main(args) {\n    console.log(\"sleeping for \"+(args.time||1000));\n    var shouldFail = args.fail||false;\n    var sleepTime = args.time||1000;\n    count = count+1;\n    if (shouldFail) {\n        console.log(\"skipping the return..\");\n        return new Promise();\n    } else {\n        return new Promise(function (resolve, reject) {\n            setTimeout(function () {\n                resolve({body: \"done sleeping \"+sleepTime, done: true});\n            }, sleepTime);\n        })\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/concurrentFail2.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nlet count=0;\nfunction main(args) {\n    console.log(\"sleeping for \"+(args.time||1000));\n    var sleepTime = args.time||1000;\n    var shouldFail = count % 2 === 0;\n    count = count+1;\n    if (shouldFail) {\n        console.log(\"a catastrophic failure..\");\n        process.exit(123);\n    } else {\n        return new Promise(function (resolve, reject) {\n            setTimeout(function () {\n                resolve({body: \"done sleeping \"+sleepTime, done: true});\n            }, sleepTime);\n        })\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/conductor.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Minimal conductor action.\n */\nfunction main(args) {\n    // propagate errors\n    if (args.error) return args\n    // unescape params: { action, state, foo, params: { bar } } becomes { action, state, params: { foo, bar } }\n    const action = args.action\n    const state = args.state\n    const params = args.params\n    delete args.action\n    delete args.state\n    delete args.params\n    return { action, state, params: Object.assign(args, params) }\n}\n"
  },
  {
    "path": "tests/dat/actions/corsHeaderMod.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main() {\n    return {\n        headers: {\n            \"Access-Control-Allow-Origin\": \"Origin set from Web Action\",\n            \"Access-Control-Allow-Headers\": \"Headers set from Web Action\",\n            \"Access-Control-Allow-Methods\": \"Methods set from Web Action\",\n            \"Location\": \"openwhisk.org\",\n            \"Set-Cookie\": \"cookie-cookie-cookie\"\n        },\n        statusCode: 200\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/countdown.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * An action that invokes itself recursively, programmatically using the whisk\n * Javascript API.\n */\nvar openwhisk = require('openwhisk')\n\nfunction main(params) {\n    var wsk = openwhisk({ignore_certs: true})\n\n    var n = parseInt(params.n);\n    console.log(n);\n    if (n === 0) {\n        console.log('Happy New Year!');\n    } else if (n > 0) {\n        return wsk.actions.invoke({\n            actionName: process.env['__OW_ACTION_NAME'],\n            params: { n: n - 1 }\n        });\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/createRule.swift",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunc main(args: [String:Any]) -> [String:Any] {\n  guard let triggerName = args[\"triggerName\"] as? String else {\n      return [\"error\": \"You must specify a triggerName parameter!\"]\n  }\n  guard let actionName = args[\"actionName\"] as? String else {\n      return [\"error\": \"You must specify a actionName parameter!\"]\n  }\n  guard let ruleName = args[\"ruleName\"] as? String else {\n      return [\"error\": \"You must specify a ruleName parameter!\"]\n  }\n  print(\"Rule Name: \\(ruleName), Trigger Name: \\(triggerName), actionName: \\(actionName)\")\n  return Whisk.createRule(ruleNamed: ruleName, withTrigger: triggerName, andAction: actionName)\n}\n"
  },
  {
    "path": "tests/dat/actions/createTrigger.swift",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunc main(args: [String:Any]) -> [String:Any] {\n guard let triggerName = args[\"triggerName\"] as? String else {\n    return [\"error\": \"You must specify a triggerName parameter!\"]\n  }\n  print(\"Trigger Name: \\(triggerName)\")\n  return Whisk.createTrigger(triggerNamed: triggerName, withParameters: [:])\n}\n"
  },
  {
    "path": "tests/dat/actions/dosLogs.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(msg) {\n    // Each log line with 16 characters (new line character counts)\n    var lines = msg.payload / 16 || 1;\n    for(var i = 1; i <= lines; i++) {\n        console.log(\"123456789abcdef\");\n    }\n    return {msg: lines};\n}\n"
  },
  {
    "path": "tests/dat/actions/echo-web-http-head.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(params) {\n    return {\n        statusCode: 200,\n        headers: { 'Request-type': params.__ow_method }\n    };\n}\n"
  },
  {
    "path": "tests/dat/actions/echo-web-http.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(params) {\n    return {\n        statusCode: 200,\n        headers: { 'Content-Type': 'application/json' },\n        body: params\n    };\n}\n"
  },
  {
    "path": "tests/dat/actions/echo.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Returns params, or an empty string if no parameter values are provided\n */\nfunction main(params) {\n    return params || {};\n}\n"
  },
  {
    "path": "tests/dat/actions/echo.json",
    "content": "{\n    \"name\": \"echo\",\n    \"version\": \"0.0.1\",\n    \"publish\": false,\n    \"exec\": {\n        \"kind\": \"nodejs:20\",\n        \"code\": \"/**\\n * Returns params, or an empty string if no parameter values are provided\\n */\\nfunction main(params) {\\n    return params || {};\\n}\\n\\n\"\n    },\n    \"annotations\": [],\n    \"parameters\": [],\n    \"limits\": {\n        \"timeout\": 60000,\n        \"memory\": 256,\n        \"logs\": 10\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/empty.js",
    "content": ""
  },
  {
    "path": "tests/dat/actions/emptyJSONResult.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(params) {\n}\n"
  },
  {
    "path": "tests/dat/actions/head.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Return the first num lines of an array.\n * @param lines An array of strings.\n * @param num Number of lines to return.\n */\nfunction main(msg) {\n    var lines = msg.lines || [];\n    var num = msg.num || 1;\n    var head = lines.slice(0, num);\n    console.log('head get first ' + num + ' lines of ' + lines + ': ' + head);\n    return {lines: head, num: num};\n}\n"
  },
  {
    "path": "tests/dat/actions/hello.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Hello, world.\n */\nfunction main(params) {\n    greeting = 'hello, ' + params.payload + '!'\n    console.log(greeting);\n    return {payload: greeting}\n}\n"
  },
  {
    "path": "tests/dat/actions/hello.py",
    "content": "\"\"\"Python Hello test.\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\n\ndef main(args):\n    \"\"\"Main.\"\"\"\n    name = args.get('name', 'stranger')\n    greeting = 'Hello ' + name + '!'\n    print(greeting)\n    return {'greeting': greeting}\n"
  },
  {
    "path": "tests/dat/actions/hello.swift",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Hello world as a Swift Whisk action.\n */\nfunc main(args: [String:Any]) -> [String:Any] {\n    if let name = args[\"name\"] as? String {\n        return [ \"greeting\" : \"Hello \\(name)!\" ]\n    } else {\n        return [ \"greeting\" : \"Hello stranger!\" ]\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/helloArray.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * helloArray action\n */\n\nfunction main(params) {\n    return [{\"key1\":\"value1\"},{\"key2\":\"value2\"}];\n}\n"
  },
  {
    "path": "tests/dat/actions/helloAsync.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * word count utility, coded as an asynchronous action for pedagogical\n * purposes\n */\nfunction wc(params) {\n    var str = params.payload;\n    var words = str.split(\" \");\n    var count = words.length;\n    console.log(\"The message '\"+str+\"' has\", count, 'words');\n    return {count: count};\n}\n\nfunction main(params) {\n    return new Promise(function(resolve, reject) {\n        setTimeout(function () {\n            resolve(wc(params));\n        }, 100);\n    });\n}\n"
  },
  {
    "path": "tests/dat/actions/helloContext.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(args) {\n    return {\n       \"api_host\": process.env['__OW_API_HOST'],\n       \"api_key\": process.env['__OW_API_KEY'],\n       \"namespace\": process.env['__OW_NAMESPACE'],\n       \"action_name\": process.env['__OW_ACTION_NAME'],\n       \"action_version\": process.env['__OW_ACTION_VERSION'],\n       \"activation_id\": process.env['__OW_ACTIVATION_ID'],\n       \"deadline\": process.env['__OW_DEADLINE']\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/helloDeadline.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(args) {\n\n    var deadline = process.env['__OW_DEADLINE']\n    var timeleft = deadline - new Date().getTime()\n    console.log(\"deadline in \" + timeleft + \" msecs\");\n\n    var timer = function () {\n       var timeleft = deadline - new Date().getTime()\n       console.log(\"deadline in \" + timeleft + \" msecs\");\n    }\n    var alarm = setInterval(timer, 1000);\n\n    return new Promise(function(resolve, reject) {\n       setTimeout(function() {\n          clearInterval(alarm);\n          if (args.forceHang) {\n              // do not resolve the promise and make the action timeout\n          } else {\n              resolve({ timedout: true });\n          }\n       }, timeleft - 500);\n    })\n\n}\n"
  },
  {
    "path": "tests/dat/actions/helloOpenwhiskPackage.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nvar openwhisk = require('openwhisk')\n\nfunction main(args) {\n    var wsk = openwhisk({ignore_certs: args.ignore_certs})\n\n    return new Promise(function (resolve, reject) {\n        return wsk.actions.list().then(list => {\n            console.log(\"action list has this many actions:\", list.length)\n            if (args.name) {\n                console.log('deleting')\n                return wsk.actions.delete({actionName: args.name}).then(result => {\n                    resolve({delete: true})\n                }).catch(function (reason) {\n                    console.log('error', reason)\n                    reject(reason)\n                })\n            } else {\n                console.log('ok')\n                resolve({list: true})\n            }\n        }).catch(function (reason) {\n            console.log('error', reason)\n            reject(reason);\n        })\n    })\n}\n"
  },
  {
    "path": "tests/dat/actions/helloPromise.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(args) {\n    return new Promise(function(resolve, reject) {\n        setTimeout(function() {\n            resolve({\n                done : true\n            });\n        }, 2000);\n    })\n}\n"
  },
  {
    "path": "tests/dat/actions/httpGet.swift",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Sample code using the experimental Swift 3 runtime\n * with links to KituraNet and GCD\n */\n\nimport KituraNet\nimport Dispatch\nimport Foundation\nimport SwiftyJSON\n\nfunc main(args:[String:Any]) -> [String:Any] {\n\n    // Force KituraNet call to run synchronously on a global queue\n    var str = \"No response\"\n    dispatch_sync(dispatch_get_global_queue(0, 0)) {\n\n            HTTP.get(\"https://httpbin.org/get\") { response in\n\n                do {\n                   str = try response!.readString()!\n                } catch {\n                    print(\"Error \\(error)\")\n                }\n\n            }\n    }\n\n    // Assume string is JSON\n    print(\"Got string \\(str)\")\n    var result:[String:Any]?\n\n    // Convert to NSData\n    let data = str.data(using: NSUTF8StringEncoding, allowLossyConversion: true)!\n\n    // test SwiftyJSON\n    let json = JSON(data: data)\n    if let jsonUrl = json[\"url\"].string {\n        print(\"Got json url \\(jsonUrl)\")\n    } else {\n        print(\"JSON DID NOT PARSE\")\n    }\n\n    // create result object to return\n    do {\n        result = try NSJSONSerialization.jsonObject(with: data, options: []) as? [String: Any]\n    } catch {\n        print(\"Error \\(error)\")\n    }\n\n    // return, which should be a dictionary\n    print(\"Result is \\(result!)\")\n    return result!\n}\n"
  },
  {
    "path": "tests/dat/actions/initexit.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nprocess.exit(1);\n"
  },
  {
    "path": "tests/dat/actions/initforever.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nwhile (true) {}\n"
  },
  {
    "path": "tests/dat/actions/invalidInput1.json",
    "content": "{\n  \"invalidJSON\":\n}\n"
  },
  {
    "path": "tests/dat/actions/invalidInput2.json",
    "content": "{\n  \"invalid\": \"JS\n  ON\"\n}\n"
  },
  {
    "path": "tests/dat/actions/invalidInput3.json",
    "content": "{\n  \"invalid\": \"JSON\"\n"
  },
  {
    "path": "tests/dat/actions/invalidInput4.json",
    "content": "{\n  \"invalid\": \"JS\"ON\"\n}\n"
  },
  {
    "path": "tests/dat/actions/invoke.swift",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport SwiftyJSON\n\nfunc main(args: [String:Any]) -> [String:Any] {\n  let invokeResult = Whisk.invoke(actionNamed: \"/whisk.system/utils/date\", withParameters: [:])\n  let dateActivation = JSON(invokeResult)\n\n  // the date we are looking for is the result inside the date activation\n  if let dateString = dateActivation[\"response\"][\"result\"][\"date\"].string {\n    print(\"It is now \\(dateString)\")\n  } else {\n    print(\"Could not parse date of of the response.\")\n  }\n\n  // return the entire invokeResult\n  return invokeResult\n}\n"
  },
  {
    "path": "tests/dat/actions/invokeNonBlocking.swift",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport SwiftyJSON\n\nfunc main(args: [String:Any]) -> [String:Any] {\n  let invokeResult = Whisk.invoke(actionNamed: \"/whisk.system/utils/date\", withParameters: [:], blocking: false)\n  let dateActivation = JSON(invokeResult)\n\n  // the date we are looking for is the result inside the date activation\n  if let activationId = dateActivation[\"activationId\"].string {\n    print(\"Invoked.\")\n  } else {\n    print(\"Failed to invoke.\")\n  }\n\n  // return the entire invokeResult\n  return invokeResult\n}\n"
  },
  {
    "path": "tests/dat/actions/jsonStringWebAction.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main() {\n    return {\n        headers: {\n            \"content-type\": \"application/json\"\n        },\n        statusCode: 200,\n        body: '{\"status\": \"success\"}'\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/log.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Emit strings to stdout/stderr.\n */\nfunction main() {\n    console.log(\"this is stdout\");\n    console.error(\"this is stderr\");\n}\n"
  },
  {
    "path": "tests/dat/actions/loggingTimeout.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// This action prints log lines for a defined duration.\n// The output is throttled by a defined delay between two log lines\n// in order to keep the log size small and to stay within the log size limit.\n\nfunction getArg(value, defaultValue) {\n   return value ? value : defaultValue;\n}\n\n// input: { duration: <duration in millis>, delay: <delay in millis> }, e.g.\n// main( { delay: 100, duration: 10000 } );\nfunction main(args) {\n\n   durationMillis = getArg(args.duration, 120000);\n   delayMillis = getArg(args.delay, 100);\n\n   logLines = 0;\n   startMillis = new Date();\n\n   timeout = setInterval(function() {\n      console.log(`[${ ++logLines }] The quick brown fox jumps over the lazy dog.`);\n   }, delayMillis);\n\n   return new Promise(function(resolve, reject) {\n      setTimeout(function() {\n         clearInterval(timeout);\n         message = `hello, I'm back after ${new Date() - startMillis} ms and printed ${logLines} log lines`\n         console.log(message)\n         resolve({ message: message });\n      }, durationMillis);\n   });\n\n}\n\n"
  },
  {
    "path": "tests/dat/actions/malformed.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nx\n"
  },
  {
    "path": "tests/dat/actions/malformed.py",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Invalid Python comment test.\"\"\"\n// invalid python comment  # noqa -- tell linters to ignore the intentional syntax error\n"
  },
  {
    "path": "tests/dat/actions/memoryWithGC.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use strict';\n\n// Usually, Linux systems have a page size of 4096 byte\n// The actual value can be obtained with `getconf PAGESIZE`\nconst pageSizeInB = 4096;\n\n// This array will be used to store all allocated blocks\n// such that they won't be garbage collected\nlet blocks = [];\n\n// Allocates a byte array that has page size\nfunction allocateMemoryBlock(sizeInB) {\n    return new Uint8Array(sizeInB);\n}\n\n// Returns a random number between 0 (inclusive) and\n// the specified value maxExclusive (exclusive)\nfunction randomUnsigned(maxExclusive) {\n    return Math.floor(Math.random() * maxExclusive);\n}\n\n// Fills the first 4 bytes of the passed byte array with random\n// numbers\nfunction fillMemoryPage(byteArray) {\n    for (let i = 0; (i < 4) && (i < pageSizeInB); i++) {\n        byteArray[i] = randomUnsigned(256);\n    }\n}\n\n// Consumes the specified amount of physical memory\n// * The memory is allocated in smaller blocks instead of\n//   allocating one large block of memory to prevent\n//   virtual OOM\n// * Size of allocated blocks is a multiple of page size\n// * The number of allocated blocks has an upper bound\n//   because a reference to each block is stored in an\n//   array. If the number of blocks gets too high, the\n//   resulting array grows so large that its contribution\n//   to memory consumption causes trouble.\n//   For this reason, the block size is adjusted to\n//   limit the number of blocks. The resulting allocation\n//   granularity can cause a slight over-consumption of\n//   memory. That's why the upper limit must be selected\n//   carefully.\n// * Fill randomly to prevent memory deduplication\nfunction eat(memoryInMiB) {\n    const memoryInB = memoryInMiB * 1024 * 1024;\n    const memoryInPages = Math.ceil(memoryInB / pageSizeInB);\n    console.log('helloEatMemory: memoryInB=' + memoryInB + ', memoryInPages=' + memoryInPages);\n\n    let blockSizeInB = pageSizeInB;\n    let memoryInBlocks = memoryInPages;\n    let pagesPerBlock = 1;\n    const maxBlocks = 8192;\n    if (memoryInPages > maxBlocks) {\n        pagesPerBlock = Math.ceil(memoryInB / (maxBlocks * pageSizeInB));\n        blockSizeInB = pagesPerBlock * pageSizeInB;\n        memoryInBlocks = Math.ceil(memoryInB / blockSizeInB);\n    }\n    console.log('helloEatMemory: pagesPerBlock=' + pagesPerBlock + ', blockSizeInB=' + blockSizeInB + ', memoryInBlocks=' + memoryInBlocks);\n\n    for (let b = 0; b < memoryInBlocks; b++) {\n        let byteArray = allocateMemoryBlock(blockSizeInB);\n        fillMemoryPage(byteArray);\n        blocks.push(byteArray);\n    }\n    console.log('helloEatMemory: blocks.length=' + blocks.length);\n}\n\nfunction main(msg) {\n    console.log('helloEatMemory: memory ' + msg.payload + 'MB');\n    global.gc();\n    eat(msg.payload);\n\n    console.log('helloEatMemory: completed allocating memory');\n    console.log(process.memoryUsage());\n\n    // This Node.js code is invoked as Apache OW action\n    // We need to explicitly clear the array such that all\n    // allocated memory gets garbage collected and\n    // we have a \"fresh\" instance on next invocation\n    // Clean up after ourselves such that the warm container\n    // does not keep memory\n    blocks = [];\n    global.gc();\n\n    return {msg: 'OK, buffer of size ' + msg.payload + ' MB has been filled.'};\n}\n\n"
  },
  {
    "path": "tests/dat/actions/multipleHeaders.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main() {\n    return {\n        headers: {\n            \"Set-Cookie\": [\"a=b\", \"c=d\"]\n        },\n        statusCode: 200\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/niam.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction niam(args) {\n    return { 'greetings': 'Hello from a non-standard entrypoint.' };\n}\n"
  },
  {
    "path": "tests/dat/actions/niam.py",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Python Non-standard entry point test.\"\"\"\n\n\ndef niam(args):\n    \"\"\"Non-standard entry point.\"\"\"\n    return {\"greetings\": \"Hello from a non-standard entrypoint.\"}\n"
  },
  {
    "path": "tests/dat/actions/niam.swift",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/* Swift action with a non-default entry point. */\nfunc niam(args: [String:Any]) -> [String:Any] {\n    return [ \"greetings\" : \"Hello from a non-standard entrypoint.\" ]\n}\n"
  },
  {
    "path": "tests/dat/actions/openFiles.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nvar fs = require(\"fs\");\n\nfunction main(params) {\n\n    var numFiles = params.numFiles;\n    var openFiles = [];\n    var error = undefined;\n\n    try {\n        for (var i = 0; i < numFiles; i++) {\n            var fh = fs.openSync(\"/dev/zero\", \"r\");\n            openFiles.push(fh);\n        }\n    } catch (err) {\n        console.log(\"ERROR: opened files = \", openFiles.length);\n        error = err;\n    }\n\n    console.log(\"opened files = \", openFiles.length);\n\n    openFiles.forEach(function(fh) {\n        fs.close(fh, (err) => {} );\n    })\n\n    if (error === undefined) {\n        return {\n            filesToOpen : numFiles,\n            filesOpen : openFiles.length,\n        }\n    } else {\n        return {\n            error : {\n                filesToOpen : numFiles,\n                filesOpen : openFiles.length,\n                message : error\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/params.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(args) {\n  return {args}\n}\n"
  },
  {
    "path": "tests/dat/actions/ping.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(msg) {\n    var hostToPing = msg.payload;\n    console.log('Pinging to ' + hostToPing);\n\n    var spawn = require('child_process').exec;\n    var promise = new Promise(function(resolve, reject) {\n        var child = spawn('ping -c 3 ' + hostToPing);\n\n        var tmp = {stdout: \"\", stderr: \"\", code: 0};\n\n        child.stdout.on('data', function (data) {\n            tmp.stdout = tmp.stdout + data;\n        });\n\n        child.stderr.on('data', function (data) {\n            tmp.stderr = tmp.stderr + data;\n        });\n\n        child.on('close', function (code) {\n            tmp.code = code;\n            console.log('code', tmp.code);\n            console.log('stdout', tmp.stdout);\n            console.log('stderr', tmp.stderr);\n            resolve(tmp);\n        });\n    });\n\n    return promise;\n}\n"
  },
  {
    "path": "tests/dat/actions/pngWeb.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main() {\n    var png = \"iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAA/klEQVQYGWNgAAEHBxaG//+ZQMyyn581Pfas+cRQnf1LfF\" +\n        \"Ljf+62smUgcUbt0FA2Zh7drf/ffMy9vLn3RurrW9e5hCU11i2azfD4zu1/DHz8TAy/foUxsXBrFzHzC7r8+M9S1vn1qxQT07\" +\n        \"dDjL9fdemrqKxlYGT6z8AIMo6hgeUfA0PUvy9fGFh5GWK3z7vNxSWt++jX99+8SoyiGQwsW38w8PJEM7x5v5SJ8f+/xv8MDA\" +\n        \"zffv9hevfkWjiXBGMpMx+j2awovjcMjFztDO8+7GF49LkbZDCDeXLTWnZO7qDfn1/+5jbw/8pjYWS4wZLztXnuEuYTk2M+Mz\" +\n        \"Iw/AcA36VewaD6fzsAAAAASUVORK5CYII=\"\n\n    return {\n        statusCode: 200,\n        headers: {'content-type': 'image/png'},\n        body: png\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/printParams.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Print the parameters to the console, sorted alphabetically by key\n */\nfunction main(params) {\n    var sep = '';\n    var retn = {};\n    var keys = [];\n\n    for (var key in params) {\n        if (params.hasOwnProperty(key)) {\n            keys.push(key);\n        }\n    }\n\n    keys.sort();\n    for (var i in keys) {\n        var key = keys[i];\n        var value = params[key];\n        console.log(sep + 'params.' + key + ':', value);\n        sep = ' ';\n        retn[key] = value;\n    }\n\n    return {params: retn};\n}\n"
  },
  {
    "path": "tests/dat/actions/python-zip/__main__.py",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom greet import greet\n\n\ndef niam(args):\n    return greet(args)\n"
  },
  {
    "path": "tests/dat/actions/python-zip/greet.py",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\ndef greet(dict):\n    if 'name' in dict:\n        name = dict['name']\n    else:\n        name = 'stranger'\n    greeting = 'Hello ' + name + '!'\n    return {'greeting': greeting}\n"
  },
  {
    "path": "tests/dat/actions/pythonVersion.py",
    "content": "\"\"\"Python Version test.\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\nimport sys\n\ndef main(args):\n    \"\"\"Main.\"\"\"\n    return {\"version\": sys.version_info.major}\n"
  },
  {
    "path": "tests/dat/actions/runexception.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main() {\n    throw \"Extraordinary exception\"\n}\n"
  },
  {
    "path": "tests/dat/actions/runexit.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main() {\n    process.exit(1);\n}\n"
  },
  {
    "path": "tests/dat/actions/sizedResult.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(args) {\n    let str = new Array(args.size).join(args.char)\n    return {\n        msg : str\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/sleep.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Node.js based OpenWhisk action that sleeps for the specified number\n * of milliseconds before returning. Uses a timer instead of a busy loop.\n * The function actually sleeps slightly longer than requested.\n *\n * @param parm Object with Number property sleepTimeInMs\n * @returns Object with String property msg describing how long the function slept\n *          or an error object on failure\n */\nfunction main(parm) {\n    if(!('sleepTimeInMs' in parm)) {\n        const result = { error: \"Parameter 'sleepTimeInMs' not specified.\" }\n        console.error(result.error)\n        return result\n    }\n\n    if(!Number.isInteger(parm.sleepTimeInMs)) {\n        const result = { error: \"Parameter 'sleepTimeInMs' must be an integer value.\" }\n        console.error(result.error)\n        return result\n    }\n\n    if((parm.sleepTimeInMs < 0) || !Number.isFinite(parm.sleepTimeInMs)) {\n        const result = { error: \"Parameter 'sleepTimeInMs' must be finite, positive integer value.\" }\n        console.error(result.error)\n        return result\n    }\n\n    console.log(\"Specified sleep time is \" + parm.sleepTimeInMs + \" ms.\")\n\n    return new Promise(function(resolve, reject) {\n        const timeBeforeSleep = new Date()\n        setTimeout(function () {\n            const actualSleepTimeInMs = new Date() - timeBeforeSleep\n            const result = { msg: \"Terminated successfully after around \" + actualSleepTimeInMs + \" ms.\" }\n            console.log(result.msg)\n            resolve(result)\n        }, parm.sleepTimeInMs)\n    })\n}\n"
  },
  {
    "path": "tests/dat/actions/sleep.py",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# Python based OpenWhisk action that sleeps for the specified number\n# of milliseconds before returning.\n# The function actually sleeps slightly longer than requested.\n#\n# @param parm Object with Number property sleepTimeInMs\n# @returns Object with String property msg describing how long the function slept\n#\nimport time\n\n\ndef main(parm):\n    sleepTimeInMs = parm.get(\"sleepTimeInMs\", 1)\n    print(\"Specified sleep time is {} ms.\".format(sleepTimeInMs))\n\n    result = {\"msg\": \"Terminated successfully after around {} ms.\".format(sleepTimeInMs)}\n\n    time.sleep(sleepTimeInMs / 1000.0)\n\n    print(result['msg'])\n    return result\n"
  },
  {
    "path": "tests/dat/actions/sort-array.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Sort a set of lines.\n * @param lines An array of strings to sort.\n */\nfunction main(msg) {\n    var lines = msg || [];\n    console.log('sort before: ' + lines);\n    lines.sort();\n    console.log('sort after: ' + lines);\n    return lines;\n}\n"
  },
  {
    "path": "tests/dat/actions/sort.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Sort a set of lines.\n * @param lines An array of strings to sort.\n */\nfunction main(msg) {\n    var lines = msg.lines || [];\n    //console.log('sort got ' + lines.length + ' lines');\n    console.log('sort input msg: ' + JSON.stringify(msg));\n    console.log('sort before: ' + lines);\n    lines.sort();\n    console.log('sort after: ' + lines);\n    return {lines: lines, length: lines.length};\n}\n"
  },
  {
    "path": "tests/dat/actions/split-array.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Splits a string into an array of strings\n * Return lines in an array.\n * @param payload A string.\n * @param separator The character, or the regular expression, to use for splitting the string\n */\nfunction main(msg) {\n    var separator = msg.separator || /\\r?\\n/;\n    var payload = msg.payload.toString();\n    var lines = payload.split(separator);\n    // return array as next action's input\n    return lines;\n}\n\n"
  },
  {
    "path": "tests/dat/actions/split.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Splits a string into an array of strings\n * Return lines in an array.\n * @param payload A string.\n * @param separator The character, or the regular expression, to use for splitting the string\n */\nfunction main(msg) {\n    var separator = msg.separator || /\\r?\\n/;\n    var payload = msg.payload.toString();\n    var lines = payload.split(separator);\n    var retn = {lines: lines, payload: msg.payload};\n    console.log('split: returning ' + JSON.stringify(retn));\n    return retn;\n}\n"
  },
  {
    "path": "tests/dat/actions/src/java/sleep/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'java'\n}\n\nversion = '1.0'\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.code.gson:gson:2.6.2\"\n}\n"
  },
  {
    "path": "tests/dat/actions/src/java/sleep/settings.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nrootProject.name = 'sleep'\n"
  },
  {
    "path": "tests/dat/actions/src/java/sleep/src/main/java/Sleep.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Build instructions:\n * - Assumption: the dependency GSON is in the local dicrectory, e.g. \"gson-2.8.2.jar\"\n * - Compile with \"javac -cp gson-2.8.2.jar Sleep.java\"\n * - Create .jar archive with \"jar cvf sleep.jar Sleep.class\"\n */\n\n/**\n * Java based OpenWhisk action that sleeps for the specified number\n * of milliseconds before returning.\n * The function actually sleeps slightly longer than requested.\n *\n * @param parm JSON object with Number property sleepTimeInMs\n * @returns JSON object with String property msg describing how long the function slept\n */\n\nimport com.google.gson.JsonObject;\npublic class Sleep {\n    public static JsonObject main(JsonObject parm) throws InterruptedException {\n        int sleepTimeInMs = 1;\n        if (parm.has(\"sleepTimeInMs\")) {\n            sleepTimeInMs = parm.getAsJsonPrimitive(\"sleepTimeInMs\").getAsInt();\n        }\n        System.out.println(\"Specified sleep time is \" + sleepTimeInMs + \" ms.\");\n\n        final String responseText = \"Terminated successfully after around \" + sleepTimeInMs + \" ms.\";\n        final JsonObject response = new JsonObject();\n        response.addProperty(\"msg\", responseText);\n\n        Thread.sleep(sleepTimeInMs);\n\n        System.out.println(responseText);\n        return response;\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/stdenv.py",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n\"\"\"Unify action container environments.\"\"\"\nimport os\n\n\ndef main(dict):\n    return {\"auth\": os.environ['__OW_API_KEY'],\n            \"edge\": os.environ['__OW_API_HOST']}\n"
  },
  {
    "path": "tests/dat/actions/step.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Increment action.\n */\nfunction main({ n }) {\n    if (typeof n === 'undefined') return { error: 'missing parameter'}\n    return { n: n + 1 }\n}\n"
  },
  {
    "path": "tests/dat/actions/textBody.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main() {\n  return {\n    headers: {\n      'Content-Type': 'text/html'\n    },\n    body: 'plain text'\n  };\n}\n"
  },
  {
    "path": "tests/dat/actions/trigger.swift",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunc main(args: [String:Any]) -> [String:Any] {\n  if let triggerName = args[\"triggerName\"] as? String {\n    print(\"Trigger Name: \\(triggerName)\")\n    return Whisk.trigger(eventNamed: triggerName, withParameters: [:])\n  } else {\n    return [\"error\": \"You must specify a triggerName parameter!\"]\n  }\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/go-1.13.txt",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport \"fmt\"\n\nfunc Main(args map[string]interface{}) map[string]interface{} {\n\tdelimiter := args[\"delimiter\"].(string)\n\tstr := delimiter + \" ☃ \" + delimiter\n    fmt.Println(str)\n\tres := make(map[string]interface{})\n\tres[\"winter\"] = str\n\treturn res\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/go-1.15.txt",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage main\n\nimport \"fmt\"\n\nfunc Main(args map[string]interface{}) map[string]interface{} {\n\tdelimiter := args[\"delimiter\"].(string)\n\tstr := delimiter + \" ☃ \" + delimiter\n    fmt.Println(str)\n\tres := make(map[string]interface{})\n\tres[\"winter\"] = str\n\treturn res\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/nodejs-10.txt",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(args) {\n    var str = args.delimiter + \" ☃ \" + args.delimiter;\n    console.log(str);\n    return { \"winter\": str };\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/nodejs-12.txt",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(args) {\n    var str = args.delimiter + \" ☃ \" + args.delimiter;\n    console.log(str);\n    return { \"winter\": str };\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/nodejs-14.txt",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(args) {\n    var str = args.delimiter + \" ☃ \" + args.delimiter;\n    console.log(str);\n    return { \"winter\": str };\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/php-7.3.txt",
    "content": "<?php\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(array $args) : array {\n    $str = $args['delimiter'] . \" ☃ \" . $args['delimiter'];\n    echo $str . \"\\n\";\n    return  [\"winter\" => $str];\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/php-7.4.txt",
    "content": "<?php\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(array $args) : array {\n    $str = $args['delimiter'] . \" ☃ \" . $args['delimiter'];\n    echo $str . \"\\n\";\n    return  [\"winter\" => $str];\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/python-3.txt",
    "content": "\"\"\"Python 3 Unicode test.\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\ndef main(args):\n    sep = args['delimiter']\n    str = sep + \" ☃ \" + sep\n    print(str)\n    return {\"winter\": str}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/python.txt",
    "content": "\"\"\"Python 3 Unicode test.\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\ndef main(args):\n    sep = args['delimiter']\n    str = sep + \" ☃ \" + sep\n    print(str)\n    return {\"winter\": str}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/ruby-2.5.txt",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ndef main(args)\n  str = args[\"delimiter\"] + \" ☃ \" + args[\"delimiter\"]\n  puts str\n  { \"winter\" => str }\nend\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/rust-1.34.txt",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nextern crate serde_json;\nuse serde_derive::{Deserialize, Serialize};\nuse serde_json::{Error, Value};\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\nstruct Input {\n       delimiter: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\nstruct Output {\n       winter: String,\n}\n\npub fn main(args: Value) -> Result<Value, Error> {\n    let input: Input = serde_json::from_value(args)?;\n    let msg = format!(\"{} {} {}\", input.delimiter,'☃',input.delimiter);\n    println!(\"{}\", msg);\n    let output = Output {\n        winter: msg,\n    };\n    serde_json::to_value(output)\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/src/dotnet2.2/Apache.OpenWhisk.UnicodeTests.Dotnet/Apache.OpenWhisk.UnicodeTests.Dotnet.csproj",
    "content": "<!--\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n-->\n<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <TargetFramework>netcoreapp2.2</TargetFramework>\n    </PropertyGroup>\n\n    <ItemGroup>\n      <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.1\" />\n    </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/src/dotnet2.2/Apache.OpenWhisk.UnicodeTests.Dotnet/Unicode.cs",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nusing System;\nusing Newtonsoft.Json.Linq;\n\nnamespace Apache.OpenWhisk.UnicodeTests.Dotnet\n{\n    public class Unicode\n    {\n        public JObject Main(JObject args)\n        {\n            string delimiter = args[\"delimiter\"].ToString();\n            JObject message = new JObject();\n            string output = $\"{delimiter} ☃ {delimiter}\";\n            message.Add(\"winter\", new JValue(output));\n            Console.WriteLine(output);\n            return (message);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/src/dotnet2.2/openwhisk-unicodetests-dotnet.sln",
    "content": "﻿Microsoft Visual Studio Solution File, Format Version 12.00\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Apache.OpenWhisk.UnicodeTests.Dotnet\", \"Apache.OpenWhisk.UnicodeTests.Dotnet\\Apache.OpenWhisk.UnicodeTests.Dotnet.csproj\", \"{B905FD2E-6975-411E-B139-36747747F524}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{B905FD2E-6975-411E-B139-36747747F524}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B905FD2E-6975-411E-B139-36747747F524}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B905FD2E-6975-411E-B139-36747747F524}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B905FD2E-6975-411E-B139-36747747F524}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/src/java/unicode/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'java'\n}\n\nversion = '1.0'\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.code.gson:gson:2.6.2\"\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/src/java/unicode/settings.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nrootProject.name = 'unicode'\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/src/java/unicode/src/main/java/Unicode.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport com.google.gson.JsonObject;\n\npublic class Unicode {\n    public static JsonObject main(JsonObject args) throws InterruptedException {\n        String delimiter = args.getAsJsonPrimitive(\"delimiter\").getAsString();\n        JsonObject response = new JsonObject();\n        String str = delimiter + \" ☃ \" + delimiter;\n        System.out.println(str);\n        response.addProperty(\"winter\", str);\n        return response;\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/swift-4.2.txt",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunc main(args: Any) -> Any {\n    let dict = args as! [String:Any]\n    if let str = dict[\"delimiter\"] as? String {\n        let msg = \"\\(str) ☃ \\(str)\"\n        print(msg)\n        return [ \"winter\" : msg ]\n    } else {\n        return [ \"error\" : \"no delimiter\" ]\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/swift-5.1.txt",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunc main(args: Any) -> Any {\n    let dict = args as! [String:Any]\n    if let str = dict[\"delimiter\"] as? String {\n        let msg = \"\\(str) ☃ \\(str)\"\n        print(msg)\n        return [ \"winter\" : msg ]\n    } else {\n        return [ \"error\" : \"no delimiter\" ]\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/unicode.tests/swift-5.3.txt",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunc main(args: Any) -> Any {\n    let dict = args as! [String:Any]\n    if let str = dict[\"delimiter\"] as? String {\n        let msg = \"\\(str) ☃ \\(str)\"\n        print(msg)\n        return [ \"winter\" : msg ]\n    } else {\n        return [ \"error\" : \"no delimiter\" ]\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/validInput1.json",
    "content": "{\n  \"a key\": \"a value\",\n  \"a bool\": true,\n  \"objKey\": {\"b\": \"c\"},\n  \"objKey2\": {\"another object\": {\"some string\": \"1111\"}},\n  \"objKey3\": {\"json object\": {\"some int\": 1111}},\n  \"a number arr\": [1,2,3],\n  \"a string arr\": [\"1\", \"2\", \"3\"],\n  \"a bool arr\": [true, false, true],\n  \"strThatLooksLikeJSON\": \"{\\\"someKey\\\": \\\"someValue\\\"}\"\n}\n"
  },
  {
    "path": "tests/dat/actions/validInput2.json",
    "content": "{\n  \"payload\": \"test\"\n}\n"
  },
  {
    "path": "tests/dat/actions/wc.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n *  word count utility\n */\nfunction main(params) {\n    var str = params.payload.toString();\n    var words = str.split(\" \");\n    var count = words.length;\n    console.log(\"The message '\"+str+\"' has\", count, 'words');\n    return { count: count };\n}\n"
  },
  {
    "path": "tests/dat/actions/wcbin.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Return word count as a binary number. This demonstrates the use of a blocking\n * invoke.\n */\nvar openwhisk = require('openwhisk')\n\nfunction main(params) {\n    var wsk = openwhisk({ignore_certs: true})\n\n    var str = params.payload;\n    console.log(\"The payload is '\" + str + \"'\");\n\n    return wsk.actions.invoke({\n        actionName: 'wc',\n        params: {\n            payload: str\n        },\n        blocking: true\n    }).then(activation => {\n        var wordsInDecimal = activation.response.result.count;\n        var wordsInBinary = wordsInDecimal.toString(2) + ' (base 2)';\n        return {\n            binaryCount: wordsInBinary\n        };\n    });\n}\n"
  },
  {
    "path": "tests/dat/actions/word_count.json",
    "content": "{\n    \"name\": \"word_count\",\n    \"version\": \"0.0.1\",\n    \"publish\": false,\n    \"exec\": {\n        \"kind\": \"nodejs:20\",\n        \"code\": \"/**\\n *  word count utility\\n */\\nfunction main(params) {\\n    var str = params.payload.toString();\\n    var words = str.split(\\\" \\\");\\n    var count = words.length;\\n    console.log(\\\"The message '\\\"+str+\\\"' has\\\", count, 'words');\\n    return { count: count };\\n}\\n\"\n    },\n    \"annotations\": [],\n    \"parameters\": [],\n    \"limits\": {\n        \"timeout\": 60000,\n        \"memory\": 256,\n        \"logs\": 10\n    }\n}\n"
  },
  {
    "path": "tests/dat/actions/zippedaction/index.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction entryPoint(args) {\n    var pq = require(\"prog-quote\")();\n\n    return pq.next().value;\n}\n\nexports.main = entryPoint;\n"
  },
  {
    "path": "tests/dat/actions/zippedaction/package.json",
    "content": "{\n  \"name\": \"test-action\",\n  \"version\": \"1.0.0\",\n  \"description\": \"An action written as an npm package.\",\n  \"main\": \"index.js\",\n  \"author\": \"OpenWhisk\",\n  \"license\": \"Apache 2.0\",\n  \"dependencies\": {\n    \"prog-quote\": \"2.0.0\"\n  }\n}\n"
  },
  {
    "path": "tests/dat/apigw/apigw_path_param_support_test_invalidActionType.json",
    "content": "{\n  \"swagger\": \"2.0\",\n  \"basePath\": \"/guest/v1\",\n  \"info\": {\n    \"title\": \"/guest/v1\",\n    \"version\": \"1.0.0\"\n  },\n  \"paths\": {\n    \"/api2/greeting2/{name}\": {\n      \"get\": {\n        \"operationId\": \"getApi2Greeting2Name\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"type\": \"string\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"Default response\"\n          }\n        },\n        \"x-openwhisk\": {\n          \"action\": \"testapi\",\n          \"namespace\": \"guest\",\n          \"package\": \"\",\n          \"url\": \"https://127.0.0.1/api/v1/web/guest/default/testapi.json\"\n        }\n      }\n    }\n  },\n  \"x-ibm-configuration\": {\n    \"assembly\": {\n      \"execute\": [\n        {\n          \"operation-switch\": {\n            \"case\": [\n              {\n                \"execute\": [\n                  {\n                    \"invoke\": {\n                      \"target-url\": \"https://127.0.0.1/api/v1/web/guest/default/testapi.json$(request.path)\",\n                      \"verb\": \"keep\"\n                    }\n                  }\n                ],\n                \"operations\": [\n                  \"getApi2Greeting2Name\"\n                ]\n              }\n            ]\n          }\n        }\n      ]\n    },\n    \"cors\": {\n      \"enabled\": true\n    }\n  }\n}"
  },
  {
    "path": "tests/dat/apigw/apigw_path_param_support_test_invalidParamName1.json",
    "content": "{\n  \"swagger\": \"2.0\",\n  \"basePath\": \"/guest/v1\",\n  \"info\": {\n    \"title\": \"/guest/v1\",\n    \"version\": \"1.0.0\"\n  },\n  \"paths\": {\n    \"/api2/greeting2/{name}\": {\n      \"get\": {\n        \"operationId\": \"getApi2Greeting2Name\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name1\",\n            \"type\": \"string\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"Default response\"\n          }\n        },\n        \"x-openwhisk\": {\n          \"action\": \"testapi\",\n          \"namespace\": \"guest\",\n          \"package\": \"\",\n          \"url\": \"https://127.0.0.1/api/v1/web/guest/default/testapi.http\"\n        }\n      }\n    }\n  },\n  \"x-ibm-configuration\": {\n    \"assembly\": {\n      \"execute\": [\n        {\n          \"operation-switch\": {\n            \"case\": [\n              {\n                \"execute\": [\n                  {\n                    \"invoke\": {\n                      \"target-url\": \"https://127.0.0.1/api/v1/web/guest/default/testapi.http$(request.path)\",\n                      \"verb\": \"keep\"\n                    }\n                  }\n                ],\n                \"operations\": [\n                  \"getApi2Greeting2Name\"\n                ]\n              }\n            ]\n          }\n        }\n      ]\n    },\n    \"cors\": {\n      \"enabled\": true\n    }\n  }\n}"
  },
  {
    "path": "tests/dat/apigw/apigw_path_param_support_test_invalidParamName2.json",
    "content": "{\n  \"swagger\": \"2.0\",\n  \"basePath\": \"/guest/v1\",\n  \"info\": {\n    \"title\": \"/guest/v1\",\n    \"version\": \"1.0.0\"\n  },\n  \"paths\": {\n    \"/api2/greeting2/{name}/{id}\": {\n      \"get\": {\n        \"operationId\": \"getApi2Greeting2Name\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"type\": \"string\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"Default response\"\n          }\n        },\n        \"x-openwhisk\": {\n          \"action\": \"testapi\",\n          \"namespace\": \"guest\",\n          \"package\": \"\",\n          \"url\": \"https://127.0.0.1/api/v1/web/guest/default/testapi.http\"\n        }\n      }\n    }\n  },\n  \"x-ibm-configuration\": {\n    \"assembly\": {\n      \"execute\": [\n        {\n          \"operation-switch\": {\n            \"case\": [\n              {\n                \"execute\": [\n                  {\n                    \"invoke\": {\n                      \"target-url\": \"https://127.0.0.1/api/v1/web/guest/default/testapi.http$(request.path)\",\n                      \"verb\": \"keep\"\n                    }\n                  }\n                ],\n                \"operations\": [\n                  \"getApi2Greeting2Name\"\n                ]\n              }\n            ]\n          }\n        }\n      ]\n    },\n    \"cors\": {\n      \"enabled\": true\n    }\n  }\n}"
  },
  {
    "path": "tests/dat/apigw/apigw_path_param_support_test_invalidTargetUrl.json",
    "content": "{\n  \"swagger\": \"2.0\",\n  \"basePath\": \"/guest/v1\",\n  \"info\": {\n    \"title\": \"/guest/v1\",\n    \"version\": \"1.0.0\"\n  },\n  \"paths\": {\n    \"/api2/greeting2/{name}\": {\n      \"get\": {\n        \"operationId\": \"getApi2Greeting2Name\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"type\": \"string\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"Default response\"\n          }\n        },\n        \"x-openwhisk\": {\n          \"action\": \"testapi\",\n          \"namespace\": \"guest\",\n          \"package\": \"\",\n          \"url\": \"https://127.0.0.1/api/v1/web/guest/default/testapi.http\"\n        }\n      }\n    }\n  },\n  \"x-ibm-configuration\": {\n    \"assembly\": {\n      \"execute\": [\n        {\n          \"operation-switch\": {\n            \"case\": [\n              {\n                \"execute\": [\n                  {\n                    \"invoke\": {\n                      \"target-url\": \"https://127.0.0.1/api/v1/web/guest/default/testapi.http\",\n                      \"verb\": \"keep\"\n                    }\n                  }\n                ],\n                \"operations\": [\n                  \"getApi2Greeting2Name\"\n                ]\n              }\n            ]\n          }\n        }\n      ]\n    },\n    \"cors\": {\n      \"enabled\": true\n    }\n  }\n}"
  },
  {
    "path": "tests/dat/apigw/apigw_path_param_support_test_withPathParameters1.json",
    "content": "{\n  \"swagger\": \"2.0\",\n  \"basePath\": \"/guest/v1\",\n  \"info\": {\n    \"title\": \"/guest/v1\",\n    \"version\": \"1.0.0\"\n  },\n  \"paths\": {\n    \"/api2/greeting2/{name}\": {\n      \"get\": {\n        \"operationId\": \"getApi2Greeting2Name\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"type\": \"string\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"Default response\"\n          }\n        },\n        \"x-openwhisk\": {\n          \"action\": \"cli_apigwtest_path_param_swagger_action\",\n          \"namespace\": \"%NAMESPACE%\",\n          \"package\": \"\",\n          \"url\": \"%HOST%/api/v1/web/%NAMESPACE%/default/cli_apigwtest_path_param_swagger_action.http\"\n        }\n      }\n    }\n  },\n  \"x-ibm-configuration\": {\n    \"assembly\": {\n      \"execute\": [\n        {\n          \"operation-switch\": {\n            \"case\": [\n              {\n                \"execute\": [\n                  {\n                    \"invoke\": {\n                      \"target-url\": \"%HOST%/api/v1/web/%NAMESPACE%/default/cli_apigwtest_path_param_swagger_action.http$(request.path)\",\n                      \"verb\": \"keep\"\n                    }\n                  }\n                ],\n                \"operations\": [\n                  \"getApi2Greeting2Name\"\n                ]\n              }\n            ]\n          }\n        }\n      ]\n    },\n    \"cors\": {\n      \"enabled\": true\n    }\n  }\n}"
  },
  {
    "path": "tests/dat/apigw/apigw_path_param_support_test_withPathParameters2.json",
    "content": "{\n  \"swagger\": \"2.0\",\n  \"basePath\": \"/guest/v1\",\n  \"info\": {\n    \"title\": \"/guest/v1\",\n    \"version\": \"1.0.0\"\n  },\n  \"paths\": {\n    \"/api2/greeting2/{name}/{something}\": {\n      \"get\": {\n        \"operationId\": \"getApi2Greeting2Name\",\n        \"parameters\": [\n          {\n            \"in\": \"path\",\n            \"name\": \"name\",\n            \"type\": \"string\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"Default response\"\n          }\n        },\n        \"x-openwhisk\": {\n          \"action\": \"cli_apigwtest_path_param_swagger_action\",\n          \"namespace\": \"guest\",\n          \"package\": \"\",\n          \"url\": \"%HOST%/api/v1/web/guest/default/cli_apigwtest_path_param_swagger_action.http\"\n        }\n      },\n      \"parameters\": [\n        {\n          \"in\": \"path\",\n          \"name\": \"something\",\n          \"type\": \"string\",\n          \"required\": true\n        }\n      ]\n    }\n  },\n  \"x-ibm-configuration\": {\n    \"assembly\": {\n      \"execute\": [\n        {\n          \"operation-switch\": {\n            \"case\": [\n              {\n                \"execute\": [\n                  {\n                    \"invoke\": {\n                      \"target-url\": \"%HOST%/api/v1/web/guest/default/cli_apigwtest_path_param_swagger_action.http$(request.path)\",\n                      \"verb\": \"keep\"\n                    }\n                  }\n                ],\n                \"operations\": [\n                  \"getApi2Greeting2Name\"\n                ]\n              }\n            ]\n          }\n        }\n      ]\n    },\n    \"cors\": {\n      \"enabled\": true\n    }\n  }\n}"
  },
  {
    "path": "tests/dat/apigw/endpoints.without.action.swagger.json",
    "content": "{\n    \"swagger\": \"2.0\",\n    \"basePath\": \"/NoActions\",\n    \"info\": {\n        \"title\": \"A descriptive name\",\n        \"version\": \"1.0\"\n    },\n    \"paths\": {\n        \"/\": {\n            \"delete\": {\n                \"operationId\": \"\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"A successful invocation response\"\n                    }\n                }\n            },\n            \"get\": {\n                \"operationId\": \"\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"A successful invocation response\"\n                    }\n                }\n            },\n            \"head\": {\n                \"operationId\": \"\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"A successful invocation response\"\n                    }\n                }\n            },\n            \"options\": {\n                \"operationId\": \"\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"A successful invocation response\"\n                    }\n                }\n            },\n            \"patch\": {\n                \"operationId\": \"\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"A successful invocation response\"\n                    }\n                }\n            },\n            \"post\": {\n                \"operationId\": \"\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"A successful invocation response\"\n                    }\n                }\n            },\n            \"put\": {\n                \"operationId\": \"\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"A successful invocation response\"\n                    }\n                }\n            }\n        }\n    },\n    \"x-ibm-configuration\": {\n        \"assembly\": {\n            \"execute\": []\n        },\n        \"cors\": {\n            \"enabled\": true\n        }\n    }\n}\n"
  },
  {
    "path": "tests/dat/apigw/local.api.bad.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nsome bad yaml in\nthese []]]\nlines:\n\nbasePath: /bp\ninfo:\n  title: /bp\n  version: 1.0.0\npaths:\n  /rp:\n    get:\n      operationId: get_/rp\n      responses:\n        default:\n          description: Default response\n      x-openwhisk:\n        action: webhttpecho\n        namespace: guest\n        package: \"\"\n        url: https://127.0.0.1/api/v1/web/guest/default/webhttpecho.http\nswagger: \"2.0\"\nx-ibm-configuration:\n  assembly:\n    execute:\n    - operation-switch:\n        case:\n        - execute:\n          - invoke:\n              target-url: https://127.0.0.1/api/v1/web/guest/default/webhttpecho.http\n              verb: keep\n          operations:\n          - get_/rp\n  cors:\n    enabled: true\n"
  },
  {
    "path": "tests/dat/apigw/local.api.yaml",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nbasePath: /bp\ninfo:\n  title: /bp\n  version: 1.0.0\npaths:\n  /rp:\n    get:\n      operationId: get_/rp\n      responses:\n        default:\n          description: Default response\n      x-openwhisk:\n        action: webhttpecho\n        namespace: guest\n        package: \"\"\n        url: https://127.0.0.1/api/v1/web/guest/default/webhttpecho.http\nswagger: \"2.0\"\nx-ibm-configuration:\n  assembly:\n    execute:\n    - operation-switch:\n        case:\n        - execute:\n          - invoke:\n              target-url: https://127.0.0.1/api/v1/web/guest/default/webhttpecho.http\n              verb: keep\n          operations:\n          - get_/rp\n  cors:\n    enabled: true\n"
  },
  {
    "path": "tests/dat/apigw/testswaggerdoc1",
    "content": "{\n    \"swagger\": \"2.0\",\n    \"basePath\": \"/CLI_APIGWTEST7_bp\",\n    \"info\": {\n        \"title\": \"CLI_APIGWTEST7 API Name\",\n        \"version\": \"1.0.0\"\n    },\n    \"paths\": {\n        \"/path\": {\n            \"get\": {\n                \"operationId\": \"get_/path\",\n                \"responses\": {\n                    \"default\": {\n                        \"description\": \"Default response\"\n                    }\n                },\n                \"x-openwhisk\": {\n                    \"action\": \"CLI_APIGWTEST7_action\",\n                    \"namespace\": \"whisk.system\",\n                    \"package\": \"\",\n                    \"url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/CLI_APIGWTEST7_action.http\"\n                }\n            }\n        },\n        \"/pathSecure1\": {\n            \"get\": {\n                \"operationId\": \"get_/pathSecure1\",\n                \"responses\": {\n                    \"default\": {\n                        \"description\": \"Default response\"\n                    }\n                },\n                \"x-openwhisk\": {\n                    \"action\": \"CLI_APIGWTEST7_action\",\n                    \"namespace\": \"whisk.system\",\n                    \"package\": \"\",\n                    \"url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/CLI_APIGWTEST7_action.http\"\n                }\n            }\n        },\n        \"/pathSecure2\": {\n            \"get\": {\n                \"operationId\": \"get_/pathSecure2\",\n                \"responses\": {\n                    \"default\": {\n                        \"description\": \"Default response\"\n                    }\n                },\n                \"x-openwhisk\": {\n                    \"action\": \"CLI_APIGWTEST7_action\",\n                    \"namespace\": \"whisk.system\",\n                    \"package\": \"\",\n                    \"url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/CLI_APIGWTEST7_action.http\"\n                }\n            }\n        }\n    },\n    \"x-ibm-configuration\": {\n        \"assembly\": {\n            \"execute\": [\n                {\n                    \"set-variable\": {\n                        \"actions\": [\n                            {\n                                \"set\": \"message.headers.Authorization\",\n                                \"value\": \"Basic Nzg5YzQ2YjEtNzFmNi00ZWQ1LThjNTQtODE2YWE0ZjhjNTAyOmFiY3pPM3haQ0xyTU42djJCS0sxZFhZRnBYbFBrY2NPRnFtMTJDZEFzTWdSVTRWck5aOWx5R1ZDR3VNREdJd1A=\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"operation-switch\": {\n                        \"case\": [\n                            {\n                                \"execute\": [\n                                    {\n                                        \"invoke\": {\n                                            \"target-url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/CLI_APIGWTEST7_action.http\",\n                                            \"verb\": \"keep\"\n                                        }\n                                    }\n                                ],\n                                \"operations\": [\n                                    \"get_/path\"\n                                ]\n                            },\n                            {\n                                \"execute\": [\n                                    {\n                                        \"invoke\": {\n                                            \"target-url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/CLI_APIGWTEST7_action.http\",\n                                            \"verb\": \"keep\"\n                                        }\n                                    },\n                                    {\n                                        \"set-variable\": {\n                                            \"actions\": [\n                                                {\n                                                    \"set\": \"message.headers.X-Require-Whisk-Auth\",\n                                                    \"value\": \"my-secret\"\n                                                }\n                                            ]\n                                        }\n                                    }\n                                ],\n                                \"operations\": [\n                                    \"get_/pathSecure1\"\n                                ]\n                            },\n                            {\n                                \"execute\": [\n                                    {\n                                        \"set-variable\": {\n                                            \"actions\": [\n                                                {\n                                                    \"set\": \"message.headers.X-Require-Whisk-Auth\",\n                                                    \"value\": \"my-secret\"\n                                                }\n                                            ]\n                                        }\n                                    },\n                                    {\n                                        \"invoke\": {\n                                            \"target-url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/CLI_APIGWTEST7_action.http\",\n                                            \"verb\": \"keep\"\n                                        }\n                                    }\n                                ],\n                                \"operations\": [\n                                    \"get_/pathSecure2\"\n                                ]\n                            }\n                        ]\n                    }\n                }\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "tests/dat/apigw/testswaggerdoc2",
    "content": "{\n    \"swagger\": \"2.0\",\n    \"basePath\": \"/test1/v1\",\n    \"info\": {\n        \"title\": \"CLI_APIGWTEST13 API Name\",\n        \"version\": \"1.0.0\"\n    },\n    \"paths\": {\n        \"/whisk_system/utils/echo\": {\n            \"get\": {\n                \"operationId\": \"get_/whisk_system/utils/echo\",\n                \"responses\": {\n                    \"default\": {\n                        \"description\": \"Default response\"\n                    }\n                },\n                \"x-openwhisk\": {\n                    \"action\": \"test1a\",\n                    \"namespace\": \"whisk.system\",\n                    \"package\": \"\",\n                    \"url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/test1a.http\"\n                }\n            },\n            \"post\": {\n                \"operationId\": \"post_/whisk_system/utils/echo\",\n                \"responses\": {\n                    \"default\": {\n                        \"description\": \"Default response\"\n                    }\n                },\n                \"x-openwhisk\": {\n                    \"action\": \"test1a\",\n                    \"namespace\": \"whisk.system\",\n                    \"package\": \"\",\n                    \"url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/test1a.http\"\n                }\n            }\n        },\n        \"/whisk_system/utils/split\": {\n            \"post\": {\n                \"operationId\": \"post_/whisk_system/utils/split\",\n                \"responses\": {\n                    \"default\": {\n                        \"description\": \"Default response\"\n                    }\n                },\n                \"x-openwhisk\": {\n                    \"action\": \"test1a\",\n                    \"namespace\": \"whisk.system\",\n                    \"package\": \"\",\n                    \"url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/test1a.http\"\n                }\n            }\n        }\n    },\n    \"x-ibm-configuration\": {\n        \"assembly\": {\n            \"execute\": [\n                {\n                    \"set-variable\": {\n                        \"actions\": [\n                            {\n                                \"set\": \"message.headers.Authorization\",\n                                \"value\": \"Basic Nzg5YzQ2YjEtNzFmNi00ZWQ1LThjNTQtODE2YWE0ZjhjNTAyOmFiY3pPM3haQ0xyTU42djJCS0sxZFhZRnBYbFBrY2NPRnFtMTJDZEFzTWdSVTRWck5aOWx5R1ZDR3VNREdJd1A=\"\n                            }\n                        ]\n                    }\n                },\n                {\n                    \"operation-switch\": {\n                        \"case\": [\n                            {\n                                \"operations\": [\n                                    \"get_/whisk_system/utils/echo\"\n                                ],\n                                \"execute\": [\n                                    {\n                                        \"invoke\": {\n                                            \"target-url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/test1a.http\",\n                                            \"verb\": \"get\"\n                                        }\n                                    }\n                                ]\n                            },\n                            {\n                                \"operations\": [\n                                    \"post_/whisk_system/utils/echo\"\n                                ],\n                                \"execute\": [\n                                    {\n                                        \"invoke\": {\n                                            \"target-url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/test1a.http\",\n                                            \"verb\": \"post\"\n                                        }\n                                    }\n                                ]\n                            },\n                            {\n                                \"operations\": [\n                                    \"post_/whisk_system/utils/split\"\n                                ],\n                                \"execute\": [\n                                    {\n                                        \"invoke\": {\n                                            \"target-url\": \"https://172.17.0.1/api/v1/web/whisk.system/default/test1a.http\",\n                                            \"verb\": \"post\"\n                                        }\n                                    }\n                                ]\n                            }\n                        ]\n                    }\n                }\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "tests/dat/apigw/testswaggerdocinvalid",
    "content": "{\n    \"swagger\": \"2.0\",\n    \"info\": {\n        \"title\": \"/\",\n        \"version\": \"1.0.0\"\n    },\n    \"paths\": {\n        \"/test1\": {\n\n            }\n    }\n}"
  },
  {
    "path": "tests/performance/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# :electric_plug: Apache OpenWhisk - Performance Tests\nA few simple but efficient test suites for determining the maximum throughput and end-user latency of the Apache OpenWhisk system.\n\n## Workflow\n- A standard OpenWhisk system is deployed. (_Note that the API Gateway is currently left out for the tests._)\n- All limits are set to 999999, which in our current use case means \"No throttling at all\".\n- The deployment is using the docker setup proposed by the OpenWhisk development team: `overlay` driver and HTTP API enabled via a UNIX port.\n\nThe load is driven by the blazingly fast [`wrk`](https://github.com/wg/wrk).\n\n#### Travis Machine Setup\nThe [machine provided by Travis](https://docs.travis-ci.com/user/ci-environment/#Virtualization-environments) has ~2 CPU cores (likely shared through virtualization) and 7.5GB memory.\n\n## Suites\n\n### wrk\n\n#### Latency Test\nDetermines the end-to-end latency a user experience when doing a blocking invocation. The action used is a no-op so the numbers returned are the plain overhead of the OpenWhisk system. The requests are directly against the controller.\n\n- 1 HTTP request at a time (concurrency: 1)\n- You can specify how long this test will run. Default are 30s.\n- no-op action\n\n**Note:** The throughput number has a 100% correlation with the latency in this case. This test does not serve to determine the maximum throughput of the system.\n\n#### Throughput Test\nDetermines the maximum throughput a user can get out of the system while using a single action. The action used is a no-op, so the numbers are plain OpenWhisk overhead. Note that the throughput does not directly correlate to end-to-end latency here, as the system does more processing in the background as it shows to the user in a blocking invocation. The requests are directly against the controller.\n\n- 4 HTTP requests at a time (concurrency: 4) (using CPU cores * 2 to exploit some buffering)\n- 10.000 samples with a single user\n- no-op action\n\n#### Running tests against your own system is simple too!\nAll you have to do is use the corresponding script located in `/*_tests` folder, remembering that the parameters are defined inline.\n\n### gatling\n\n#### Simulations\n\nYou can specify two thresholds for the simulations.\nThe reason is, that Gatling is able to handle each assertion as a JUnit test.\nOn using CI/CD pipelines (e.g. Jenkins) you will be able to set a threshold on an amount of failed testcases to mark the build as stable, unstable and failed.\n\n##### ApiV1Simulation\n\nThis Simulation calls the `api/v1`.\nYou can specify the endpoint, the amount of connections against the backend and the duration of this burst.\n\nThe test is doing as many requests as possible for the given amount of time (`SECONDS`). Afterwards it compares if the test reached the intended throughput (`REQUESTS_PER_SEC`, `MIN_REQUESTS_PER_SEC`).\n\nAvailable environment variables:\n\n```\nOPENWHISK_HOST                (required)\nCONNECTIONS                   (required)\nSECONDS                       (default: 10)\nREQUESTS_PER_SEC              (required)\nMIN_REQUESTS_PER_SEC          (default: REQUESTS_PER_SEC)\nMAX_ERRORS_ALLOWED            (default: 0)\nMAX_ERRORS_ALLOWED_PERCENTAGE (default: 0)\n```\n\nYou can run the simulation with (in OPENWHISK_HOME)\n```\nOPENWHISK_HOST=\"openwhisk.mydomain.com\" CONNECTIONS=\"10\" REQUESTS_PER_SEC=\"50\" ./gradlew gatlingRun-org.apache.openwhisk.ApiV1Simulation\n```\n\n##### Latency Simulation\n\nThis simulation creates actions of the following four kinds: `nodejs:default`, `swift:default`, `java:default` and\n`python:default`.\nAfterwards the action is invoked once. This is the cold-start and will not be part of the thresholds.\nNext, the action will be invoked 100 times blocking and one after each other. Between each invoke is a pause of\n`PAUSE_BETWEEN_INVOKES` milliseconds. The last step is to delete the action.\n\nOnce one language is finished, the next kind will be taken. They are not running in parallel. There are never more than\n1 activations in the system, as we only want to measure latency of warm activations.\nAs all actions are invoked blocking and only one action is in the system, it doesn't matter how many controllers\nand invokers are deployed. If several controllers or invokers are deployed, all controllers send the activation\nalways to the same invoker.\n\nThe comparison of the thresholds is against the mean response times of the warm activations.\n\nAvailable environment variables:\n\n```\nOPENWHISK_HOST                (required)\nAPI_KEY                       (required, format: UUID:KEY)\nPAUSE_BETWEEN_INVOKES         (default: 0)\nMEAN_RESPONSE_TIME            (required)\nMAX_MEAN_RESPONSE_TIME        (default: MEAN_RESPONSE_TIME)\nEXCLUDED_KINDS                (default: \"\", format: \"python:default,java:default,swift:default\")\nMAX_ERRORS_ALLOWED            (default: 0)\nMAX_ERRORS_ALLOWED_PERCENTAGE (default: 0)\n```\n\nIt is possible to override the `MEAN_RESPONSE_TIME`, `MAX_MEAN_RESPONSE_TIME`, `MAX_ERRORS_ALLOWED` and `MAX_ERRORS_ALLOWED_PERCENTAGE`\nfor each kind by adding the kind as prefix in upper case, like `JAVA_MEAN_RESPONSE_TIME`.\n\n\nYou can run the simulation with (in OPENWHISK_HOME)\n```\nOPENWHISK_HOST=\"openwhisk.mydomain.com\" MEAN_RESPONSE_TIME=\"20\" API_KEY=\"UUID:KEY\" ./gradlew gatlingRun-org.apache.openwhisk.LatencySimulation\n```\n\n##### BlockingInvokeOneActionSimulation\n\nThis simulation executes the same action with the same user over and over again.\nThe aim of this test is, to test the throughput of the system, if all containers are always warm.\n\nThe action that is invoked, writes one log line and returns a little JSON.\n\nThe simulations creates the action in the beginning, invokes it as often as possible for 5 seconds, to warm all containers up and invokes it afterwards for the given amount of time.\nThe warmup-phase will not be part of the assertions.\n\nTo run the test, you can specify the amount of concurrent requests. Keep in mind, that the actions are invoked blocking and the system is limited to `AMOUNT_OF_INVOKERS * SLOTS_PER_INVOKER * NON_BLACKBOX_INVOKER_RATIO` concurrent actions/requests.\n\nThe test is doing as many requests as possible for the given amount of time (`SECONDS`). Afterwards it compares if the test reached the intended throughput (`REQUESTS_PER_SEC`, `MIN_REQUESTS_PER_SEC`).\n\nAvailable environment variables:\n```\nOPENWHISK_HOST                (required)\nAPI_KEY                       (required, format: UUID:KEY)\nCONNECTIONS                   (required)\nSECONDS                       (default: 10)\nREQUESTS_PER_SEC              (required)\nMIN_REQUESTS_PER_SEC          (default: REQUESTS_PER_SEC)\nMAX_ERRORS_ALLOWED            (default: 0)\nMAX_ERRORS_ALLOWED_PERCENTAGE (default: 0)\n```\n\nYou can run the simulation with\n```\nOPENWHISK_HOST=\"openwhisk.mydomain.com\" CONNECTIONS=\"10\" REQUESTS_PER_SEC=\"50\" API_KEY=\"UUID:KEY\" ./gradlew gatlingRun-org.apache.openwhisk.BlockingInvokeOneActionSimulation\n```\n\n##### ColdBlockingInvokeSimulation\n\nThis simulation makes as much cold invocations as possible. Therefore, you have to specify how many users should be used.\nThis amount of users is executing actions in parallel. I recommend using the same amount of users like your amount of node-js action slots in your invokers.\n\nThe users, that are used are loaded from the file `gatling_tests/src/gatling/resources/data/users.csv`. If you want to increase the number of parallel users, you have to specify at least this amount of valid users in that file.\n\nEach user creates n actions (default is 5). Afterwards all users are executing their actions in parallel. But each user is rotating it's action. That's how the cold starts are enforced.\n\nThe aim of the test is, to test the throughput of the system, if all containers are always cold.\n\nThe action that is invoked, writes one log line and returns a little JSON.\n\nThe test is doing as many requests as possible for the given amount of time (`SECONDS`). Afterwards it compares if the test reached the intended throughput (`REQUESTS_PER_SEC`, `MIN_REQUESTS_PER_SEC`).\n\nAvailable environment variables:\n```\nOPENWHISK_HOST                (required)\nUSERS                         (required)\nSECONDS                       (default: 10)\nREQUESTS_PER_SEC              (required)\nMIN_REQUESTS_PER_SEC          (default: REQUESTS_PER_SEC)\nMAX_ERRORS_ALLOWED            (default: 0)\nMAX_ERRORS_ALLOWED_PERCENTAGE (default: 0)\n```\n\nYou can run the simulation with\n```\nOPENWHISK_HOST=\"openwhisk.mydomain.com\" USERS=\"10\" REQUESTS_PER_SEC=\"50\" ./gradlew gatlingRun-org.apache.openwhisk.ColdBlockingInvokeSimulation\n```\n"
  },
  {
    "path": "tests/performance/gatling_tests/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'io.gatling.gradle' version '3.9.0'\n    id 'eclipse'\n    id 'scala'\n}\n\nsourceSets {\n    gatling {\n        scala.srcDirs = [\"src/gatling/scala/\"]\n        resources.srcDirs = [\"src/gatling/resources/\"]\n    }\n}\n\ndependencies {\n    gatling \"io.spray:spray-json_${gradle.scala.depVersion}:1.3.6\"\n    gatling \"commons-io:commons-io:2.11.0\"\n}\n\ntask buildArtifacts(type:Exec) {\n  commandLine './build.sh'\n}\n\ntasks.matching {it != buildArtifacts}.all {it.dependsOn buildArtifacts}\n"
  },
  {
    "path": "tests/performance/gatling_tests/build.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nGRADLEW_PATH=${OPENWHISK_HOME:-../../../../../../../../..}\n\nif [ -f \".built\" ]; then\n  echo \"Test zip artifacts already built, skipping\"\n  exit 0\nfi\n\n# Let gradle set the source and target versions to let it sort out if it can produce\n# byte code that is compatible with java8.\n(cd src/gatling/resources/data/src/java && \"$GRADLEW_PATH/gradlew\" build && cp build/libs/gatling-1.0.jar ../../javaAction.jar)\ntouch .built\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/resources/conf/logback.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n    <appender name=\"CONSOLE\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <immediateFlush>false</immediateFlush>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.FileAppender\">\n        <immediateFlush>false</immediateFlush>\n        <file>build/gatling.log</file>\n        <append>false</append>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n    <!-- This will log the details of the failed requests -->\n    <logger name=\"io.gatling.http.ahc\" level=\"DEBUG\" additivity=\"false\" >\n        <appender-ref ref=\"FILE\" />\n    </logger>\n\n    <root level=\"WARN\">\n        <appender-ref ref=\"CONSOLE\"/>\n        <appender-ref ref=\"FILE\" />\n    </root>\n</configuration>\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/resources/data/nodeJSAction.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(params) {\n    var greeting = \"Hello\" + (params.text || \"stranger\") + \"!\";\n    console.log(greeting);\n    return { payload: greeting };\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/resources/data/nodeJSAsyncAction.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main(params) {\n    var greeting = \"Hello\" + (params.text || \"stranger\") + \"!\";\n    console.log(greeting);\n    return new Promise(function (resolve, reject) {\n        setTimeout(function () {\n            resolve({ payload: greeting });\n        }, 175);\n    })\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/resources/data/pythonAction.py",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\ndef main(dict):\n    if 'text' in dict:\n        text = dict['text']\n    else:\n        text = \"stranger\"\n    greeting = \"Hello \" + text + \"!\"\n    print(greeting)\n    return {\"payload\": greeting}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/resources/data/src/java/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'java'\n}\n\ncompileJava   {\n  sourceCompatibility = '1.8'\n  targetCompatibility = '1.8'\n}\n\nversion = '1.0'\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.code.gson:gson:2.6.2\"\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/resources/data/src/java/settings.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nrootProject.name = 'gatling'\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/resources/data/src/java/src/main/java/JavaAction.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Build the jar with the following commands:\n//\n// javac -cp gson-2.8.2.jar JavaAction.java\n// jar cvf javaAction.jar JavaAction.class\n\nimport com.google.gson.JsonObject;\n\npublic class JavaAction {\n    public static JsonObject main(JsonObject args) {\n        String text;\n\n        try {\n            text = args.getAsJsonPrimitive(\"text\").getAsString();\n        } catch(Exception e) {\n            text = \"stranger\";\n        }\n\n        JsonObject response = new JsonObject();\n        System.out.println(\"Hello \" + text + \"!\");\n        response.addProperty(\"payload\", \"Hello \" + text + \"!\");\n        return response;\n    }\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/resources/data/swiftAction.swift",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunc main(args: [String:Any]) -> [String:Any] {\n    if let text = args[\"text\"] as? String {\n        print(\"Hello \" + text + \"!\")\n        return [ \"payload\" : \"Hello \" + text + \"!\" ]\n    } else {\n        print(\"Hello stranger!\")\n        return [ \"payload\" : \"Hello stranger!\" ]\n    }\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/resources/data/users.csv",
    "content": "uuid,key\n23bc46b1-71f6-4ed5-8c54-816aa4f8c502,123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\nadd_more_users,here\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/scala/org/apache/openwhisk/ApiV1Simulation.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk\n\nimport org.apache.openwhisk.extension.whisk.Predef._\nimport io.gatling.core.Predef._\n\nimport scala.concurrent.duration._\n\nclass ApiV1Simulation extends Simulation {\n\n  // Specify parameters for the run\n  val host = sys.env(\"OPENWHISK_HOST\")\n  val connections = sys.env(\"CONNECTIONS\").toInt\n  val seconds = sys.env.getOrElse(\"SECONDS\", \"10\").toInt.seconds\n\n  // Specify thresholds\n  val requestsPerSec = sys.env(\"REQUESTS_PER_SEC\").toInt\n  val minimalRequestsPerSec = sys.env.getOrElse(\"MIN_REQUESTS_PER_SEC\", requestsPerSec.toString).toInt\n  val maxErrorsAllowed: Int = sys.env.getOrElse(\"MAX_ERRORS_ALLOWED\", \"0\").toInt\n  val maxErrorsAllowedPercentage: Double = sys.env.getOrElse(\"MAX_ERRORS_ALLOWED_PERCENTAGE\", \"0.1\").toDouble\n\n  // Generate the OpenWhiskProtocol\n  val openWhiskProtocol = openWhisk.apiHost(host)\n\n  // Define scenario\n  val test = scenario(\"api/v1 endpoint\")\n    .during(seconds) {\n      exec(openWhisk(\"Call api/v1 endpoint\").info())\n    }\n\n  setUp(test.inject(atOnceUsers(connections)))\n    .protocols(openWhiskProtocol)\n    // One failure will make the build yellow\n    .assertions(details(\"Call api/v1 endpoint\").requestsPerSec.gt(minimalRequestsPerSec))\n    .assertions(details(\"Call api/v1 endpoint\").requestsPerSec.gt(requestsPerSec))\n    // Mark the build yellow, if there are failed requests. And red if both conditions fail.\n    .assertions(details(\"Call api/v1 endpoint\").failedRequests.count.lte(maxErrorsAllowed))\n    .assertions(details(\"Call api/v1 endpoint\").failedRequests.percent.lte(maxErrorsAllowedPercentage))\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/scala/org/apache/openwhisk/BlockingInvokeOneActionSimulation.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk\n\nimport java.nio.charset.StandardCharsets\n\nimport org.apache.openwhisk.extension.whisk.OpenWhiskProtocolBuilder\nimport org.apache.openwhisk.extension.whisk.Predef._\nimport io.gatling.core.Predef._\nimport io.gatling.core.structure.ScenarioBuilder\nimport io.gatling.core.util.ClasspathPackagedResource\n\nimport scala.concurrent.duration._\n\nclass BlockingInvokeOneActionSimulation extends Simulation {\n  // Specify parameters for the run\n  val host = sys.env(\"OPENWHISK_HOST\")\n\n  // Specify authentication\n  val Array(uuid, key) = sys.env(\"API_KEY\").split(\":\")\n\n  val connections: Int = sys.env(\"CONNECTIONS\").toInt\n  val seconds: FiniteDuration = sys.env.getOrElse(\"SECONDS\", \"10\").toInt.seconds\n\n  // Specify thresholds\n  val requestsPerSec: Int = sys.env(\"REQUESTS_PER_SEC\").toInt\n  val minimalRequestsPerSec: Int = sys.env.getOrElse(\"MIN_REQUESTS_PER_SEC\", requestsPerSec.toString).toInt\n  val maxErrorsAllowed: Int = sys.env.getOrElse(\"MAX_ERRORS_ALLOWED\", \"0\").toInt\n  val maxErrorsAllowedPercentage: Double = sys.env.getOrElse(\"MAX_ERRORS_ALLOWED_PERCENTAGE\", \"0.1\").toDouble\n\n  // Generate the OpenWhiskProtocol\n  val openWhiskProtocol: OpenWhiskProtocolBuilder = openWhisk.apiHost(host)\n\n  // Specify async\n  val async = sys.env.getOrElse(\"ASYNC\", \"false\").toBoolean\n\n  val actionName = \"testActionForBlockingInvokeOneAction\"\n  val actionfile = if (async) \"/data/nodeJSAsyncAction.js\" else \"/data/nodeJSAction.js\"\n\n  // Define scenario\n  val test: ScenarioBuilder = scenario(s\"Invoke one ${if (async) \"async\" else \"sync\"} action blocking\")\n    .doIf(_.userId == 1) {\n      exec(openWhisk(\"Create action\")\n        .authenticate(uuid, key)\n        .action(actionName)\n        .create(ClasspathPackagedResource(actionfile, getClass.getResource(actionfile)).string(StandardCharsets.UTF_8)))\n    }\n    .rendezVous(connections)\n    .during(5.seconds) {\n      exec(openWhisk(\"Warm containers up\").authenticate(uuid, key).action(actionName).invoke())\n    }\n    .rendezVous(connections)\n    .during(seconds) {\n      exec(openWhisk(\"Invoke action\").authenticate(uuid, key).action(actionName).invoke())\n    }\n    .rendezVous(connections)\n    .doIf(_.userId == 1) {\n      exec(openWhisk(\"Delete action\").authenticate(uuid, key).action(actionName).delete())\n    }\n\n  setUp(test.inject(atOnceUsers(connections)))\n    .protocols(openWhiskProtocol)\n    // One failure will make the build yellow\n    .assertions(details(\"Invoke action\").requestsPerSec.gt(minimalRequestsPerSec))\n    .assertions(details(\"Invoke action\").requestsPerSec.gt(requestsPerSec))\n    // Mark the build yellow, if there are failed requests. And red if both conditions fail.\n    .assertions(details(\"Invoke action\").failedRequests.count.lte(maxErrorsAllowed))\n    .assertions(details(\"Invoke action\").failedRequests.percent.lte(maxErrorsAllowedPercentage))\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/scala/org/apache/openwhisk/ColdBlockingInvokeSimulation.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk\n\nimport java.nio.charset.StandardCharsets\n\nimport org.apache.openwhisk.extension.whisk.OpenWhiskProtocolBuilder\nimport org.apache.openwhisk.extension.whisk.Predef._\nimport io.gatling.core.Predef._\nimport io.gatling.core.session.Expression\nimport io.gatling.core.structure.ScenarioBuilder\nimport io.gatling.core.util.ClasspathPackagedResource\n\nimport scala.concurrent.duration._\n\nclass ColdBlockingInvokeSimulation extends Simulation {\n  // Specify parameters for the run\n  val host = sys.env(\"OPENWHISK_HOST\")\n\n  val users: Int = sys.env(\"USERS\").toInt\n  val codeSize: Int = sys.env.getOrElse(\"CODE_SIZE\", \"0\").toInt\n  val seconds: FiniteDuration = sys.env.getOrElse(\"SECONDS\", \"10\").toInt.seconds\n  val actionsPerUser: Int = sys.env.getOrElse(\"ACTIONS_PER_USER\", \"5\").toInt\n\n  // Specify thresholds\n  val requestsPerSec: Int = sys.env(\"REQUESTS_PER_SEC\").toInt\n  val minimalRequestsPerSec: Int = sys.env.getOrElse(\"MIN_REQUESTS_PER_SEC\", requestsPerSec.toString).toInt\n  val maxErrorsAllowed: Int = sys.env.getOrElse(\"MAX_ERRORS_ALLOWED\", \"0\").toInt\n  val maxErrorsAllowedPercentage: Double = sys.env.getOrElse(\"MAX_ERRORS_ALLOWED_PERCENTAGE\", \"0.1\").toDouble\n\n  // Generate the OpenWhiskProtocol\n  val openWhiskProtocol: OpenWhiskProtocolBuilder = openWhisk.apiHost(host)\n\n  val feeder = csv(\"data/users.csv\").queue\n\n  // Define scenario\n  val test: ScenarioBuilder = scenario(\"Invoke one action blocking\")\n    .feed(feeder)\n    .doIf(true) {\n      // This assignment assures to use the same user within this block. Otherwise create, invoke and delete would\n      // use other users.\n      val uuid: Expression[String] = \"${uuid}\"\n      val key: Expression[String] = \"${key}\"\n\n      val actionsPerUser = 5\n      val actionName: String = \"action-${i}\"\n\n      // Each user uses the given amount of actions\n      repeat(actionsPerUser, \"i\") {\n        exec(\n          openWhisk(\"Create action\")\n            .authenticate(uuid, key)\n            .action(actionName)\n            .create(actionCode))\n      }.rendezVous(users)\n        // Execute all actions for the given amount of time.\n        .during(seconds) {\n          // Cycle through the actions of this user, to not invoke the same action directly one after each other.\n          // Otherwise there is the possibility, that it is warm.\n          repeat(actionsPerUser, \"i\") {\n            exec(openWhisk(\"Invoke action\").authenticate(uuid, key).action(actionName).invoke())\n          }\n        }\n        .rendezVous(users)\n        .repeat(actionsPerUser, \"i\") {\n          exec(openWhisk(\"Delete action\").authenticate(uuid, key).action(actionName).delete())\n        }\n    }\n\n  private def actionCode = {\n    val code = ClasspathPackagedResource(\"nodeJSAction.js\", getClass.getResource(\"/data/nodeJSAction.js\"))\n      .string(StandardCharsets.UTF_8)\n    //Pad the code with empty space to increase the stored code size\n    if (codeSize > 0) code + \" \" * codeSize else code\n  }\n\n  setUp(test.inject(atOnceUsers(users)))\n    .protocols(openWhiskProtocol)\n    // One failure will make the build yellow\n    .assertions(details(\"Invoke action\").requestsPerSec.gt(minimalRequestsPerSec))\n    .assertions(details(\"Invoke action\").requestsPerSec.gt(requestsPerSec))\n    // Mark the build yellow, if there are failed requests. And red if both conditions fail.\n    .assertions(details(\"Invoke action\").failedRequests.count.lte(maxErrorsAllowed))\n    .assertions(details(\"Invoke action\").failedRequests.percent.lte(maxErrorsAllowedPercentage))\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/scala/org/apache/openwhisk/LatencySimulation.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk\n\nimport java.nio.charset.StandardCharsets\nimport java.util.Base64\n\nimport org.apache.openwhisk.extension.whisk.Predef._\nimport io.gatling.core.Predef._\nimport io.gatling.core.session.Expression\nimport io.gatling.core.util.ClasspathPackagedResource\n\nimport scala.concurrent.duration._\n\nclass LatencySimulation extends Simulation {\n  // Specify parameters for the run\n  val host = sys.env(\"OPENWHISK_HOST\")\n\n  // Specify authentication\n  val Array(uuid, key) = sys.env(\"API_KEY\").split(\":\")\n\n  val pauseBetweenInvokes: Int = sys.env.getOrElse(\"PAUSE_BETWEEN_INVOKES\", \"0\").toInt\n\n  val MEAN_RESPONSE_TIME = \"MEAN_RESPONSE_TIME\"\n  val MAX_MEAN_RESPONSE_TIME = \"MAX_MEAN_RESPONSE_TIME\"\n  val MAX_ERRORS_ALLOWED = \"MAX_ERRORS_ALLOWED\"\n  val MAX_ERRORS_ALLOWED_PERCENTAGE = \"MAX_ERRORS_ALLOWED_PERCENTAGE\"\n\n  // Specify thresholds\n  val meanResponseTime: Int = sys.env(MEAN_RESPONSE_TIME).toInt\n  val maximalMeanResponseTime: Int = sys.env.getOrElse(MAX_MEAN_RESPONSE_TIME, meanResponseTime.toString).toInt\n\n  val maxErrorsAllowed: Int = sys.env.getOrElse(MAX_ERRORS_ALLOWED, \"0\").toInt\n  val maxErrorsAllowedPercentage: Double = sys.env.getOrElse(MAX_ERRORS_ALLOWED_PERCENTAGE, \"0.1\").toDouble\n\n  def toKindSpecificKey(kind: String, suffix: String) = kind.split(':').head.toUpperCase + \"_\" + suffix\n\n  // Exclude runtimes\n  val excludedKinds: Seq[String] = sys.env.getOrElse(\"EXCLUDED_KINDS\", \"\").split(\",\")\n\n  // Generate the OpenWhiskProtocol\n  val openWhiskProtocol = openWhisk.apiHost(host)\n\n  /**\n   * Generate a list of actions to execute. The list is a tuple of (kind, code, actionName, main)\n   * `kind` is needed to create the action\n   * `code` is loaded form the files located in `resources/data`\n   * `actionName` is the name of the action in OpenWhisk\n   * `main` is only needed for java. This is the name of the class where the main method is located.\n   */\n  val actions: Seq[(String, String, String, String)] = Map(\n    \"nodejs:default\" -> (ClasspathPackagedResource(\"nodeJSAction.js\", getClass.getResource(\"/data/nodeJSAction.js\"))\n      .string(StandardCharsets.UTF_8), \"latencyTest_node\", \"\"),\n    \"python:default\" -> (ClasspathPackagedResource(\"pythonAction.py\", getClass.getResource(\"/data/pythonAction.py\"))\n      .string(StandardCharsets.UTF_8), \"latencyTest_python\", \"\"),\n    \"swift:default\" -> (ClasspathPackagedResource(\"swiftAction.swift\", getClass.getResource(\"/data/swiftAction.swift\"))\n      .string(StandardCharsets.UTF_8), \"latencyTest_swift\", \"\"),\n    \"java:default\" -> (Base64.getEncoder.encodeToString(ClasspathPackagedResource(\n      \"javaAction.jar\",\n      getClass.getResource(\"/data/javaAction.jar\")).bytes), \"latencyTest_java\", \"JavaAction\"))\n    .filterNot(e => excludedKinds.contains(e._1))\n    .map {\n      case (kind, (code, name, main)) =>\n        (kind, code, name, main)\n    }\n    .toSeq\n\n  // Define scenario\n  val test = scenario(\"Invoke one action after each other to test latency\")\n    .foreach(actions, \"action\") {\n      val code: Expression[String] = \"${action._2}\"\n      exec(\n        openWhisk(\"Create ${action._1} action\")\n          .authenticate(uuid, key)\n          .action(\"${action._3}\")\n          .create(code, \"${action._1}\", \"${action._4}\"))\n        .exec(openWhisk(\"Cold ${action._1} invocation\").authenticate(uuid, key).action(\"${action._3}\").invoke())\n        .repeat(100) {\n          // Add a pause of 100 milliseconds. Reason for this pause is, that collecting of logs runs asynchronously in\n          // invoker. If this is not finished before the next request arrives, a new cold-start has to be done.\n          pause(pauseBetweenInvokes.milliseconds)\n            .exec(openWhisk(\"Warm ${action._1} invocation\").authenticate(uuid, key).action(\"${action._3}\").invoke())\n        }\n        .exec(openWhisk(\"Delete ${action._1} action\").authenticate(uuid, key).action(\"${action._3}\").delete())\n    }\n\n  val testSetup = setUp(test.inject(atOnceUsers(1)))\n    .protocols(openWhiskProtocol)\n\n  actions\n    .map { case (kind, _, _, _) => kind }\n    .foldLeft(testSetup) { (agg, kind) =>\n      val cur = s\"Warm $kind invocation\"\n      // One failure will make the build yellow\n      val specificMeanResponseTime: Int =\n        sys.env.getOrElse(toKindSpecificKey(kind, MEAN_RESPONSE_TIME), meanResponseTime.toString).toInt\n      val specificMaxMeanResponseTime =\n        sys.env.getOrElse(toKindSpecificKey(kind, MAX_MEAN_RESPONSE_TIME), maximalMeanResponseTime.toString).toInt\n      val specificMaxErrorsAllowed =\n        sys.env.getOrElse(toKindSpecificKey(kind, MAX_ERRORS_ALLOWED), maxErrorsAllowed.toString).toInt\n      val specificMaxErrorsAllowedPercentage = sys.env\n        .getOrElse(toKindSpecificKey(kind, MAX_ERRORS_ALLOWED_PERCENTAGE), maxErrorsAllowedPercentage.toString)\n        .toDouble\n\n      agg\n        .assertions(details(cur).responseTime.mean.lte(specificMeanResponseTime))\n        .assertions(details(cur).responseTime.mean.lt(specificMaxMeanResponseTime))\n        // Mark the build yellow, if there are failed requests. And red if both conditions fail.\n        .assertions(details(cur).failedRequests.count.lte(specificMaxErrorsAllowed))\n        .assertions(details(cur).failedRequests.percent.lte(specificMaxErrorsAllowedPercentage))\n    }\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/scala/org/apache/openwhisk/extension/whisk/OpenWhiskActionBuilder.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.extension.whisk\n\nimport io.gatling.core.Predef._\nimport io.gatling.core.action.Action\nimport io.gatling.core.action.builder.ActionBuilder\nimport io.gatling.core.session.Expression\nimport io.gatling.core.structure.ScenarioContext\nimport io.gatling.http.request.builder.{Http, HttpRequestBuilder}\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\ncase class OpenWhiskActionBuilderBase(requestName: Expression[String]) {\n\n  implicit private val http = new Http(requestName)\n\n  /** Call the `/api/v1`-endpoint of the specified system */\n  def info() = {\n    OpenWhiskActionBuilder(http.get(\"/api/v1\"))\n  }\n\n  /**\n   * Specify authentication data. This is needed to perform operations on namespaces or working with entities.\n   *\n   * @param uuid The UUID of the namespace\n   * @param key The key of the namespace\n   */\n  def authenticate(uuid: Expression[String], key: Expression[String]) = {\n    OpenWhiskActionBuilderWithNamespace(uuid, key)\n  }\n}\n\ncase class OpenWhiskActionBuilderWithNamespace(private val uuid: Expression[String],\n                                               private val key: Expression[String],\n                                               private val namespace: String = \"_\")(implicit private val http: Http) {\n\n  /**\n   * Specify on which namespace you want to perform any action.\n   *\n   * @param namespace The namespace you want to use.\n   */\n  def namespace(namespace: String) = {\n    OpenWhiskActionBuilderWithNamespace(uuid, key, namespace)\n  }\n\n  /** List all namespaces you have access to, with your current authentication. */\n  def list() = {\n    OpenWhiskActionBuilder(http.get(\"/api/v1/namespaces\").basicAuth(uuid, key))\n  }\n\n  /**\n   * Perform any request against the actions-API. E.g. creating, invoking or deleting actions.\n   *\n   * @param actionName Name of the action in the Whisk-system.\n   */\n  def action(actionName: String) = {\n    OpenWhiskActionBuilderWithAction(uuid, key, namespace, actionName)\n  }\n}\n\ncase class OpenWhiskActionBuilderWithAction(private val uuid: Expression[String],\n                                            private val key: Expression[String],\n                                            private val namespace: String,\n                                            private val action: String)(implicit private val http: Http) {\n  private val path: Expression[String] = s\"/api/v1/namespaces/$namespace/actions/$action\"\n\n  /** Fetch the action from OpenWhisk */\n  def get() = {\n    OpenWhiskActionBuilder(http.get(path).basicAuth(uuid, key))\n  }\n\n  /** Delete the action from OpenWhisk */\n  def delete() = {\n    OpenWhiskActionBuilder(http.delete(path).basicAuth(uuid, key))\n  }\n\n  /**\n   * Create the action in OpenWhisk.\n   *\n   * @param code The code of the action to create.\n   * @param kind The kind of the action you want to create. Default is `nodejs:default`.\n   * @param main Main method of your action. This is only needed for java actions.\n   */\n  def create(code: Expression[String], kind: Expression[String] = \"nodejs:default\", main: Expression[String] = \"\") = {\n\n    val json: Expression[String] = session => {\n      code(session).flatMap { c =>\n        kind(session).flatMap { k =>\n          main(session).map { m =>\n            val exec = Map(\"kind\" -> k, \"code\" -> c) ++ (if (m.size > 0) Map(\"main\" -> m) else Map[String, String]())\n            JsObject(\"exec\" -> exec.toJson).compactPrint\n          }\n        }\n      }\n    }\n\n    OpenWhiskActionBuilder(http.put(path).basicAuth(uuid, key).body(StringBody(json)))\n  }\n\n  /** Invoke the action. */\n  def invoke() = {\n    OpenWhiskActionBuilder(http.post(path).queryParam(\"blocking\", \"true\").basicAuth(uuid, key))\n  }\n}\n\ncase class OpenWhiskActionBuilder(http: HttpRequestBuilder) extends ActionBuilder {\n  override def build(ctx: ScenarioContext, next: Action): Action = {\n    http.build(ctx, next)\n  }\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/scala/org/apache/openwhisk/extension/whisk/OpenWhiskDsl.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.extension.whisk\n\nimport io.gatling.core.session.Expression\n\ntrait OpenWhiskDsl {\n  def openWhisk = OpenWhiskProtocolBuilderBase\n\n  def openWhisk(requestName: Expression[String]) = new OpenWhiskActionBuilderBase(requestName)\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/scala/org/apache/openwhisk/extension/whisk/OpenWhiskProtocolBuilder.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.extension.whisk\n\nimport java.net.URL\n\nimport com.softwaremill.quicklens._\nimport io.gatling.core.config.GatlingConfiguration\nimport io.gatling.http.Predef._\nimport io.gatling.core.Predef._\nimport io.gatling.http.protocol.HttpProtocol\n\nimport scala.language.implicitConversions\n\n/**\n * This is the OpenWhiskProtocol.\n *\n * @param apiHost url or address to connect to\n * @param protocol protocol to use. e.g. https\n * @param port port to use. e.g. 443\n */\ncase class OpenWhiskProtocol(apiHost: String, protocol: String = \"https\", port: Int = 443)\n\nobject OpenWhiskProtocol {\n\n  def apply(url: String): OpenWhiskProtocol = {\n    if (url.startsWith(\"http://\") || url.startsWith(\"https://\")) {\n      val u = new URL(url)\n      val port = if (u.getPort > 0) u.getPort else u.getDefaultPort\n      OpenWhiskProtocol(u.getHost, u.toURI.getScheme, port)\n    } else new OpenWhiskProtocol(url)\n  }\n}\n\ncase object OpenWhiskProtocolBuilderBase {\n  def apiHost(url: String): OpenWhiskProtocolBuilder = OpenWhiskProtocolBuilder(OpenWhiskProtocol(url))\n}\n\nobject OpenWhiskProtocolBuilder {\n\n  /** convert the OpenWhiskProtocolBuilder to an HttpProtocol. */\n  implicit def toHttpProtocol(builder: OpenWhiskProtocolBuilder)(\n    implicit configuration: GatlingConfiguration): HttpProtocol = builder.build\n}\n\ncase class OpenWhiskProtocolBuilder(private val protocol: OpenWhiskProtocol) {\n\n  /** set the api host */\n  def apiHost(url: String): OpenWhiskProtocolBuilder = this.modify(_.protocol.apiHost).setTo(url)\n\n  /** set the protocol */\n  def protocol(protocol: String): OpenWhiskProtocolBuilder = this.modify(_.protocol.protocol).setTo(protocol)\n\n  /** set the port */\n  def port(port: Int): OpenWhiskProtocolBuilder = this.modify(_.protocol.port).setTo(port)\n\n  /** build the http protocol with the parameters provided by the openwhisk-protocol. */\n  def build(implicit configuration: GatlingConfiguration) = {\n    http\n      .baseUrl(s\"${protocol.protocol}://${protocol.apiHost}:${protocol.port}\")\n      .contentTypeHeader(\"application/json\")\n      .userAgentHeader(\"gatlingLoadTest\")\n      .warmUp(\"http://google.com\")\n      .build\n  }\n}\n"
  },
  {
    "path": "tests/performance/gatling_tests/src/gatling/scala/org/apache/openwhisk/extension/whisk/Predef.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.extension.whisk\n\nobject Predef extends OpenWhiskDsl\n"
  },
  {
    "path": "tests/performance/preparation/actions/async.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main() {\n  return new Promise(function (resolve, reject) {\n    setTimeout(function () {\n      resolve({done: true});\n    }, 175);\n  })\n}\n"
  },
  {
    "path": "tests/performance/preparation/actions/noop.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction main() {\n  return {};\n}\n"
  },
  {
    "path": "tests/performance/preparation/create.sh",
    "content": "#!/bin/sh\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\n# Host to use. Needs to include the protocol.\nhost=$1\n# Credentials to use for the test. USER:PASS format.\ncredentials=$2\n# Name of the action to create and test.\naction=$3\n# Path to action src\naction_src=$4\n# Concurrency setting\naction_concurrency=${5:-1}\n\n# jq will json encode the src (need to strip leading/trailing quotes that jq adds)\n#action_code=$(jq -cs . \"$action_src\" | sed 's/^.\\(.*\\).$/\\1/')\naction_code=$(cat \"$action_src\")\n\n# setup the action json to create the action\naction_json='{\"namespace\":\"_\",\"name\":\"'\"$action\"'\",\"exec\":{\"kind\":\"nodejs:default\",\"code\":\"\"},\"limits\":{\"concurrency\":'\"$action_concurrency\"'}}'\naction_json=$(echo  \"$action_json\" | jq -c --arg code \"$action_code\" '.exec.code=($code)')\n\n\n# create a noop action\necho \"Creating action $action\"\ncurl -k -u \"$credentials\" \"$host/api/v1/namespaces/_/actions/$action\" -XPUT -d \"$action_json\" -H \"Content-Type: application/json\"\n\n# run the noop action\necho \"Running $action once to assert an intact system\"\ncurl -k -u \"$credentials\" \"$host/api/v1/namespaces/_/actions/$action?blocking=true\" -XPOST\n"
  },
  {
    "path": "tests/performance/preparation/deploy-lean.sh",
    "content": "#!/bin/sh\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\nSCRIPTDIR=\"$(cd \"$(dirname \"$0\")\"; pwd)\"\nROOTDIR=\"$SCRIPTDIR/../../..\"\n\n# Build Openwhisk\ncd $ROOTDIR\nTERM=dumb ./gradlew distDocker -PdockerImagePrefix=testing $GRADLE_PROJS_SKIP\n\n# Deploy Openwhisk\ncd $ROOTDIR/ansible\nANSIBLE_CMD=\"$ANSIBLE_CMD -e limit_invocations_per_minute=999999 -e limit_invocations_concurrent=999999 -e controller_client_auth=false -e userLogs_spi=\\\"org.apache.openwhisk.core.containerpool.logging.LogDriverLogStoreProvider\\\"\"\n\n$ANSIBLE_CMD setup.yml\n\n$ANSIBLE_CMD prereq.yml\n$ANSIBLE_CMD couchdb.yml\n$ANSIBLE_CMD initdb.yml\n$ANSIBLE_CMD wipe.yml\n\n$ANSIBLE_CMD controller.yml -e lean=true\n$ANSIBLE_CMD edge.yml\n"
  },
  {
    "path": "tests/performance/preparation/deploy.sh",
    "content": "#!/bin/sh\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\nSCRIPTDIR=\"$(cd \"$(dirname \"$0\")\"; pwd)\"\nROOTDIR=\"$SCRIPTDIR/../../..\"\n\n# Build Openwhisk\ncd $ROOTDIR\nTERM=dumb ./gradlew distDocker -PdockerImagePrefix=testing $GRADLE_PROJS_SKIP\n\n# Deploy Openwhisk\ncd $ROOTDIR/ansible\nANSIBLE_CMD=\"$ANSIBLE_CMD -e limit_invocations_per_minute=999999 -e limit_invocations_concurrent=999999 -e controller_client_auth=false -e userLogs_spi=\\\"org.apache.openwhisk.core.containerpool.logging.LogDriverLogStoreProvider\\\"\"\n\n$ANSIBLE_CMD setup.yml\n$ANSIBLE_CMD prereq.yml\n$ANSIBLE_CMD couchdb.yml\n$ANSIBLE_CMD initdb.yml\n$ANSIBLE_CMD wipe.yml\n\n$ANSIBLE_CMD kafka.yml\n$ANSIBLE_CMD controller.yml\n$ANSIBLE_CMD invoker.yml\n$ANSIBLE_CMD edge.yml\n"
  },
  {
    "path": "tests/performance/wrk_tests/latency.sh",
    "content": "#!/bin/sh\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\ncurrentDir=\"$(cd \"$(dirname \"$0\")\"; pwd)\"\n\n# Host to use. Needs to include the protocol.\nhost=$1\n# Credentials to use for the test. USER:PASS format.\ncredentials=$2\n# Path to action src\naction_src=$3\n# How long to run the test\nduration=${4:-30s}\n\n$currentDir/throughput.sh $host $credentials  $action_src 1 1 1 $duration\n"
  },
  {
    "path": "tests/performance/wrk_tests/post.lua",
    "content": "--\n-- Licensed to the Apache Software Foundation (ASF) under one or more\n-- contributor license agreements.  See the NOTICE file distributed with\n-- this work for additional information regarding copyright ownership.\n-- The ASF licenses this file to You under the Apache License, Version 2.0\n-- (the \"License\"); you may not use this file except in compliance with\n-- the License.  You may obtain a copy of the License at\n--\n--     http://www.apache.org/licenses/LICENSE-2.0\n--\n-- Unless required by applicable law or agreed to in writing, software\n-- distributed under the License is distributed on an \"AS IS\" BASIS,\n-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-- See the License for the specific language governing permissions and\n-- limitations under the License.\n--\nwrk.method = \"POST\"\n"
  },
  {
    "path": "tests/performance/wrk_tests/throughput.sh",
    "content": "#!/bin/sh\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\ncurrentDir=\"$(cd \"$(dirname \"$0\")\"; pwd)\"\n\n# Host to use. Needs to include the protocol.\nhost=$1\n# Credentials to use for the test. USER:PASS format.\ncredentials=$2\n# Path to action src\naction_src=$3\n# concurrency level of the throughput test: How many requests should\n# open in parallel.\nconcurrency=$4\n# Action concurrency setting (how many concurrent activations does action allow?)\naction_concurrency=${5:-1}\n# How many threads to utilize, directly correlates to the number\n# of CPU cores\nthreads=${6:-4}\n# How long to run the test\nduration=${7:-30s}\n\n# Use the filename (without extension) of the action_src as the name of the action\naction=\"$(basename $action_src | cut -f 1 -d '.')_$action_concurrency\"\n\n\"$currentDir/../preparation/create.sh\" \"$host\" \"$credentials\" \"$action\" \"$action_src\" \"$action_concurrency\"\n\n# run throughput tests\nencodedAuth=$(echo \"$credentials\" | tr -d '\\n' | base64 | tr -d '\\n')\ndocker run --pid=host --userns=host --rm -v \"$currentDir\":/data williamyeh/wrk \\\n  --threads \"$threads\" \\\n  --connections \"$concurrency\" \\\n  --duration \"$duration\" \\\n  --header \"Authorization: basic $encodedAuth\" \\\n  --header \"X-Request-ID: throughput-$action\" \\\n  \"$host/api/v1/namespaces/_/actions/$action?blocking=true\" \\\n  --latency \\\n  --timeout 10s \\\n  --script post.lua\n"
  },
  {
    "path": "tests/src/main/scala/org/apache/openwhisk/GradleWorkaround.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk\n\n/**\n *  This class is a workaround for https://github.com/gradle/gradle/issues/6849\n */\nclass GradleWorkaround {}\n"
  },
  {
    "path": "tests/src/test/resources/application.conf.j2",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one or more contributor\n# license agreements; and to You under the Apache License, Version 2.0.\n\nwhisk.spi {\n  SimpleSpi = org.apache.openwhisk.spi.SimpleSpiImpl\n  MissingSpi = org.apache.openwhisk.spi.MissingImpl\n  MissingModule = missing.module\n}\n\n# Blocking requests fall back to non-blocking after ~60s\npekko.http.client.idle-timeout = 90 s\npekko.http.host-connection-pool.idle-timeout = 90 s\npekko.http.host-connection-pool.client.idle-timeout = 90 s\n\n# Avoid system exit for test runs\npekko.jvm-exit-on-fatal-error = off\n\n# Each ActorSystem binds to a free port\npekko.remote.artery.canonical.port=0\n\nwhisk {\n    feature-flags {\n      require-api-key-annotation = {{ whisk.feature_flags.require_api_key_annotation | default(true) }}\n      require-response-payload = {{ whisk.feature_flags.require_response_payload | default(true) }}\n    }\n    # kafka related configuration\n    kafka {\n        replication-factor = 1\n        topics {\n            KafkaConnectorTestTopic {\n                segment-bytes   =  536870912\n                retention-bytes = 1073741824\n                retention-ms    = 3600000\n            }\n            prefix = \"{{ kafka.topicsPrefix }}\"\n            user-event {\n                prefix = \"{{ kafka.topicsUserEventPrefix }}\"\n            }\n        }\n        common {\n          security-protocol: {{ kafka.protocol }}\n          ssl-truststore-location: \"{{ openwhisk_home }}/ansible/roles/kafka/files/{{ kafka.ssl.keystore.name }}\"\n          ssl-truststore-password: \"{{ kafka.ssl.keystore.password }}\"\n          ssl-keystore-location: \"{{ openwhisk_home }}/ansible/roles/kafka/files/{{ kafka.ssl.keystore.name }}\"\n          ssl-keystore-password: \"{{ kafka.ssl.keystore.password }}\"\n        }\n        consumer {\n          max-poll-interval-ms: 10000\n        }\n    }\n\n    couchdb {\n        protocol = \"{{ db.protocol }}\"\n        host     = \"{{ db.host }}\"\n        port     = \"{{ db.port }}\"\n        username = \"{{ db.credentials.admin.user }}\"\n        password = \"{{ db.credentials.admin.pass }}\"\n        provider = \"{{ db.provider }}\"\n        databases {\n          WhiskAuth       = \"{{ db.whisk.auth }}\"\n          WhiskEntity     = \"{{ db.whisk.actions }}\"\n          WhiskActivation = \"{{ db.whisk.activations }}\"\n        }\n    }\n\n    cosmosdb {\n        endpoint   = ${?COSMOSDB_ENDPOINT}\n        key        = ${?COSMOSDB_KEY}\n        db         = ${?COSMOSDB_NAME}\n        throughput = 400\n    }\n\n    mongodb {\n        docker-image = \"{{ 'mongo:' ~ mongodb.version }}\"\n    }\n\n    controller {\n      protocol = {{ controller.protocol }}\n      https {\n        keystore-flavor = \"{{ controller.ssl.storeFlavor }}\"\n        keystore-path = \"{{ openwhisk_home }}/ansible/roles/controller/files/{{ controller.ssl.keystore.name }}\"\n        keystore-password = \"{{ controller.ssl.keystore.password }}\"\n        client-auth = \"{{ controller.ssl.clientAuth }}\"\n      }\n    }\n    invoker {\n      protocol = {{ invoker.protocol }}\n      https {\n        keystore-flavor = \"{{ invoker.ssl.storeFlavor }}\"\n        keystore-path = \"{{ openwhisk_home }}/ansible/roles/invoker/files/{{ invoker.ssl.keystore.name }}\"\n        keystore-password = \"{{ invoker.ssl.keystore.password }}\"\n        client-auth = \"{{ invoker.ssl.clientAuth }}\"\n      }\n    }\n    user-events {\n        enabled = {{ user_events }}\n    }\n\n    container-factory {\n        runtimes-registry {\n            url = \"{{ runtimes_registry | default('') }}\"\n        }\n        user-images-registry {\n            url = \"{{ user_images_registry | default('') }}\"\n        }\n    }\n\n    parameter-storage {\n        current = \"off\"\n    }\n\n    elasticsearch {\n        docker-image = \"{{ elasticsearch.docker_image | default('docker.elastic.co/elasticsearch/elasticsearch:' ~ elasticsearch.version ) }}\"\n    }\n\n    helm.release = \"release\"\n    runtime.delete.timeout = \"30 seconds\"\n\n    duration-checker {\n        time-window = \"{{ durationChecker.timeWindow }}\"\n    }\n\n    activation-store {\n        elasticsearch {\n            protocol      = \"{{ db.elasticsearch.protocol }}\"\n            hosts         = \"{{ elasticsearch_connect_string }}\"\n            index-pattern = \"{{ db.elasticsearch.index_pattern }}\"\n            username      = \"{{ db.elasticsearch.auth.admin.username }}\"\n            password      = \"{{ db.elasticsearch.auth.admin.password }}\"\n        }\n    }\n\n    etcd {\n        hosts = \"{{ etcd_connect_string }}\"\n        lease {\n            timeout = \"{{ etcd.lease.timeout }}\"\n        }\n        pool {\n            threads = \"{{ etcd.pool_threads }}\"\n        }\n    }\n\n    scheduler {\n        protocol = \"{{ scheduler.protocol }}\"\n        grpc {\n            tls = \"{{ scheduler.grpc.tls | default('false') | lower }}\"\n        }\n        queue {\n            idle-grace = \"{{ scheduler.queue.idleGrace | default('20 seconds') }}\"\n            stop-grace = \"{{ scheduler.queue.stopGrace | default('20 seconds') }}\"\n            flush-grace = \"{{ scheduler_queue_flushGrace | default('60 seconds') }}\"\n            graceful-shutdown-timeout = \"{{ scheduler.queue.gracefulShutdownTimeout | default('5 seconds') }}\"\n            max-retention-size = \"{{ scheduler.queue.maxRetentionSize | default(10000) }}\"\n            max-retention-ms = \"{{ scheduler.queue.maxRetentionMs | default(60000) }}\"\n            max-blackbox-retention-ms = \"{{ scheduler.queue.maxBlackboxRetentionMs}}\"\n            throttling-fraction = \"{{ scheduler.queue.throttlingFraction | default(0.9) }}\"\n            duration-buffer-size = \"{{ scheduler.queue.durationBufferSize | default(10) }}\"\n        }\n        queue-manager {\n            max-scheduling-time = \"{{ scheduler.queueManager.maxSchedulingTime }}\"\n            max-retries-to-get-queue = \"{{ scheduler.queueManager.maxRetriesToGetQueue }}\"\n        }\n        max-peek = \"{{ scheduler.maxPeek }}\"\n        in-progress-job-retention = \"{{ scheduler.inProgressJobRetention | default('20 seconds') }}\"\n        data-management-service {\n            retry-interval = \"{{ scheduler.dataManagementService.retryInterval | default('1 second') }}\"\n        }\n        blackbox-multiple = \"{{ scheduler.blackboxMultiple }}\"\n    }\n}\n\n#test-only overrides so that tests can override defaults in application.conf (todo: move all defaults to reference.conf)\ntest {\n  whisk {\n    concurrency-limit {\n      max = 200\n    }\n    namespace-default-limit.concurrency-limit {\n      min = 1\n      max = 200\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n    <jmxConfigurator></jmxConfigurator>\n    <appender name=\"console\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>[%d{yyyy-MM-dd'T'HH:mm:ss.SSS'Z'}] [%p] %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <!-- Apache HttpClient -->\n    <logger name=\"org.apache.http\" level=\"ERROR\" />\n\n    <!-- Kafka -->\n    <logger name=\"org.apache.kafka\" level=\"ERROR\" />\n    <logger name=\"kafka\" level=\"ERROR\" />\n\n    <!-- Zookeeper -->\n    <logger name=\"org.apache.zookeeper\" level=\"ERROR\" />\n    <logger name=\"org.apache.curator\" level=\"ERROR\" />\n\n    <logger name=\"org.apache.pekko.event.slf4j.Slf4jLogger\" level=\"WARN\" />\n\n    <!-- Disable verbose info logging from processor library-->\n    <logger name=\"com.microsoft.azure.documentdb.changefeedprocessor.services\" level=\"WARN\" />\n    <logger name=\"com.microsoft.azure.documentdb.changefeedprocessor.internal\" level=\"WARN\" />\n\n    <logger name=\"org.apache.openwhisk.core.database.cosmosdb.cache\" level=\"DEBUG\" />\n\n    <root level=\"${logback.log.level:-INFO}\">\n        <appender-ref ref=\"console\" />\n    </root>\n</configuration>"
  },
  {
    "path": "tests/src/test/resources/swagger-config.json",
    "content": "{\n    \"library\": \"okhttp-gson\",\n    \"dateLibrary\": \"java8\",\n    \"hideGenerationTimestamp\": true\n}\n"
  },
  {
    "path": "tests/src/test/resources/templates/build.gradle.mustache",
    "content": "apply plugin: 'idea'\napply plugin: 'eclipse'\n\ngroup = 'io.swagger'\nversion = '1.0.0'\n\nbuildscript {\n    repositories {\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:2.3.+'\n        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'\n    }\n}\n\nrepositories {\n    mavenCentral()\n}\n\n\nif(hasProperty('target') && target == 'android') {\n\n    apply plugin: 'com.android.library'\n    apply plugin: 'com.github.dcendents.android-maven'\n\n    android {\n        compileSdkVersion 25\n        buildToolsVersion '25.0.2'\n        defaultConfig {\n            minSdkVersion 14\n            targetSdkVersion 25\n        }\n        compileOptions {\n            sourceCompatibility JavaVersion.VERSION_1_8\n            targetCompatibility JavaVersion.VERSION_1_8\n        }\n\n        // Rename the aar correctly\n        libraryVariants.all { variant ->\n            variant.outputs.each { output ->\n                def outputFile = output.outputFile\n                if (outputFile != null && outputFile.name.endsWith('.aar')) {\n                    def fileName = \"${project.name}-${variant.baseName}-${version}.aar\"\n                    output.outputFile = new File(outputFile.parent, fileName)\n                }\n            }\n        }\n\n        dependencies {\n            provided 'javax.annotation:jsr250-api:1.0'\n        }\n    }\n\n    afterEvaluate {\n        android.libraryVariants.all { variant ->\n            def task = project.tasks.create \"jar${variant.name.capitalize()}\", Jar\n            task.description = \"Create jar artifact for ${variant.name}\"\n            task.dependsOn variant.javaCompile\n            task.from variant.javaCompile.destinationDir\n            task.destinationDir = project.file(\"${project.buildDir}/outputs/jar\")\n            task.archiveName = \"${project.name}-${variant.baseName}-${version}.jar\"\n            artifacts.add('archives', task);\n        }\n    }\n\n    task sourcesJar(type: Jar) {\n        from android.sourceSets.main.java.srcDirs\n        classifier = 'sources'\n    }\n\n    artifacts {\n        archives sourcesJar\n    }\n\n} else {\n\n    apply plugin: 'java'\n    apply plugin: 'maven-publish'\n\n    sourceCompatibility = JavaVersion.VERSION_1_8\n    targetCompatibility = JavaVersion.VERSION_1_8\n\n    /*\n    install {\n        repositories.mavenInstaller {\n            pom.artifactId = 'swagger-java-client'\n        }\n    }\n    */\n    publishing {\n        publications {\n            mavenJava(MavenPublication) {\n                from components.java\n                groupId = 'com.example'\n                artifactId = 'swagger-java-client'\n                version = '1.0.0'\n            }\n        }\n        repositories {\n            maven {\n                name = \"localRepo\"\n                url = uri(\"${buildDir}/repo\") // or mavenLocal() or remote repo\n            }\n        }\n    }\n\n\n    task execute(type:JavaExec) {\n       main = System.getProperty('mainClass')\n       classpath = sourceSets.main.runtimeClasspath\n    }\n}\n\ndependencies {\n    implementation 'io.swagger:swagger-annotations:1.5.17'\n    implementation 'com.squareup.okhttp:okhttp:2.7.5'\n    implementation 'com.squareup.okhttp:logging-interceptor:2.7.5'\n    implementation 'com.google.code.gson:gson:2.8.1'\n    implementation 'io.gsonfire:gson-fire:1.8.0'\n    testImplementation 'junit:junit:4.12'\n}\n"
  },
  {
    "path": "tests/src/test/scala/actionContainers/ActionContainer.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage actionContainers\n\nimport java.io.ByteArrayOutputStream\nimport java.io.File\nimport java.io.PrintWriter\n\nimport scala.util.Try\nimport scala.concurrent.Await\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.Future\nimport scala.concurrent.blocking\nimport scala.concurrent.duration.Duration\nimport scala.concurrent.duration.DurationInt\nimport scala.sys.process.ProcessLogger\nimport scala.sys.process.stringToProcess\nimport scala.util.Random\nimport scala.util.{Failure, Success}\nimport org.apache.commons.lang3.StringUtils\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.concurrent.ExecutionContext\nimport spray.json._\nimport common.StreamLogging\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.Exec\nimport common.WhiskProperties\nimport org.apache.openwhisk.core.containerpool.Container\n\n/**\n * For testing convenience, this interface abstracts away the REST calls to a\n * container as blocking method calls of this interface.\n */\ntrait ActionContainer {\n  def init(value: JsValue): (Int, Option[JsObject])\n  def run(value: JsValue): (Int, Option[JsObject])\n  def runForJsArray(value: JsValue): (Int, Option[JsArray])\n  def runMultiple(values: Seq[JsValue])(implicit ec: ExecutionContext): Seq[(Int, Option[JsObject])]\n}\n\ntrait ActionProxyContainerTestUtils extends AnyFlatSpec with Matchers with StreamLogging {\n  import ActionContainer.{filterSentinel, sentinel}\n\n  def initPayload(code: String, main: String = \"main\", env: Option[Map[String, JsString]] = None): JsObject =\n    JsObject(\n      \"value\" -> JsObject(\n        \"code\" -> { if (code != null) JsString(code) else JsNull },\n        \"main\" -> JsString(main),\n        \"binary\" -> JsBoolean(Exec.isBinaryCode(code)),\n        \"env\" -> env.map(JsObject(_)).getOrElse(JsNull)))\n\n  def runPayload(args: JsValue, other: Option[JsObject] = None): JsObject =\n    JsObject(Map(\"value\" -> args) ++ (other map { _.fields } getOrElse Map.empty))\n\n  def checkStreams(out: String,\n                   err: String,\n                   additionalCheck: (String, String) => Unit,\n                   sentinelCount: Int = 1,\n                   concurrent: Boolean = false): Unit = {\n    withClue(\"expected number of stdout sentinels\") {\n      sentinelCount shouldBe StringUtils.countMatches(out, sentinel)\n    }\n    //sentinels should be all together\n    if (concurrent) {\n      withClue(\"expected grouping of stdout sentinels\") {\n        out should include((1 to sentinelCount).map(_ => sentinel + \"\\n\").mkString)\n      }\n    }\n    withClue(\"expected number of stderr sentinels\") {\n      sentinelCount shouldBe StringUtils.countMatches(err, sentinel)\n    }\n    //sentinels should be all together\n    if (concurrent) {\n      withClue(\"expected grouping of stderr sentinels\") {\n        err should include((1 to sentinelCount).map(_ => sentinel + \"\\n\").mkString)\n      }\n    }\n\n    val (o, e) = (filterSentinel(out), filterSentinel(err))\n    o should not include sentinel\n    e should not include sentinel\n    additionalCheck(o, e)\n  }\n}\n\nobject ActionContainer {\n  private lazy val dockerBin: String = {\n    List(\"/usr/bin/docker\", \"/usr/local/bin/docker\").find { bin =>\n      new File(bin).isFile\n    }.get // This fails if the docker binary couldn't be located.\n  }\n\n  lazy val dockerCmd: String = {\n    /*\n     * The docker host is set to a provided property 'docker.host' if it's\n     * available; otherwise we check with WhiskProperties to see whether we are\n     * running on a docker-machine.\n     *\n     * IMPLICATION:  The test must EITHER have the 'docker.host' system\n     * property set OR the 'OPENWHISK_HOME' environment variable set and a\n     * valid 'whisk.properties' file generated.  The 'docker.host' system\n     * property takes precedence.\n     *\n     * WARNING:  Adding a non-docker-machine environment that contains 'mac'\n     * (i.e. 'environments/local-mac') will likely break things.\n     *\n     * The plan is to move builds to using 'gradle-docker-plugin', which know\n     * its docker socket and to have it pass the docker socket implicitly using\n     * 'systemProperty \"docker.host\", docker.url'.  Eventually, we will also\n     * need to handle TLS certificates here.  Again, 'gradle-docker-plugin'\n     * knows where they are; we will just add system properties to get the\n     * information onto the docker command line.\n     */\n    val dockerCmdString = dockerBin +\n      sys.props\n        .get(\"docker.host\")\n        .orElse(sys.env.get(\"DOCKER_HOST\"))\n        .orElse {\n          Try { // whisk.properties file may not exist\n            // Check if we are running on docker-machine env.\n            Option(WhiskProperties.getProperty(\"environment.type\"))\n              .filter(_.toLowerCase.contains(\"docker-machine\"))\n              .map {\n                case _ => s\"tcp://${WhiskProperties.getMainDockerEndpoint}\"\n              }\n          }.toOption.flatten\n        }\n        .map(\" --host \" + _)\n        .getOrElse(\"\")\n\n    // Test here that this actually works, otherwise throw a somewhat understandable error message\n    proc(s\"$dockerCmdString info\").onComplete {\n      case Success((v, _, _)) if v != 0 =>\n        throw new RuntimeException(s\"\"\"\n              |Unable to connect to docker host using $dockerCmdString as command string.\n              |The docker host is determined using the Java property 'docker.host' or\n              |the environment variable 'DOCKER_HOST'. Please verify that one or the\n              |other is set for your build/test process.\"\"\".stripMargin)\n      case Success((v, _, _)) if v == 0 => // Do nothing\n      case Failure(t)                   => throw t\n    }\n\n    dockerCmdString\n  }\n\n  private def docker(command: String): String = s\"$dockerCmd $command\"\n\n  // Runs a process asynchronously. Returns a future with (exitCode,stdout,stderr)\n  private def proc(cmd: String): Future[(Int, String, String)] = Future {\n    blocking {\n      val out = new ByteArrayOutputStream\n      val err = new ByteArrayOutputStream\n      val outW = new PrintWriter(out)\n      val errW = new PrintWriter(err)\n      val v = cmd ! ProcessLogger(o => outW.println(o), e => errW.println(e))\n      outW.close()\n      errW.close()\n      (v, out.toString, err.toString)\n    }\n  }\n\n  // Tying it all together, we have a method that runs docker, waits for\n  // completion for some time then returns the exit code, the output stream\n  // and the error stream.\n  private def awaitDocker(cmd: String, t: Duration): (Int, String, String) = {\n    Await.result(proc(docker(cmd)), t)\n  }\n\n  // Filters out the sentinel markers inserted by the container (see relevant private code in Invoker)\n  val sentinel = Container.ACTIVATION_LOG_SENTINEL\n  def filterSentinel(str: String): String = str.replaceAll(sentinel, \"\").trim\n\n  def withContainer(imageName: String, environment: Map[String, String] = Map.empty)(\n    code: ActionContainer => Unit)(implicit actorSystem: ActorSystem, logging: Logging): (String, String) = {\n    val rand = { val r = Random.nextInt; if (r < 0) -r else r }\n    val name = imageName.toLowerCase.replaceAll(\"\"\"[^a-z]\"\"\", \"\") + rand\n    val envArgs = environment.toSeq\n      .map {\n        case (k, v) => s\"-e $k=$v\"\n      }\n      .mkString(\" \")\n\n    // We create the container... and find out its IP address...\n    def createContainer(portFwd: Option[Int] = None): Unit = {\n      val runOut = awaitDocker(\n        s\"run ${portFwd.map(p => s\"-p $p:8080\").getOrElse(\"\")} --name $name $envArgs -d $imageName\",\n        60.seconds)\n      assert(runOut._1 == 0, \"'docker run' did not exit with 0: \" + runOut)\n    }\n\n    // ...find out its IP address...\n    val (ip, port) =\n      if (System.getProperty(\"os.name\").toLowerCase().contains(\"mac\") && !sys.env\n            .get(\"DOCKER_HOST\")\n            .exists(_.trim.nonEmpty)) {\n        // on MacOSX, where docker for mac does not permit communicating with container directly\n        val p = 8988 // port must be available or docker run will fail\n        createContainer(Some(p))\n        Thread.sleep(1500) // let container/server come up cleanly\n        (\"localhost\", p)\n      } else {\n        // not \"mac\" i.e., docker-for-mac, use direct container IP directly (this is OK for Ubuntu, and docker-machine)\n        createContainer()\n        val ipOut = awaitDocker(s\"\"\"inspect --format '{{.NetworkSettings.IPAddress}}' $name\"\"\", 10.seconds)\n        assert(ipOut._1 == 0, \"'docker inspect did not exit with 0\")\n        (ipOut._2.replaceAll(\"\"\"[^0-9.]\"\"\", \"\"), 8080)\n      }\n\n    // ...we create an instance of the mock container interface...\n    val mock = new ActionContainer {\n      def init(value: JsValue): (Int, Option[JsObject]) = syncPost(ip, port, \"/init\", value)\n      def run(value: JsValue): (Int, Option[JsObject]) = syncPost(ip, port, \"/run\", value)\n      def runForJsArray(value: JsValue): (Int, Option[JsArray]) = syncPostForJsArray(ip, port, \"/run\", value)\n      def runMultiple(values: Seq[JsValue])(implicit ec: ExecutionContext): Seq[(Int, Option[JsObject])] =\n        concurrentSyncPost(ip, port, \"/run\", values)\n    }\n\n    try {\n      // ...and finally run the code with it.\n      code(mock)\n      // I'm told this is good for the logs.\n      Thread.sleep(100)\n      val (_, out, err) = awaitDocker(s\"logs $name\", 10.seconds)\n      (out, err)\n    } finally {\n      awaitDocker(s\"kill $name\", 10.seconds)\n      awaitDocker(s\"rm $name\", 10.seconds)\n    }\n  }\n\n  private def syncPost(host: String, port: Int, endPoint: String, content: JsValue)(\n    implicit logging: Logging,\n    as: ActorSystem): (Int, Option[JsObject]) = {\n\n    implicit val transid = TransactionId.testing\n\n    org.apache.openwhisk.core.containerpool.PekkoContainerClient.post(host, port, endPoint, content, 30.seconds)\n  }\n\n  private def syncPostForJsArray(host: String, port: Int, endPoint: String, content: JsValue)(\n    implicit logging: Logging,\n    as: ActorSystem): (Int, Option[JsArray]) = {\n\n    implicit val transid = TransactionId.testing\n\n    org.apache.openwhisk.core.containerpool.PekkoContainerClient\n      .postForJsArray(host, port, endPoint, content, 30.seconds)\n  }\n\n  private def concurrentSyncPost(host: String, port: Int, endPoint: String, contents: Seq[JsValue])(\n    implicit logging: Logging,\n    as: ActorSystem): Seq[(Int, Option[JsObject])] = {\n\n    implicit val transid = TransactionId.testing\n\n    org.apache.openwhisk.core.containerpool.PekkoContainerClient\n      .concurrentPost(host, port, endPoint, contents, 30.seconds)\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/actionContainers/BasicActionRunnerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage actionContainers\n\nimport org.junit.runner.RunWith\nimport org.scalatest.exceptions.TestPendingException\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\n@RunWith(classOf[JUnitRunner])\ntrait BasicActionRunnerTests extends ActionProxyContainerTestUtils {\n\n  /**\n   * Must be defined by the runtime test suites. A typical implementation looks like this:\n   *      withContainer(\"container-image-name\", env)(code)\n   * See [[ActionContainer.withContainer]] for details.\n   *\n   * @param env the environment to pass to the container\n   * @param code the code to initialize with\n   */\n  def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit): (String, String)\n\n  /**\n   * Runs tests for actions which receive an empty initializer (no source or exec).\n   *\n   * @param stub true if the proxy provides a stub\n   */\n  def testNoSourceOrExec: TestConfig\n\n  /**\n   * Runs tests for actions which receive an empty code initializer (exec with code equal to the empty string).\n   *\n   * @param stub true if the proxy provides a stub\n   */\n  def testNoSource = testNoSourceOrExec\n\n  /**\n   * Runs tests for actions which do not return a dictionary and confirms expected error messages.\n   *\n   * @param code code to execute, should not return a JSON object\n   * @param main the main function\n   * @param checkResultInLogs should be true iff the result of the action is expected to appear in stdout or stderr\n   */\n  def testNotReturningJson: TestConfig\n\n  /**\n   * Tests that an action will not allow more than one initialization and confirms expected error messages.\n   *\n   * @param code the code to execute, the identity/echo function is sufficient.\n   * @param main the main function\n   */\n  def testInitCannotBeCalledMoreThanOnce: TestConfig\n\n  /**\n   * Tests the echo action for an entry point other than \"main\".\n   *\n   * @param code the code to execute, must be the identity/echo function.\n   * @param main the main function to run, must not be \"main\"\n   */\n  def testEntryPointOtherThanMain: TestConfig\n\n  /**\n   * Tests the echo action for different input parameters.\n   * The test actions must also print hello [stdout, stderr] to the respective streams.\n   *\n   * @param code the code to execute, must be the identity/echo function.\n   * @param main the main function\n   */\n  def testEcho: TestConfig\n\n  /**\n   * Tests a unicode action. The action must properly handle unicode characters in the executable,\n   * receive a unicode character, and construct a response with a unicode character. It must also\n   * emit unicode characters correctly to stdout.\n   *\n   * @param code a function { delimiter } => { winter: \"❄ \" + delimiter + \" ❄\"}\n   * @param main the main function\n   */\n  def testUnicode: TestConfig\n\n  /**\n   * Tests the action constructs the activation context correctly.\n   *\n   * @param code a function returning the activation context consisting of the following dictionary\n   *             { \"api_host\": process.env.__OW_API_HOST,\n   *               \"api_key\": process.env__OW_API_KEY,\n   *               \"namespace\": process.env.__OW_NAMESPACE,\n   *               \"action_name\": process.env.__OW_ACTION_NAME,\n   *               \"action_version\": process.env.__OW_ACTION_VERSION,\n   *               \"activation_id\": process.env.__OW_ACTIVATION_ID,\n   *               \"deadline\": process.env.__OW_DEADLINE\n   *             }\n   * @param main the main function\n   * @param enforceEmptyOutputStream true to check empty stdout stream\n   * @param enforceEmptyErrorStream true to check empty stderr stream\n   */\n  def testEnv: TestConfig\n\n  /**\n   * Tests that action parameters at initialization time are available before an action\n   * is initialized. The value of a parameter is always a String (and may include the empty string).\n   *\n   * @param code a function returning a dictionary consisting of the following properties\n   *             { \"SOME_VAR\" : process.env.SOME_VAR,\n   *               \"ANOTHER_VAR\": process.env.ANOTHER_VAR\n   *             }\n   * @param main the main function\n   */\n  def testEnvParameters: TestConfig = TestConfig(\"\", skipTest = true) // so as not to break downstream dependencies\n\n  /**\n   * Tests the action to confirm it can handle a large parameter (larger than 128K) when using STDIN.\n   *\n   * @param code the identity/echo function.\n   * @param main the main function\n   */\n  def testLargeInput: TestConfig\n\n  behavior of \"runtime proxy\"\n\n  it should \"handle initialization with no code\" in {\n    val config = testNoSource\n\n    val (out, err) = withActionContainer() { c =>\n      val (initCode, out) = c.init(initPayload(\"\", \"\"))\n      if (config.hasCodeStub) {\n        initCode should be(200)\n      } else {\n        initCode should not be (200)\n      }\n    }\n  }\n\n  it should \"handle initialization with no content\" in {\n    val config = testNoSourceOrExec\n\n    val (out, err) = withActionContainer() { c =>\n      val (initCode, out) = c.init(JsObject.empty)\n      if (!config.hasCodeStub) {\n        initCode should not be (200)\n        out should {\n          be(Some(JsObject(\"error\" -> JsString(\"Missing main/no code to execute.\")))) or\n            be(Some(\n              JsObject(\"error\" -> JsString(\"The action failed to generate or locate a binary. See logs for details.\"))))\n        }\n      } else {\n        initCode should be(200)\n      }\n    }\n  }\n\n  it should \"run and report an error for function not returning a json object\" in {\n    val config = testNotReturningJson\n    if (!config.skipTest) { // this may not be possible in types languages\n      val (out, err) = withActionContainer() { c =>\n        val (initCode, _) = c.init(initPayload(config.code, config.main))\n        initCode should be(200)\n        val (runCode, out) = c.run(JsObject.empty)\n        runCode should not be (200)\n\n        out should (be(Some(JsObject(\"error\" -> JsString(\"The action did not return a dictionary or array.\")))) or be(\n          Some(JsObject(\"error\" -> JsString(\"The action did not return a dictionary.\")))))\n      }\n\n      checkStreams(out, err, {\n        case (o, e) =>\n          // some runtimes may emita an error message,\n          // or for native runtimes emit the result to stdout\n          if (config.enforceEmptyOutputStream) o shouldBe empty\n          if (config.enforceEmptyErrorStream) e shouldBe empty\n      })\n    }\n  }\n\n  it should \"fail to initialize a second time\" in {\n    val config = testInitCannotBeCalledMoreThanOnce\n\n    val errorMessage = \"Cannot initialize the action more than once.\"\n\n    val (out, err) = withActionContainer() { c =>\n      val (initCode1, _) = c.init(initPayload(config.code, config.main))\n      initCode1 should be(200)\n\n      val (initCode2, error2) = c.init(initPayload(config.code, config.main))\n      initCode2 should not be (200)\n      error2 should be(Some(JsObject(\"error\" -> JsString(errorMessage))))\n    }\n\n    checkStreams(out, err, {\n      case (o, e) =>\n        (o + e) should include(errorMessage)\n    }, sentinelCount = 0)\n  }\n\n  it should s\"invoke non-standard entry point\" in {\n    val config = testEntryPointOtherThanMain\n    config.main should not be (\"main\")\n\n    val (out, err) = withActionContainer() { c =>\n      val (initCode, _) = c.init(initPayload(config.code, config.main))\n      initCode should be(200)\n\n      val arg = JsObject(\"string\" -> JsString(\"hello\"))\n      val (runCode, out) = c.run(runPayload(arg))\n      runCode should be(200)\n      out should be(Some(arg))\n    }\n\n    checkStreams(out, err, {\n      case (o, e) =>\n        // some native runtimes will emit the result to stdout\n        if (config.enforceEmptyOutputStream) o shouldBe empty\n        e shouldBe empty\n    })\n  }\n\n  it should s\"echo arguments and print message to stdout/stderr\" in {\n    val config = testEcho\n\n    val argss = List(\n      JsObject(\"string\" -> JsString(\"hello\")),\n      JsObject(\"string\" -> JsString(\"❄ ☃ ❄\")),\n      JsObject(\"numbers\" -> JsArray(JsNumber(42), JsNumber(1))),\n      // JsObject(\"boolean\" -> JsTrue), // fails with swift3 returning boolean: 1\n      JsObject(\"object\" -> JsObject(\"a\" -> JsString(\"A\"))))\n\n    val (out, err) = withActionContainer() { c =>\n      val (initCode, _) = c.init(initPayload(config.code, config.main))\n      initCode should be(200)\n\n      for (args <- argss) {\n        val (runCode, out) = c.run(runPayload(args))\n        runCode should be(200)\n        out should be(Some(args))\n      }\n    }\n\n    checkStreams(out, err, {\n      case (o, e) =>\n        o should include(\"hello stdout\")\n        // some languages may not support printing to stderr\n        if (!config.skipTest) e should include(\"hello stderr\")\n    }, argss.length)\n  }\n\n  it should s\"handle unicode in source, input params, logs, and result\" in {\n    val config = testUnicode\n\n    val (out, err) = withActionContainer() { c =>\n      val (initCode, _) = c.init(initPayload(config.code, config.main))\n      initCode should be(200)\n\n      val (runCode, runRes) = c.run(runPayload(JsObject(\"delimiter\" -> JsString(\"❄\"))))\n      runRes.get.fields.get(\"winter\") shouldBe Some(JsString(\"❄ ☃ ❄\"))\n    }\n\n    checkStreams(out, err, {\n      case (o, _) =>\n        o.toLowerCase should include(\"❄ ☃ ❄\")\n    })\n  }\n\n  it should s\"export environment variables before initialization\" in {\n    val config = testEnvParameters\n\n    if (config.skipTest) {\n      throw new TestPendingException\n    } else {\n      val env = Map(\"SOME_VAR\" -> JsString(\"xyz\"), \"ANOTHER_VAR\" -> JsString.empty)\n\n      val (out, err) = withActionContainer() { c =>\n        val (initCode, _) = c.init(initPayload(config.code, config.main, Some(env)))\n        initCode should be(200)\n\n        val (runCode, out) = c.run(runPayload(JsObject.empty))\n        runCode should be(200)\n        out shouldBe defined\n        val fields = out.get.fields\n        fields(\"SOME_VAR\") shouldBe JsString(\"xyz\")\n        fields(\"ANOTHER_VAR\") shouldBe JsString(\"\")\n      }\n\n      checkStreams(out, err, {\n        case (o, e) =>\n          if (config.enforceEmptyOutputStream) o shouldBe empty\n          if (config.enforceEmptyErrorStream) e shouldBe empty\n      })\n    }\n  }\n\n  it should s\"confirm expected environment variables\" in {\n    val config = testEnv\n\n    val props = Seq(\n      \"api_host\" -> \"xyz\",\n      \"api_key\" -> \"abc\",\n      \"namespace\" -> \"zzz\",\n      \"action_name\" -> \"xxx\",\n      \"action_version\" -> \"0.0.1\",\n      \"activation_id\" -> \"iii\",\n      \"deadline\" -> \"123\")\n\n    val env = props.map { case (k, v) => s\"__OW_${k.toUpperCase()}\" -> v }\n\n    // the api host is sent as a docker run environment parameter\n    val (out, err) = withActionContainer(env.take(1).toMap) { c =>\n      val (initCode, _) = c.init(initPayload(config.code, config.main))\n      initCode should be(200)\n\n      // we omit the api host from the run payload so the docker run env var is used\n      val (runCode, out) = c.run(runPayload(JsObject.empty, Some(props.drop(1).toMap.toJson.asJsObject)))\n      runCode should be(200)\n      out shouldBe defined\n      props.map {\n        case (k, v) =>\n          withClue(k) {\n            out.get.fields(k) shouldBe JsString(v)\n          }\n\n      }\n    }\n\n    checkStreams(out, err, {\n      case (o, e) =>\n        if (config.enforceEmptyOutputStream) o shouldBe empty\n        if (config.enforceEmptyErrorStream) e shouldBe empty\n    })\n  }\n\n  it should s\"echo a large input\" in {\n    val config = testLargeInput\n    if (config.skipTest) {\n      throw new TestPendingException\n    } else {\n      var passed = true\n\n      val (out, err) = withActionContainer() { c =>\n        val (initCode, _) = c.init(initPayload(config.code, config.main))\n        initCode should be(200)\n\n        val arg = JsObject(\"arg\" -> JsString((\"a\" * 1048561)))\n        val (_, runRes) = c.run(runPayload(arg))\n        if (runRes.get != arg) {\n          println(s\"result did not match: ${runRes.get}\")\n          passed = false\n        }\n      }\n\n      if (!passed) {\n        println(out)\n        println(err)\n        assert(false)\n      }\n    }\n  }\n\n  case class TestConfig(code: String,\n                        main: String = \"main\",\n                        enforceEmptyOutputStream: Boolean = true,\n                        enforceEmptyErrorStream: Boolean = true,\n                        hasCodeStub: Boolean = false,\n                        skipTest: Boolean = false)\n}\n"
  },
  {
    "path": "tests/src/test/scala/actionContainers/ResourceHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage actionContainers\n\nimport java.net.URI\nimport java.net.URLClassLoader\nimport java.nio.file.Files\nimport java.nio.file.Path\nimport java.nio.file.Paths\nimport java.nio.file.SimpleFileVisitor\nimport java.nio.file.FileVisitResult\nimport java.nio.file.FileSystems\nimport java.nio.file.attribute.BasicFileAttributes\nimport java.nio.charset.StandardCharsets\nimport java.util.Base64\n\nimport javax.tools.ToolProvider\n\nimport collection.JavaConverters._\n\n/**\n * A collection of utility objects to create ephemeral action resources based\n *  on file contents.\n */\nobject ResourceHelpers {\n\n  /** Creates a zip file based on the contents of a top-level directory. */\n  object ZipBuilder {\n    def mkBase64Zip(sources: Seq[(Seq[String], String)]): String = {\n      val (tmpDir, _) = writeSourcesToTempDirectory(sources)\n      val archive = makeZipFromDir(tmpDir)\n      readAsBase64(archive)\n    }\n  }\n\n  /**\n   * A convenience object to compile and package Java sources into a JAR, and to\n   * encode that JAR as a base 64 string. The compilation options include the\n   * current classpath, which is why Google GSON is readily available (though not\n   * packaged in the JAR).\n   */\n  object JarBuilder {\n    def mkBase64Jar(sources: Seq[(Seq[String], String)]): String = {\n      // Note that this pipeline doesn't delete any of the temporary files.\n      val binDir = compile(sources)\n      val jarPath = makeJarFromDir(binDir)\n      val base64 = readAsBase64(jarPath)\n      base64\n    }\n\n    def mkBase64Jar(source: (Seq[String], String)): String = {\n      mkBase64Jar(Seq(source))\n    }\n\n    private def compile(sources: Seq[(Seq[String], String)]): Path = {\n      require(!sources.isEmpty)\n\n      // The absolute paths of the source file\n      val (srcDir, srcAbsPaths) = writeSourcesToTempDirectory(sources)\n\n      // A temporary directory for the destination files.\n      val binDir = Files.createTempDirectory(\"bin\").toAbsolutePath()\n\n      // Preparing the compiler\n      val compiler = ToolProvider.getSystemJavaCompiler()\n      val fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)\n\n      // Collecting all files to be compiled\n      val compUnit = fileManager.getJavaFileObjectsFromFiles(srcAbsPaths.map(_.toFile).asJava)\n\n      // Setting the options\n      val compOptions = Seq(\"-d\", binDir.toAbsolutePath().toString(), \"-classpath\", buildClassPath())\n      val compTask = compiler.getTask(null, fileManager, null, compOptions.asJava, null, compUnit)\n\n      // ...and off we go.\n      compTask.call()\n\n      binDir\n    }\n\n    private def buildClassPath(): String = {\n      val bcp = System.getProperty(\"java.class.path\")\n\n      val list = this.getClass().getClassLoader() match {\n        case ucl: URLClassLoader =>\n          bcp :: ucl.getURLs().map(_.getFile().toString()).toList\n\n        case _ =>\n          List(bcp)\n      }\n\n      list.mkString(System.getProperty(\"path.separator\"))\n    }\n  }\n\n  /**\n   * Creates a temporary directory and reproduces the desired file structure\n   * in it. Returns the path of the temporary directory and the path of each\n   * file as represented in it.\n   */\n  private def writeSourcesToTempDirectory(sources: Seq[(Seq[String], String)]): (Path, Seq[Path]) = {\n    // A temporary directory for the source files.\n    val srcDir = Files.createTempDirectory(\"src\").toAbsolutePath()\n\n    val srcAbsPaths = for ((sourceName, sourceContent) <- sources) yield {\n      // The relative path of the source file\n      val srcRelPath = Paths.get(sourceName.head, sourceName.tail: _*)\n      // The absolute path of the source file\n      val srcAbsPath = srcDir.resolve(srcRelPath)\n      // Create parent directories if needed.\n      Files.createDirectories(srcAbsPath.getParent)\n      // Writing contents\n      Files.write(srcAbsPath, sourceContent.getBytes(StandardCharsets.UTF_8))\n\n      srcAbsPath\n    }\n\n    (srcDir, srcAbsPaths)\n  }\n\n  private def makeZipFromDir(dir: Path): Path = makeArchiveFromDir(dir, \".zip\")\n\n  private def makeJarFromDir(dir: Path): Path = makeArchiveFromDir(dir, \".jar\")\n\n  /**\n   * Compresses all files beyond a directory into a zip file.\n   * Note that Jar files are just zip files.\n   */\n  private def makeArchiveFromDir(dir: Path, extension: String): Path = {\n    // Any temporary file name for the archive.\n    val arPath = Files.createTempFile(\"output\", extension).toAbsolutePath()\n\n    // We \"mount\" it as a filesystem, so we can just copy files into it.\n    val dstUri = new URI(\"jar:\" + arPath.toUri().getScheme(), arPath.toAbsolutePath().toString(), null)\n    // OK, that's a hack. Doing this because newFileSystem wants to create that file.\n    arPath.toFile().delete()\n    val fs = FileSystems.newFileSystem(dstUri, Map((\"create\" -> \"true\")).asJava)\n\n    // Traversing all files in the bin directory...\n    Files.walkFileTree(\n      dir,\n      new SimpleFileVisitor[Path]() {\n        override def visitFile(path: Path, attributes: BasicFileAttributes) = {\n          // The path relative to the src dir\n          val relPath = dir.relativize(path)\n\n          // The corresponding path in the zip\n          val arRelPath = fs.getPath(relPath.toString())\n\n          // If this file is not top-level in the src dir...\n          if (relPath.getParent() != null) {\n            // ...create the directory structure if it doesn't exist.\n            if (!Files.exists(arRelPath.getParent())) {\n              Files.createDirectories(arRelPath.getParent())\n            }\n          }\n\n          // Finally we can copy that file.\n          Files.copy(path, arRelPath)\n\n          FileVisitResult.CONTINUE\n        }\n      })\n\n    fs.close()\n\n    arPath\n  }\n\n  /** Reads the contents of a (possibly binary) file into a base64-encoded String */\n  def readAsBase64(path: Path): String = {\n    val encoder = Base64.getEncoder()\n    new String(encoder.encode(Files.readAllBytes(path)), StandardCharsets.UTF_8)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage apigw.healthtests\n\nimport java.io.BufferedWriter\nimport java.io.File\nimport java.io.FileWriter\n\nimport scala.concurrent.duration.DurationInt\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport common.TestHelpers\nimport common.TestUtils\nimport common.TestUtils._\nimport common.WskOperations\nimport common.WskProps\nimport common.WskTestHelpers\nimport io.restassured.RestAssured\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport system.rest.RestUtil\n\n/**\n * Basic tests of the download link for Go CLI binaries\n */\n@RunWith(classOf[JUnitRunner])\nabstract class ApiGwEndToEndTests\n    extends AnyFlatSpec\n    with Matchers\n    with RestUtil\n    with TestHelpers\n    with WskTestHelpers\n    with BeforeAndAfterAll {\n\n  implicit val wskprops: common.WskProps = WskProps()\n  val wsk: WskOperations\n  lazy val namespace: String = wsk.namespace.whois()\n  val createCode: Int\n\n  // Custom CLI properties file\n  val cliWskPropsFile: java.io.File = File.createTempFile(\"wskprops\", \".tmp\")\n\n  /*\n   * Create a CLI properties file for use by the tests\n   */\n  override def beforeAll: Unit = {\n    cliWskPropsFile.deleteOnExit()\n    val wskprops = WskProps(token = \"SOME TOKEN\")\n    wskprops.writeFile(cliWskPropsFile)\n    println(s\"wsk temporary props file created here: ${cliWskPropsFile.getCanonicalPath()}\")\n  }\n\n  def verifyAPICreated(rr: RunResult): Unit = {\n    rr.stdout should include(\"ok: created API\")\n    val apiurl = rr.stdout.split(\"\\n\")(1)\n    println(s\"apiurl: '$apiurl'\")\n  }\n\n  def verifyAPIList(rr: RunResult,\n                    actionName: String,\n                    testurlop: String,\n                    testapiname: String,\n                    testbasepath: String,\n                    testrelpath: String): Unit = {\n    rr.stdout should include(\"ok: APIs\")\n    rr.stdout should include regex (s\"$actionName\\\\s+$testurlop\\\\s+$testapiname\\\\s+\")\n    rr.stdout should include(testbasepath + testrelpath)\n  }\n\n  def verifyAPISwaggerCreated(rr: RunResult): Unit = {\n    rr.stdout should include(\"ok: created API\")\n  }\n\n  def writeSwaggerFile(rr: RunResult): File = {\n    val swaggerfile = File.createTempFile(\"api\", \".json\")\n    swaggerfile.deleteOnExit()\n    val bw = new BufferedWriter(new FileWriter(swaggerfile))\n    bw.write(rr.stdout)\n    bw.close()\n    swaggerfile\n  }\n\n  def getSwaggerApiUrl(rr: RunResult): String = rr.stdout.split(\"\\n\")(1)\n\n  behavior of \"Wsk api\"\n\n  it should s\"create an API and successfully invoke that API\" in {\n    val testName = \"APIGW_HEALTHTEST1\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_echo\"\n    val urlqueryparam = \"name\"\n    val urlqueryvalue = testName\n\n    try {\n      println(\"Namespace: \" + namespace)\n\n      // Delete any lingering stale api from previous run that may not have been deleted properly\n      wsk.api.delete(\n        basepathOrApiName = testbasepath,\n        expectedExitCode = DONTCARE_EXIT,\n        cliCfgFile = Some(cliWskPropsFile.getCanonicalPath()))\n\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo-web-http.js\")\n      println(\"action creation Namespace: \" + namespace)\n      wsk.action.create(\n        name = actionName,\n        artifact = Some(file),\n        expectedExitCode = createCode,\n        annotations = Map(\"web-export\" -> true.toJson))\n\n      println(\"creation Namespace: \" + namespace)\n      // Create the API\n      var rr = wsk.api.create(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname),\n        responsetype = Some(\"http\"),\n        cliCfgFile = Some(cliWskPropsFile.getCanonicalPath()))\n      verifyAPICreated(rr)\n\n      // Validate the API was successfully created\n      // List result will look like:\n      // ok: APIs\n      // Action                            Verb             API Name  URL\n      // /_//whisk.system/utils/echo          get  APIGW_HEALTHTEST1 API Name  http://172.17.0.1:9001/api/ab9082cd-ea8e-465a-8a65-b491725cc4ef/APIGW_HEALTHTEST1_bp/path\n      rr = wsk.api.list(\n        basepathOrApiName = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        cliCfgFile = Some(cliWskPropsFile.getCanonicalPath()))\n      verifyAPIList(rr, actionName, testurlop, testapiname, testbasepath, testrelpath)\n\n      // Recreate the API using a JSON swagger file\n      rr = wsk.api.get(basepathOrApiName = Some(testbasepath), cliCfgFile = Some(cliWskPropsFile.getCanonicalPath()))\n      val swaggerfile = writeSwaggerFile(rr)\n\n      // Delete API to that it can be recreated again using the generated swagger file\n      val deleteApiResult = wsk.api.delete(\n        basepathOrApiName = testbasepath,\n        expectedExitCode = DONTCARE_EXIT,\n        cliCfgFile = Some(cliWskPropsFile.getCanonicalPath()))\n\n      // Create the API again, but use the swagger file this time\n      rr = wsk.api\n        .create(swagger = Some(swaggerfile.getAbsolutePath()), cliCfgFile = Some(cliWskPropsFile.getCanonicalPath()))\n      verifyAPISwaggerCreated(rr)\n      val swaggerapiurl = getSwaggerApiUrl(rr)\n      println(s\"Returned api url: '${swaggerapiurl}'\")\n\n      // Call the API URL and validate the results\n      val start = java.lang.System.currentTimeMillis\n      val apiToInvoke = s\"$swaggerapiurl?$urlqueryparam=$urlqueryvalue&guid=$start\"\n      println(s\"Invoking: '${apiToInvoke}'\")\n      val response = org.apache.openwhisk.utils.retry({\n        val response = RestAssured.given().config(sslconfig).get(s\"$apiToInvoke\")\n        println(\"URL invocation response status: \" + response.statusCode)\n        response.statusCode should be(200)\n        response\n      }, 6, Some(2.second))\n      val end = java.lang.System.currentTimeMillis\n      val elapsed = end - start\n      println(\"Elapsed time (milliseconds) for a successful response: \" + elapsed)\n      val responseString = response.body.asString\n      println(\"URL invocation response: \" + responseString)\n      responseString.parseJson.asJsObject.fields(urlqueryparam).convertTo[String] should be(urlqueryvalue)\n\n    } finally {\n      println(\"Deleting action: \" + actionName)\n      val finallydeleteActionResult = wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      println(\"Deleting API: \" + testbasepath)\n      val finallydeleteApiResult = wsk.api.delete(\n        basepathOrApiName = testbasepath,\n        expectedExitCode = DONTCARE_EXIT,\n        cliCfgFile = Some(cliWskPropsFile.getCanonicalPath()))\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/apigw/healthtests/ApiGwRestEndToEndTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage apigw.healthtests\n\nimport java.io.BufferedWriter\nimport java.io.File\nimport java.io.FileWriter\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.TestUtils._\nimport common.rest.WskRestOperations\nimport common.rest.RestResult\nimport common.WskActorSystem\n\n@RunWith(classOf[JUnitRunner])\nclass ApiGwRestEndToEndTests extends ApiGwEndToEndTests with WskActorSystem {\n\n  override lazy val wsk = new WskRestOperations\n  override val createCode = OK.intValue\n\n  override def verifyAPICreated(rr: RunResult): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    apiResultRest.statusCode shouldBe OK\n    val apiurl = apiResultRest.getField(\"gwApiUrl\") + \"/path\"\n    println(s\"apiurl: '$apiurl'\")\n  }\n\n  override def verifyAPIList(rr: RunResult,\n                             actionName: String,\n                             testurlop: String,\n                             testapiname: String,\n                             testbasepath: String,\n                             testrelpath: String): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject(\"apis\")(0), \"value\")\n    val apidoc = RestResult.getFieldJsObject(apiValue, \"apidoc\")\n    val basepath = RestResult.getField(apidoc, \"basePath\")\n    basepath shouldBe testbasepath\n\n    val paths = RestResult.getFieldJsObject(apidoc, \"paths\")\n    paths.fields.contains(testrelpath) shouldBe true\n\n    val info = RestResult.getFieldJsObject(apidoc, \"info\")\n    val title = RestResult.getField(info, \"title\")\n    title shouldBe testapiname\n\n    val relpath = RestResult.getFieldJsObject(paths, testrelpath)\n    val urlop = RestResult.getFieldJsObject(relpath, testurlop)\n    val openwhisk = RestResult.getFieldJsObject(urlop, \"x-openwhisk\")\n    val actionN = RestResult.getField(openwhisk, \"action\")\n    actionN shouldBe actionName\n  }\n\n  override def verifyAPISwaggerCreated(rr: RunResult): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    apiResultRest.statusCode shouldBe OK\n  }\n\n  override def writeSwaggerFile(rr: RunResult): File = {\n    val swaggerfile = File.createTempFile(\"api\", \".json\")\n    swaggerfile.deleteOnExit()\n    val bw = new BufferedWriter(new FileWriter(swaggerfile))\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject(\"apis\")(0), \"value\")\n    val apidoc = RestResult.getFieldJsObject(apiValue, \"apidoc\")\n    bw.write(apidoc.toString())\n    bw.close()\n    swaggerfile\n  }\n\n  override def getSwaggerApiUrl(rr: RunResult): String = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    apiResultRest.getField(\"gwApiUrl\") + \"/path\"\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/ConcurrencyHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext, Future}\n\ntrait ConcurrencyHelpers {\n  def concurrently[T](times: Int, timeout: FiniteDuration)(op: => T)(implicit ec: ExecutionContext): Iterable[T] =\n    Await.result(Future.sequence((1 to times).map(_ => Future(op))), timeout)\n\n  def concurrently[B, T](over: Iterable[B], timeout: FiniteDuration)(op: B => T)(\n    implicit ec: ExecutionContext): Iterable[T] =\n    Await.result(Future.sequence(over.map(v => Future(op(v)))), timeout)\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/FreePortFinder.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport java.net.ServerSocket\n\n/**\n * Utility to find a free port such that any launched service can be configured to use that.\n * This helps in ensuring that test do not fail due to conflict with any existing service using a standard port\n */\nobject FreePortFinder {\n\n  def freePort(): Int = {\n    val socket = new ServerSocket(0)\n    try socket.getLocalPort\n    finally if (socket != null) socket.close()\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/JsHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// This is a copy of the JsHelpers as a trait to allow\n// catalog tests to still work until they are migrated\npackage common\n\nimport spray.json.JsObject\nimport spray.json.JsValue\n\n/**\n * @deprecated Use {@link whisk.common.JsHelpers} instead.\n */\n@Deprecated\ntrait JsHelpers {\n  implicit class JsObjectHelper(js: JsObject) {\n    def getFieldPath(path: String*): Option[JsValue] = {\n      org.apache.openwhisk.utils.JsHelpers.getFieldPath(js, path.toList)\n    }\n\n    def fieldPathExists(path: String*): Boolean = {\n      org.apache.openwhisk.utils.JsHelpers.fieldPathExists(js, path.toList)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/LoggedFunction.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport scala.collection.mutable\n\n/**\n * Extensions for Functions to provide introspection of their respective calls.\n *\n * Use like:\n *     val func = LoggedFunction { (a, b) => a + b }\n *     func(1, 2)\n *     func.calls should have size 1\n *     func.calls.head shouldBe (1, 2)\n */\nclass LoggedFunction1[A1, B](body: A1 => B) extends Function1[A1, B] {\n  val calls = mutable.Buffer[A1]()\n\n  override def apply(v1: A1): B = {\n    calls += (v1)\n    body(v1)\n  }\n}\n\nclass LoggedFunction2[A1, A2, B](body: (A1, A2) => B) extends Function2[A1, A2, B] {\n  val calls = mutable.Buffer[(A1, A2)]()\n\n  override def apply(v1: A1, v2: A2): B = {\n    calls += ((v1, v2))\n    body(v1, v2)\n  }\n}\n\nclass LoggedFunction3[A1, A2, A3, B](body: (A1, A2, A3) => B) extends Function3[A1, A2, A3, B] {\n  val calls = mutable.Buffer[(A1, A2, A3)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3): B = {\n    calls += ((v1, v2, v3))\n    body(v1, v2, v3)\n  }\n}\n\nclass LoggedFunction4[A1, A2, A3, A4, B](body: (A1, A2, A3, A4) => B) extends Function4[A1, A2, A3, A4, B] {\n  val calls = mutable.Buffer[(A1, A2, A3, A4)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3, v4: A4): B = {\n    calls += ((v1, v2, v3, v4))\n    body(v1, v2, v3, v4)\n  }\n}\n\nclass LoggedFunction5[A1, A2, A3, A4, A5, B](body: (A1, A2, A3, A4, A5) => B) extends Function5[A1, A2, A3, A4, A5, B] {\n  val calls = mutable.Buffer[(A1, A2, A3, A4, A5)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3, v4: A4, v5: A5): B = {\n    calls += ((v1, v2, v3, v4, v5))\n    body(v1, v2, v3, v4, v5)\n  }\n}\nclass LoggedFunction6[A1, A2, A3, A4, A5, A6, B](body: (A1, A2, A3, A4, A5, A6) => B)\n    extends Function6[A1, A2, A3, A4, A5, A6, B] {\n  val calls = mutable.Buffer[(A1, A2, A3, A4, A5, A6)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3, v4: A4, v5: A5, v6: A6): B = {\n    calls += ((v1, v2, v3, v4, v5, v6))\n    body(v1, v2, v3, v4, v5, v6)\n  }\n}\n\nclass LoggedFunction7[A1, A2, A3, A4, A5, A6, A7, B](body: (A1, A2, A3, A4, A5, A6, A7) => B)\n    extends Function7[A1, A2, A3, A4, A5, A6, A7, B] {\n  val calls = mutable.Buffer[(A1, A2, A3, A4, A5, A6, A7)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3, v4: A4, v5: A5, v6: A6, v7: A7): B = {\n    calls += ((v1, v2, v3, v4, v5, v6, v7))\n    body(v1, v2, v3, v4, v5, v6, v7)\n  }\n}\n\nclass LoggedFunction8[A1, A2, A3, A4, A5, A6, A7, A8, B](body: (A1, A2, A3, A4, A5, A6, A7, A8) => B)\n    extends Function8[A1, A2, A3, A4, A5, A6, A7, A8, B] {\n  val calls = mutable.Buffer[(A1, A2, A3, A4, A5, A6, A7, A8)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3, v4: A4, v5: A5, v6: A6, v7: A7, v8: A8): B = {\n    calls += ((v1, v2, v3, v4, v5, v6, v7, v8))\n    body(v1, v2, v3, v4, v5, v6, v7, v8)\n  }\n}\n\nclass SynchronizedLoggedFunction1[A1, B](body: A1 => B) extends Function1[A1, B] {\n  val calls = mutable.Buffer[A1]()\n\n  override def apply(v1: A1): B = {\n    calls.synchronized(calls += (v1))\n    body(v1)\n  }\n}\n\nclass SynchronizedLoggedFunction2[A1, A2, B](body: (A1, A2) => B) extends Function2[A1, A2, B] {\n  val calls = mutable.Buffer[(A1, A2)]()\n\n  override def apply(v1: A1, v2: A2): B = {\n    calls.synchronized(calls += ((v1, v2)))\n    body(v1, v2)\n  }\n}\n\nclass SynchronizedLoggedFunction3[A1, A2, A3, B](body: (A1, A2, A3) => B) extends Function3[A1, A2, A3, B] {\n  val calls = mutable.Buffer[(A1, A2, A3)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3): B = {\n    calls.synchronized(calls += ((v1, v2, v3)))\n    body(v1, v2, v3)\n  }\n}\n\nclass SynchronizedLoggedFunction4[A1, A2, A3, A4, B](body: (A1, A2, A3, A4) => B) extends Function4[A1, A2, A3, A4, B] {\n  val calls = mutable.Buffer[(A1, A2, A3, A4)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3, v4: A4): B = {\n    calls.synchronized(calls += ((v1, v2, v3, v4)))\n    body(v1, v2, v3, v4)\n  }\n}\n\nclass SynchronizedLoggedFunction5[A1, A2, A3, A4, A5, B](body: (A1, A2, A3, A4, A5) => B)\n    extends Function5[A1, A2, A3, A4, A5, B] {\n  val calls = mutable.Buffer[(A1, A2, A3, A4, A5)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3, v4: A4, v5: A5): B = {\n    calls.synchronized(calls += ((v1, v2, v3, v4, v5)))\n    body(v1, v2, v3, v4, v5)\n  }\n}\nclass SynchronizedLoggedFunction6[A1, A2, A3, A4, A5, A6, B](body: (A1, A2, A3, A4, A5, A6) => B)\n    extends Function6[A1, A2, A3, A4, A5, A6, B] {\n  val calls = mutable.Buffer[(A1, A2, A3, A4, A5, A6)]()\n\n  override def apply(v1: A1, v2: A2, v3: A3, v4: A4, v5: A5, v6: A6): B = {\n    calls.synchronized(calls += ((v1, v2, v3, v4, v5, v6)))\n    body(v1, v2, v3, v4, v5, v6)\n  }\n}\n\nobject LoggedFunction {\n  def apply[A1, B](body: (A1) => B) = new LoggedFunction1[A1, B](body)\n  def apply[A1, A2, B](body: (A1, A2) => B) = new LoggedFunction2[A1, A2, B](body)\n  def apply[A1, A2, A3, B](body: (A1, A2, A3) => B) = new LoggedFunction3[A1, A2, A3, B](body)\n  def apply[A1, A2, A3, A4, B](body: (A1, A2, A3, A4) => B) = new LoggedFunction4[A1, A2, A3, A4, B](body)\n  def apply[A1, A2, A3, A4, A5, B](body: (A1, A2, A3, A4, A5) => B) = new LoggedFunction5[A1, A2, A3, A4, A5, B](body)\n  def apply[A1, A2, A3, A4, A5, A6, B](body: (A1, A2, A3, A4, A5, A6) => B) =\n    new LoggedFunction6[A1, A2, A3, A4, A5, A6, B](body)\n  def apply[A1, A2, A3, A4, A5, A6, A7, B](body: (A1, A2, A3, A4, A5, A6, A7) => B) =\n    new LoggedFunction7[A1, A2, A3, A4, A5, A6, A7, B](body)\n  def apply[A1, A2, A3, A4, A5, A6, A7, A8, B](body: (A1, A2, A3, A4, A5, A6, A7, A8) => B) =\n    new LoggedFunction8[A1, A2, A3, A4, A5, A6, A7, A8, B](body)\n}\n\nobject SynchronizedLoggedFunction {\n  def apply[A1, B](body: (A1) => B) = new SynchronizedLoggedFunction1[A1, B](body)\n  def apply[A1, A2, B](body: (A1, A2) => B) = new SynchronizedLoggedFunction2[A1, A2, B](body)\n  def apply[A1, A2, A3, B](body: (A1, A2, A3) => B) = new SynchronizedLoggedFunction3[A1, A2, A3, B](body)\n  def apply[A1, A2, A3, A4, B](body: (A1, A2, A3, A4) => B) = new SynchronizedLoggedFunction4[A1, A2, A3, A4, B](body)\n  def apply[A1, A2, A3, A4, A5, B](body: (A1, A2, A3, A4, A5) => B) =\n    new SynchronizedLoggedFunction5[A1, A2, A3, A4, A5, B](body)\n  def apply[A1, A2, A3, A4, A5, A6, B](body: (A1, A2, A3, A4, A5, A6) => B) =\n    new SynchronizedLoggedFunction6[A1, A2, A3, A4, A5, A6, B](body)\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/Pair.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common;\n\npublic class Pair {\n    public final String fst;\n    public final String snd;\n\n    public Pair(String a, String b) {\n        this.fst = a;\n        this.snd = b;\n    }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/RunCliCmd.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport java.io.File\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable.Buffer\nimport org.scalatest.matchers.should.Matchers\nimport TestUtils._\nimport scala.concurrent.duration._\nimport scala.collection.mutable\n\ntrait RunCliCmd extends Matchers {\n\n  /**\n   * The base command to run. This returns a new mutable buffer, intended for building the rest of the command line.\n   */\n  def baseCommand: Buffer[String]\n\n  val prohibitAuthOverride = false\n\n  /**\n   * Delegates execution of the command to an underlying implementation.\n   *\n   * @param expectedExitCode the expected exit code\n   * @param dir the working directory\n   * @param env an environment for the command\n   * @param fileStdin argument file to redirect to stdin (optional)\n   * @param params parameters to pass on the command line\n   * @return an instance of RunResult\n   */\n  def runCmd(expectedExitCode: Int,\n             dir: File,\n             env: Map[String, String],\n             fileStdin: Option[File],\n             params: Seq[String]): RunResult = {\n    TestUtils.runCmd(expectedExitCode, dir, TestUtils.logger, env.asJava, fileStdin.getOrElse(null), params: _*)\n  }\n\n  /**\n   * Runs a command wsk [params] where the arguments come in as a sequence.\n   *\n   * @return RunResult which contains stdout, stderr, exit code\n   */\n  def cli(params: Seq[String],\n          expectedExitCode: Int = SUCCESS_EXIT,\n          verbose: Boolean = false,\n          env: Map[String, String] = Map(\"WSK_CONFIG_FILE\" -> \"\"),\n          workingDir: File = new File(\".\"),\n          stdinFile: Option[File] = None,\n          showCmd: Boolean = false,\n          hideFromOutput: Seq[String] = Seq.empty,\n          retriesOnError: Int = 3): RunResult = {\n    require(retriesOnError >= 0, \"retry count on network error must not be negative\")\n\n    val args = baseCommand\n    if (verbose) args += \"--verbose\"\n    val finalParams = if (!prohibitAuthOverride) { params } else {\n      params.filter(s =>\n        !s.equals(\"--auth\") && !(params.indexOf(s) > 0 && params(params.indexOf(s) - 1).equals(\"--auth\")))\n    }\n    args.appendAll(finalParams)\n    if (showCmd) println(args.mkString(\" \"))\n\n    val rr =\n      retry(0, retriesOnError, () => runCmd(DONTCARE_EXIT, workingDir, sys.env ++ env, stdinFile, args.toSeq))\n\n    withClue(hideStr(reportFailure(args, expectedExitCode, rr).toString(), hideFromOutput)) {\n      if (expectedExitCode != TestUtils.DONTCARE_EXIT) {\n        val ok = (rr.exitCode == expectedExitCode) || (expectedExitCode == TestUtils.ANY_ERROR_EXIT && rr.exitCode != 0)\n        if (!ok) {\n          rr.exitCode shouldBe expectedExitCode\n        }\n      }\n    }\n\n    rr\n  }\n\n  /** Retries cmd on network error exit. */\n  private def retry(i: Int, N: Int, cmd: () => RunResult): RunResult = {\n    val rr = cmd()\n    if (rr.exitCode != SUCCESS_EXIT && i < N) {\n      Thread.sleep(1.second.toMillis)\n      println(s\"command will retry to due to network error: $rr\")\n      retry(i + 1, N, cmd)\n    } else rr\n  }\n\n  /**\n   * Takes a string and a list of sensitive strings. Any sensistive string found in\n   * the target string will be replaced with \"XXXXX\", returning the processed string.\n   */\n  private def hideStr(str: String, hideThese: Seq[String]): String = {\n    // Iterate through each string to hide, replacing it in the target string (str)\n    hideThese.fold(str)((updatedStr, replaceThis) => updatedStr.replace(replaceThis, \"XXXXX\"))\n  }\n\n  private def reportFailure(args: Buffer[String], ec: Integer, rr: RunResult) = {\n    val s = new StringBuilder()\n    s.append(args.mkString(\" \") + \"\\n\")\n    if (rr.stdout.nonEmpty) s.append(rr.stdout + \"\\n\")\n    if (rr.stderr.nonEmpty) s.append(rr.stderr)\n    s.append(\"exit code:\")\n  }\n}\n\nobject WskAdmin {\n  val wskadmin = new RunCliCmd {\n    override def baseCommand: mutable.Buffer[String] = WskAdmin.baseCommand\n  }\n\n  private val binDir = WhiskProperties.getFileRelativeToWhiskHome(\"bin\")\n  private val binaryName = \"wskadmin\"\n\n  def exists = {\n    val dir = binDir\n    val exec = new File(dir, binaryName)\n    assert(dir.exists, s\"did not find $dir\")\n    assert(exec.exists, s\"did not find $exec\")\n  }\n\n  def baseCommand = {\n    Buffer(WhiskProperties.python, new File(binDir, binaryName).toString)\n  }\n\n  def listKeys(namespace: String, pick: Integer = 1): List[(String, String)] = {\n    wskadmin\n      .cli(Seq(\"user\", \"list\", namespace, \"--pick\", pick.toString))\n      .stdout\n      .split(\"\\n\")\n      .map(\"\"\"\\s+\"\"\".r.split(_))\n      .map(parts => (parts(0), parts(1)))\n      .toList\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/SimpleExec.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport org.apache.openwhisk.common.{Logging, TransactionId}\n\nimport scala.sys.process.{stringSeqToProcess, ProcessLogger}\n\n/**\n * Utility to exec processes\n */\nobject SimpleExec {\n\n  /**\n   * Runs a external process.\n   *\n   * @param cmd an array of String -- their concatenation is the command to exec\n   * @return a triple of (stdout, stderr, exitcode) from running the command\n   */\n  def syncRunCmd(cmd: Seq[String])(implicit transid: TransactionId, logging: Logging): (String, String, Int) = {\n    logging.info(this, s\"Running command: ${cmd.mkString(\" \")}\")\n    val pb = stringSeqToProcess(cmd)\n\n    val outs = new StringBuilder()\n    val errs = new StringBuilder()\n\n    val exitCode = pb ! ProcessLogger(outStr => {\n      outs.append(outStr)\n      outs.append(\"\\n\")\n    }, errStr => {\n      errs.append(errStr)\n      errs.append(\"\\n\")\n    })\n\n    logging.debug(this, s\"Done running command: ${cmd.mkString(\" \")}\")\n\n    def noLastNewLine(sb: StringBuilder) = {\n      if (sb.isEmpty) \"\" else sb.substring(0, sb.size - 1)\n    }\n\n    (noLastNewLine(outs), noLastNewLine(errs), exitCode)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/StreamLogging.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport java.io.ByteArrayOutputStream\nimport java.io.PrintStream\n\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.common.PrintStreamLogging\nimport java.nio.charset.StandardCharsets\n\n/**\n * Logging facility, that can be used by tests.\n *\n * It contains the implicit Logging-instance, that is needed implicitly for some methods and classes.\n * the logger logs to the stream, that can be accessed from your test, to check if a specific message has been written.\n */\ntrait StreamLogging {\n  lazy val stream = new ByteArrayOutputStream\n  lazy val printstream = new PrintStream(stream)\n  implicit lazy val logging: Logging = new PrintStreamLogging(printstream)\n\n  def logLines = new String(stream.toByteArray, StandardCharsets.UTF_8).linesIterator.toList\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/TestHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport org.scalatest.BeforeAndAfterEachTestData\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.TestData\n\ntrait TestHelpers extends AnyFlatSpec with BeforeAndAfterEachTestData {\n\n  override def beforeEach(td: TestData): Unit = {\n    println(s\"\\nStarting test ${td.name} at ${TestUtils.getDateTime()}\")\n    super.beforeEach(td)\n  }\n\n  override def afterEach(td: TestData): Unit = {\n    println(s\"\\nFinished test ${td.name} at ${TestUtils.getDateTime()}\")\n    super.afterEach(td)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/TestUtils.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common;\n\nimport static common.TestUtils.RunResult.executor;\nimport static org.junit.Assert.assertTrue;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.text.SimpleDateFormat;\nimport java.util.*;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\nimport org.junit.rules.TestWatcher;\nimport org.junit.runner.Description;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\n\nimport junit.runner.Version;\n\n/**\n * Miscellaneous utilities used in whisk test suite\n */\npublic class TestUtils {\n    protected static final Logger logger = Logger.getLogger(\"basic\");\n\n    public static final int SUCCESS_EXIT        = 0;\n    public static final int ERROR_EXIT          = 1;\n    public static final int MISUSE_EXIT         = 2;\n    public static final int NETWORK_ERROR_EXIT  = 3;\n    public static final int DONTCARE_EXIT       = -1;       // any value is ok\n    public static final int ANY_ERROR_EXIT      = -2;       // any non-zero value is ok\n\n    public static final int ACCEPTED        = 202;      // 202\n    public static final int BAD_REQUEST     = 144;      // 400 - 256 = 144\n    public static final int UNAUTHORIZED    = 145;      // 401 - 256 = 145\n    public static final int FORBIDDEN       = 147;      // 403 - 256 = 147\n    public static final int NOT_FOUND       = 148;      // 404 - 256 = 148\n    public static final int NOT_ALLOWED     = 149;      // 405 - 256 = 149\n    public static final int CONFLICT        = 153;      // 409 - 256 = 153\n    public static final int TOO_LARGE       = 157;      // 413 - 256 = 157\n    public static final int THROTTLED       = 173;      // 429 (TOO_MANY_REQUESTS) - 256 = 173\n    public static final int APP_ERROR       = 246;      // 502 - 256 = 246\n    public static final int TIMEOUT         = 246;      // 502 (GATEWAY_TIMEOUT) - 256 = 246\n\n    private static final File catalogDir = WhiskProperties.getFileRelativeToWhiskHome(\"catalog\");\n    private static final File testActionsDir = WhiskProperties.getFileRelativeToWhiskHome(\"tests/dat/actions\");\n    private static final File testApiGwDir = WhiskProperties.getFileRelativeToWhiskHome(\"tests/dat/apigw\");\n    private static final File vcapFile = WhiskProperties.getVCAPServicesFile();\n    private static final String envServices = System.getenv(\"VCAP_SERVICES\");\n    private static final String loggerLevel = System.getProperty(\"LOG_LEVEL\", Level.WARNING.toString());\n\n    static {\n        logger.setLevel(Level.parse(loggerLevel.trim()));\n        System.out.println(\"JUnit version is: \" + Version.id());\n    }\n\n    /**\n     * Gets path to file relative to catalog directory.\n     *\n     * (@)deprecated this method will be removed in future version; use {@link #getTestActionFilename()} instead\n     * @param name relative filename\n     */\n    public static String getCatalogFilename(String name) {\n        return new File(catalogDir, name).toString();\n    }\n\n    /**\n     * Gets path to test action file relative to test catalog directory.\n     *\n     * @param name the filename of the test action\n     * @return\n     */\n    public static String getTestActionFilename(String name) {\n        return new File(testActionsDir, name).toString();\n    }\n\n    /**\n     * Gets path to test apigw file relative to test catalog directory.\n     *\n     * @param name the filename of the test action\n     * @return\n     */\n    public static String getTestApiGwFilename(String name) {\n        return new File(testApiGwDir, name).toString();\n    }\n\n    /**\n     * Gets the value of VCAP_SERVICES.\n     *\n     * @return VCAP_SERVICES as a JSON object\n     */\n    public static JsonObject getVCAPServices() {\n        try {\n            if (envServices != null) {\n                return new JsonParser().parse(envServices).getAsJsonObject();\n            } else {\n                return new JsonParser().parse(new FileReader(vcapFile)).getAsJsonObject();\n            }\n        } catch (Throwable t) {\n            System.out.println(\"failed to parse VCAP\" + t);\n            return new JsonObject();\n        }\n    }\n\n    /**\n     * Gets a VCAP_SERVICES credentials.\n     *\n     * @return VCAP credentials as a <String, String> map for each\n     *         <property, value> pair in credentials\n     */\n    public static Map<String, String> getVCAPcredentials(String vcapService) {\n        try {\n            JsonObject credentials = getCredentials(vcapService);\n            Map<String, String> map = new HashMap<String, String>();\n            for (Map.Entry<String, JsonElement> entry : credentials.entrySet()) {\n                map.put(entry.getKey(), credentials.get(entry.getKey()).getAsString());\n            }\n\n            return map;\n        } catch (Throwable t) {\n            System.out.println(\"failed to parse VCAP\" + t);\n            return Collections.emptyMap();\n        }\n    }\n\n    /**\n     * Gets a VCAP_SERVICES credentials as the json objects.\n     *\n     * @return VCAP credentials as a json object.\n     */\n    public static JsonObject getCredentials(String vcapService) {\n        JsonArray vcapArray = getVCAPServices().get(vcapService).getAsJsonArray();\n        JsonObject vcapObject = vcapArray.get(0).getAsJsonObject();\n        JsonObject credentials = vcapObject.get(\"credentials\").getAsJsonObject();\n        return credentials;\n    }\n\n    /**\n     * Creates a JUnit test watcher. Use with @rule.\n     * @return a junit {@link TestWatcher} that prints a message when each test starts and ends\n     */\n    public static TestWatcher makeTestWatcher() {\n        return new TestWatcher() {\n            protected void starting(Description description) {\n                System.out.format(\"\\nStarting test %s at %s\\n\", description.getMethodName(), getDateTime());\n            }\n\n            protected void finished(Description description) {\n                System.out.format(\"Finished test %s at %s\\n\\n\", description.getMethodName(), getDateTime());\n            }\n        };\n    }\n\n    /**\n     * @return a formatted string representing the date and time\n     */\n    public static String getDateTime() {\n        Date date = new Date();\n        SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss.SSS\");\n        return sdf.format(date);\n    }\n\n    /**\n     * @return a formatted string representing the time of day\n     */\n    public static String getTime() {\n        Date date = new Date();\n        SimpleDateFormat sdf = new SimpleDateFormat(\"HH:mm:ss.SSS\");\n        return sdf.format(date);\n    }\n\n    /**\n     * Determines if the test build is running for main repo and not on any fork or PR\n     */\n    public static boolean isBuildingOnMainRepo(){\n        //Based on https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables\n        String repoName = System.getenv(\"TRAVIS_REPO_SLUG\");\n        if (repoName == null) {\n            return false; //Not a travis build\n        } else {\n            return repoName.startsWith(\"apache/\") && \"false\".equals(System.getenv(\"TRAVIS_PULL_REQUEST\"));\n        }\n    }\n\n    /**\n     * Encapsulates the result of running a native command, providing:\n     *   exitCode the exit code of the process\n     *   stdout the messages printed to standard out\n     *   stderr the messages printed to standard error\n     */\n    public static class RunResult {\n        public static final ExecutorService executor = Executors.newFixedThreadPool(2);\n\n        public final int exitCode;\n        public final String stdout;\n        public final String stderr;\n\n        protected RunResult(int exitCode, String stdout, String stderr) {\n            this.exitCode = exitCode;\n            this.stdout = stdout;\n            this.stderr = stderr;\n        }\n\n        public Pair logs() {\n            return new Pair(stdout, stderr);\n        }\n\n        public void validateExitCode(int expectedExitCode) {\n            if (expectedExitCode == TestUtils.DONTCARE_EXIT)\n                return;\n            boolean ok = (exitCode == expectedExitCode) || (expectedExitCode == TestUtils.ANY_ERROR_EXIT && exitCode != 0);\n            if (!ok) {\n                System.out.format(\"expected exit code = %d\\n%s\", expectedExitCode, toString());\n                assertTrue(\"Exit code:\" + exitCode, exitCode == expectedExitCode);\n            }\n        }\n\n        @Override\n        public String toString() {\n            StringBuilder fmt = new StringBuilder();\n            fmt.append(String.format(\"exit code = %d\\n\", exitCode));\n            fmt.append(String.format(\"stdout: %s\\n\", stdout));\n            fmt.append(String.format(\"stderr: %s\\n\", stderr));\n            return fmt.toString();\n        }\n    }\n\n    /**\n     * Runs a command in another process.\n     *\n     * @param expectedExitCode the expected exit code for the process\n     * @param dir the working directory the command runs with\n     * @param params the parameters (including executable) to run\n     * @return RunResult instance\n     * @throws IOException\n     */\n    public static RunResult runCmd(int expectedExitCode, File dir, String... params) throws IOException {\n        return runCmd(expectedExitCode, dir, logger, null, params);\n    }\n\n    /**\n     * Runs a command in another process.\n     *\n     * @param expectedExitCode the exit code expected from the command when it exists\n     * @param dir the working directory the command runs with\n     * @param logger the object to manage logging message\n     * @param env an environment map\n     * @param params the parameters (including executable) to run\n     * @return RunResult instance\n     * @throws IOException\n     */\n    public static RunResult runCmd(int expectedExitCode, File dir, Logger logger, Map<String, String> env, String... params) throws IOException {\n        return runCmd(expectedExitCode, dir, logger, env, null, params);\n    }\n\n    /**\n     * Runs a command in another process.\n     *\n     * @param expectedExitCode the exit code expected from the command when it exists\n     * @param dir the working directory the command runs with\n     * @param logger the object to manage logging message\n     * @param env an environment map\n     * @param fileStdin a file to use as the command's stdin input\n     * @param params the parameters (including executable) to run\n     * @return RunResult instance\n     * @throws IOException\n     */\n    public static RunResult runCmd(int expectedExitCode, File dir, Logger logger, Map<String, String> env, File fileStdin, String... params) throws IOException {\n        ProcessBuilder pb = new ProcessBuilder(params);\n        pb.directory(dir);\n        if (env != null) {\n            pb.environment().putAll(env);\n        }\n\n        if (fileStdin != null) {\n            pb.redirectInput(fileStdin);\n        }\n        Process p = pb.start();\n\n        Future<String> stdoutFuture = executor.submit(() -> inputStreamToString(p.getInputStream()));\n\n        Future<String> stderrFuture = executor.submit(() -> inputStreamToString(p.getErrorStream()));\n\n        String stdout = \"\";\n        String stderr = \"\";\n        try {\n            stdout = stdoutFuture.get();\n            stderr = stderrFuture.get();\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        } catch (ExecutionException e) {\n            e.printStackTrace();\n        }\n\n        try {\n            p.waitFor();\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        }\n        RunResult rr = new RunResult(p.exitValue(), stdout, stderr);\n        if (logger != null) {\n            logger.info(\"RunResult: \" + rr);\n        }\n        rr.validateExitCode(expectedExitCode);\n        return rr;\n    }\n\n    private static String inputStreamToString(InputStream in) throws IOException {\n        BufferedReader reader = new BufferedReader(new InputStreamReader(in));\n        StringBuilder builder = new StringBuilder();\n        String line = null;\n        while ((line = reader.readLine()) != null) {\n            builder.append(line);\n            builder.append(System.getProperty(\"line.separator\"));\n        }\n        return builder.toString();\n    }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/TimingHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport java.time.Instant\nimport scala.concurrent.duration._\n\ntrait TimingHelpers {\n  def between(start: Instant, end: Instant): FiniteDuration =\n    FiniteDuration(java.time.Duration.between(start, end).toNanos, NANOSECONDS)\n\n  def durationOf[A](block: => A): (FiniteDuration, A) = {\n    val start = Instant.now\n    val value = block\n    val end = Instant.now\n    (between(start, end), value)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/WhiskProperties.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.util.Properties;\n\nimport static org.junit.Assert.assertTrue;\n\n/**\n * Properties that describe a whisk installation\n */\npublic class WhiskProperties {\n\n    /**\n     * System property key which refers to OpenWhisk Edge Host url\n     */\n    public static final String WHISK_SERVER = \"whisk.server\";\n\n    /**\n     * System property key which refers to authentication key to be used for testing\n     */\n    private static final String WHISK_AUTH = \"whisk.auth\";\n\n    /**\n     * The name of the properties file.\n     */\n    protected static final String WHISK_PROPS_FILE = \"whisk.properties\";\n\n    /**\n     * Default concurrency level if otherwise unspecified\n     */\n    private static final int DEFAULT_CONCURRENCY = 20;\n\n    /**\n     * If true, then tests will direct to the router rather than the edge\n     * components.\n     */\n    public static final boolean testRouter = System.getProperty(\"test.router\", \"false\").equals(\"true\");\n\n    /**\n     * The root of the whisk installation, used to retrieve files relative to\n     * home.\n     */\n    private static final String whiskHome;\n\n    /**\n     * The properties read from the WHISK_PROPS_FILE.\n     */\n    private static final Properties whiskProperties;\n\n    static {\n        /**\n         * Finds the whisk home directory. This is resolved to either (in\n         * order):\n         *\n         * 1. a system property openwhisk.dir\n         *\n         * 2. OPENWHISK_HOME from the environment\n         *\n         * 3. a path in the directory tree containing WHISK_PROPS_FILE.\n         *\n         * @return the path to whisk home as a string\n         * @throws assertion\n         *             failure if whisk home cannot be determined\n         */\n        String wskdir = System.getProperty(\"openwhisk.home\", System.getenv(\"OPENWHISK_HOME\"));\n        if (wskdir == null) {\n            String dir = System.getProperty(\"user.dir\");\n\n            if (dir != null) {\n                File propfile = findFileRecursively(dir, WHISK_PROPS_FILE);\n                if (propfile != null) {\n                    wskdir = propfile.getParent();\n                }\n            }\n        }\n\n        assertTrue(\"could not determine openwhisk home\", wskdir != null);\n\n        if (isWhiskPropertiesRequired()) {\n            File wskpropsFile = new File(wskdir, WHISK_PROPS_FILE);\n            assertTrue(String.format(\"'%s' does not exists but required\", wskpropsFile), wskpropsFile.exists());\n\n            // loads properties from file\n            whiskProperties = loadProperties(wskpropsFile);\n\n            // set whisk home from read properties\n            whiskHome = whiskProperties.getProperty(\"openwhisk.home\");\n        } else {\n            whiskProperties = new Properties();\n            whiskHome = wskdir;\n        }\n\n        System.out.format(\"test router? %s\\n\", testRouter);\n    }\n\n    /**\n     * The path to the Go CLI executable.\n     */\n    public static String getCLIPath() {\n        return whiskHome + \"/bin/wsk\";\n    }\n\n    public static File getFileRelativeToWhiskHome(String name) {\n        return new File(whiskHome, name);\n    }\n\n    public static int getControllerInstances() {\n        return Integer.parseInt(whiskProperties.getProperty(\"controller.instances\"));\n    }\n\n    public static String getProperty(String name) {\n        return whiskProperties.getProperty(name);\n    }\n\n    public static Boolean getBooleanProperty(String name, Boolean defaultValue) {\n        String value = whiskProperties.getProperty(name);\n        if (value == null) {\n            return defaultValue;\n        }\n\n        return Boolean.parseBoolean(value);\n    }\n\n    public static String getKafkaHosts() {\n        return whiskProperties.getProperty(\"kafka.hosts\");\n    }\n\n    public static int getKafkaMonitorPort() {\n        return Integer.parseInt(whiskProperties.getProperty(\"kafkaras.host.port\"));\n    }\n\n    public static String getZookeeperHost() {\n        return whiskProperties.getProperty(\"zookeeper.hosts\");\n    }\n\n    public static String getMainDockerEndpoint() {\n        return whiskProperties.getProperty(\"main.docker.endpoint\");\n    }\n\n    public static String[] getInvokerHosts() {\n        // split of empty string is non-empty array\n        String hosts = whiskProperties.getProperty(\"invoker.hosts\");\n        return (hosts == null || hosts.equals(\"\")) ? new String[0] : hosts.split(\",\");\n    }\n\n    public static String[] getAdditionalHosts() {\n        // split of empty string is non-empty array\n        String hosts = whiskProperties.getProperty(\"additional.hosts\");\n        return (hosts == null || hosts.equals(\"\")) ? new String[0] : hosts.split(\",\");\n    }\n\n    public static int numberOfInvokers() {\n        return getInvokerHosts().length;\n    }\n\n    public static boolean isSSLCheckRelaxed() {\n        return Boolean.valueOf(getPropFromSystemOrEnv(\"whisk.ssl.relax\"));\n    }\n\n    public static String getSslCertificateChallenge() {\n        return whiskProperties.getProperty(\"whisk.ssl.challenge\");\n    }\n\n    /**\n     * Note that when testRouter == true, we pretend the router host is edge\n     * host.\n     */\n    public static String getEdgeHost() {\n        String server = getPropFromSystemOrEnv(WHISK_SERVER);\n        if (server != null) {\n            return server;\n        }\n        return testRouter ? getRouterHost() : whiskProperties.getProperty(\"edge.host\");\n    }\n\n    public static String getRealEdgeHost() {\n        return whiskProperties.getProperty(\"edge.host\");\n    }\n\n    public static String getAuthForTesting() {\n        return whiskProperties.getProperty(\"testing.auth\");\n    }\n\n    public static String getRouterHost() {\n        return whiskProperties.getProperty(\"router.host\");\n    }\n\n    public static String getApiProto() {\n        return whiskProperties.getProperty(\"whisk.api.host.proto\");\n    }\n\n    public static String getApiHost() {\n        return whiskProperties.getProperty(\"whisk.api.host.name\");\n    }\n\n    public static String getApiPort() {\n        return whiskProperties.getProperty(\"whisk.api.host.port\");\n    }\n\n    public static String getApiHostForAction() {\n        String apihost = getApiProto() + \"://\" + getApiHost() + \":\" + getApiPort();\n        if (apihost.startsWith(\"https://\") && apihost.endsWith(\":443\")) {\n            return apihost.replaceAll(\":443\", \"\");\n        } else if (apihost.startsWith(\"http://\") && apihost.endsWith(\":80\")) {\n            return apihost.replaceAll(\":80\", \"\");\n        } else return apihost;\n    }\n\n    public static String getApiHostForClient(String subdomain, boolean includeProtocol) {\n        String proto = whiskProperties.getProperty(\"whisk.api.host.proto\");\n        String port = whiskProperties.getProperty(\"whisk.api.host.port\");\n        String host = whiskProperties.getProperty(\"whisk.api.localhost.name\");\n        if (includeProtocol) {\n            return proto + \"://\" + subdomain + \".\" + host + \":\" + port;\n        } else {\n            return subdomain + \".\" + host + \":\" + port;\n        }\n    }\n\n    public static int getPartsInVanitySubdomain() {\n        return Integer.parseInt(whiskProperties.getProperty(\"whisk.api.vanity.subdomain.parts\"));\n    }\n\n    public static int getEdgeHostApiPort() {\n        return Integer.parseInt(whiskProperties.getProperty(\"edge.host.apiport\"));\n    }\n\n    public static String getControllerHosts() {\n        return whiskProperties.getProperty(\"controller.hosts\");\n    }\n\n    public static String getDBHosts() {\n        return whiskProperties.getProperty(\"db.hostsList\");\n    }\n\n    public static int getControllerBasePort() {\n        return Integer.parseInt(whiskProperties.getProperty(\"controller.host.basePort\"));\n    }\n\n    public static String getBaseControllerHost() {\n        return getControllerHosts().split(\",\")[0];\n    }\n\n    public static String getBaseDBHost() {\n        return getDBHosts().split(\",\")[0];\n    }\n\n    public static String getBaseControllerAddress() {\n        return getBaseControllerHost() + \":\" + getControllerBasePort();\n    }\n\n    public static String getBaseInvokerAddress(){\n        return getInvokerHosts()[0] + \":\" + whiskProperties.getProperty(\"invoker.hosts.basePort\");\n    }\n\n    public static int getMaxActionInvokesPerMinute() {\n        String valStr = whiskProperties.getProperty(\"limits.actions.invokes.perMinute\");\n        return Integer.parseInt(valStr);\n    }\n\n    /**\n     * read the contents of auth key file and return as a Pair\n     * <username,password>\n     */\n    public static Pair getBasicAuth() {\n        File f = getAuthFileForTesting();\n        String contents = readAuthKey(f);\n        String[] parts = contents.split(\":\");\n        assert parts.length == 2;\n        return new Pair(parts[0], parts[1]);\n    }\n\n    /**\n     * @return the path to a file holding the auth key used during junit testing\n     */\n    public static File getAuthFileForTesting() {\n        String testAuth = getAuthForTesting();\n        if (testAuth.startsWith(File.separator)) {\n            return new File(testAuth);\n        } else {\n            return WhiskProperties.getFileRelativeToWhiskHome(testAuth);\n        }\n    }\n\n    /**\n     * read the contents of a file which holds an auth key.\n     */\n    public static String readAuthKey(File filename) {\n        // the following funny relative path works both from Eclipse and when\n        // running in bin/ directory from ant\n        try {\n            byte[] encoded = Files.readAllBytes(filename.toPath());\n            String authKey = new String(encoded, \"UTF-8\").trim();\n            return authKey;\n        } catch (IOException e) {\n            throw new IllegalStateException(e);\n        }\n    }\n\n    /**\n     * Returns auth key to be used for testing\n     */\n    public static String getAuthKeyForTesting() {\n        String authKey = getPropFromSystemOrEnv(WHISK_AUTH);\n        if (authKey == null) {\n            authKey = readAuthKey(getAuthFileForTesting());\n        }\n        return authKey;\n    }\n\n    /**\n     * @return the path to a file holding the VCAP_SERVICES used during junit\n     *         testing\n     */\n    public static File getVCAPServicesFile() {\n        String vcapServices = whiskProperties.getProperty(\"vcap.services.file\");\n        if (vcapServices == null) {\n            return null;\n        } else if (vcapServices.startsWith(File.separator)) {\n            return new File(vcapServices);\n        } else {\n            return WhiskProperties.getFileRelativeToWhiskHome(vcapServices);\n        }\n    }\n\n    /**\n     * are we running on Mac OS X?\n     */\n    public static boolean onMacOSX() {\n        String osname = System.getProperty(\"os.name\");\n        return osname.toLowerCase().contains(\"mac\");\n    }\n\n    /**\n     * are we running on Linux?\n     */\n    public static boolean onLinux() {\n        String osname = System.getProperty(\"os.name\");\n        return osname.equalsIgnoreCase(\"linux\");\n    }\n\n    public static int getMaxActionSizeMB(){\n        return Integer.parseInt(getProperty(\"whisk.action.size.max\", \"10\"));\n    }\n\n    /**\n     * python interpreter.\n     */\n    public static final String python = \"python\";\n\n    protected static File findFileRecursively(String dir, String needle) {\n        if (dir != null) {\n            File base = new File(dir);\n            File file = new File(base, needle);\n            if (file.exists()) {\n                return file;\n            } else {\n                return findFileRecursively(base.getParent(), needle);\n            }\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * Load properties from whisk.properties\n     */\n    protected static Properties loadProperties(File propsFile) {\n        Properties props = new Properties();\n        InputStream input = null;\n\n        try {\n            input = new FileInputStream(propsFile);\n            // load a properties file\n            props.load(input);\n        } catch (IOException ex) {\n            ex.printStackTrace();\n        } finally {\n            if (input != null) {\n                try {\n                    input.close();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n        return props;\n    }\n\n    private static boolean isWhiskPropertiesRequired() {\n        return getPropFromSystemOrEnv(WHISK_SERVER) == null;\n    }\n\n    public static String getProperty(String key, String defaultValue) {\n        String value = getPropFromSystemOrEnv(key);\n        if (value == null) {\n            value = whiskProperties.getProperty(key, defaultValue);\n        }\n        return value;\n    }\n\n    private static String getPropFromSystemOrEnv(String key) {\n        String value = System.getProperty(key);\n        if (value == null) {\n            value = System.getenv(toEnvName(key));\n        }\n        return value;\n    }\n\n    private static String toEnvName(String p) {\n        return p.replace('.', '_').toUpperCase();\n    }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/WskActorSystem.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport scala.concurrent.Await\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.duration.DurationInt\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.Http\n\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.Suite\n\n/**\n * Helper trait to provide an implicit actor system and execution context\n *  to tests, and properly shut it down at the end.\n *\n *  If you use this trait and override afterAll(), make sure to also call super.afterAll().\n */\ntrait WskActorSystem extends BeforeAndAfterAll {\n  self: Suite =>\n\n  implicit val actorSystem: ActorSystem = ActorSystem()\n\n  implicit def executionContext: ExecutionContext = actorSystem.dispatcher\n\n  override def afterAll() = {\n    try {\n      Await.result(Http().shutdownAllConnectionPools(), 30.seconds)\n    } finally {\n      actorSystem.terminate()\n      Await.result(actorSystem.whenTerminated, 30.seconds)\n    }\n    super.afterAll()\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/WskCliOperations.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport java.io.File\nimport java.time.Instant\n\nimport scala.Left\nimport scala.Right\nimport scala.collection.mutable.Buffer\nimport scala.concurrent.duration.Duration\nimport scala.concurrent.duration.DurationInt\nimport scala.language.postfixOps\nimport scala.util.Failure\nimport scala.util.Success\nimport scala.util.Try\n\nimport common.TestUtils._\nimport spray.json.JsObject\nimport spray.json.JsValue\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.utils.retry\n\nimport FullyQualifiedNames.fqn\nimport FullyQualifiedNames.resolve\n\n/**\n * Provide Scala bindings for the whisk CLI.\n *\n * Each of the top level CLI commands is a \"noun\" class that extends one\n * of several traits that are common to the whisk collections and corresponds\n * to one of the top level CLI nouns.\n *\n * Each of the \"noun\" classes mixes in the RunCliCmd trait which runs arbitrary\n * wsk commands and returns the results. Optionally RunCliCmd can validate the exit\n * code matched a desired value.\n *\n * The various collections support one or more of these as common traits:\n * list, get, delete, and sanitize.\n *\n * Sanitize is akin to delete but accepts a failure because entity may not\n * exit. Additionally, some of the nouns define custom commands.\n *\n * All of the commands define default values that are either optional\n * or omitted in the common case. This makes for a compact implementation\n * instead of using a Builder pattern.\n *\n * An implicit WskProps instance is required for all of CLI commands. This\n * type provides the authentication key for the API as well as the namespace.\n * It also sets the apihost and apiversion explicitly to avoid ambiguity with\n * a local property file if it exists.\n */\nclass Wsk(cliPath: String = Wsk.defaultCliPath) extends WskOperations with RunCliCmd {\n\n  assert({\n    val f = new File(cliPath)\n    f.exists && f.isFile && f.canExecute\n  }, s\"did not find $cliPath\")\n\n  override def baseCommand = Buffer(cliPath)\n\n  override implicit val action = new CliActionOperations(this)\n  override implicit val trigger = new CliTriggerOperations(this)\n  override implicit val rule = new CliRuleOperations(this)\n  override implicit val activation = new CliActivationOperations(this)\n  override implicit val pkg = new CliPackageOperations(this)\n  override implicit val namespace = new CliNamespaceOperations(this)\n  override implicit val api = new CliGatewayOperations(this)\n}\n\ntrait CliListOrGetFromCollectionOperations extends ListOrGetFromCollectionOperations {\n  val wsk: RunCliCmd\n\n  /**\n   * List entities in collection.\n   *\n   * @param namespace (optional) if specified must be  fully qualified namespace\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def list(namespace: Option[String] = None,\n                    limit: Option[Int] = None,\n                    nameSort: Option[Boolean] = None,\n                    expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"list\", resolve(namespace), \"--auth\", wp.authKey) ++ {\n      limit map { l =>\n        Seq(\"--limit\", l.toString)\n      } getOrElse Seq.empty\n    } ++ {\n      nameSort map { n =>\n        Seq(\"--name-sort\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n\n  /**\n   * Gets entity from collection.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def get(name: String,\n                   expectedExitCode: Int = SUCCESS_EXIT,\n                   summary: Boolean = false,\n                   fieldFilter: Option[String] = None,\n                   url: Option[Boolean] = None,\n                   save: Option[Boolean] = None,\n                   saveAs: Option[String] = None)(implicit wp: WskProps): RunResult = {\n\n    val params = Seq(noun, \"get\", \"--auth\", wp.authKey) ++\n      Seq(fqn(name)) ++ { if (summary) Seq(\"--summary\") else Seq.empty } ++ {\n      fieldFilter map { f =>\n        Seq(f)\n      } getOrElse Seq.empty\n    } ++ {\n      url map { u =>\n        Seq(\"--url\")\n      } getOrElse Seq.empty\n    } ++ {\n      save map { s =>\n        Seq(\"--save\")\n      } getOrElse Seq.empty\n    } ++ {\n      saveAs map { s =>\n        Seq(\"--save-as\", s)\n      } getOrElse Seq.empty\n    }\n\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n}\n\ntrait CliDeleteFromCollectionOperations extends DeleteFromCollectionOperations {\n  val wsk: RunCliCmd\n\n  /**\n   * Deletes entity from collection.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def delete(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    wsk.cli(wp.overrides ++ Seq(noun, \"delete\", \"--auth\", wp.authKey, fqn(name)), expectedExitCode)\n  }\n\n  /**\n   * Deletes entity from collection but does not assert that the command succeeds.\n   * Use this if deleting an entity that may not exist and it is OK if it does not.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   */\n  override def sanitize(name: String)(implicit wp: WskProps): RunResult = {\n    delete(name, DONTCARE_EXIT)\n  }\n}\n\nclass CliActionOperations(override val wsk: RunCliCmd)\n    extends CliListOrGetFromCollectionOperations\n    with CliDeleteFromCollectionOperations\n    with HasActivation\n    with ActionOperations {\n\n  override protected val noun = \"action\"\n\n  /**\n   * Creates action. Parameters mirror those available in the CLI.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(\n    name: String,\n    artifact: Option[String],\n    kind: Option[String] = None, // one of docker, copy, sequence or none for autoselect else an explicit type\n    main: Option[String] = None,\n    docker: Option[String] = None,\n    parameters: Map[String, JsValue] = Map.empty,\n    annotations: Map[String, JsValue] = Map.empty,\n    delAnnotations: Array[String] = Array(),\n    parameterFile: Option[String] = None,\n    annotationFile: Option[String] = None,\n    timeout: Option[Duration] = None,\n    memory: Option[ByteSize] = None,\n    logsize: Option[ByteSize] = None,\n    concurrency: Option[Int] = None,\n    shared: Option[Boolean] = None,\n    update: Boolean = false,\n    web: Option[String] = None,\n    websecure: Option[String] = None,\n    expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, if (!update) \"create\" else \"update\", \"--auth\", wp.authKey, fqn(name)) ++ {\n      artifact map { Seq(_) } getOrElse Seq.empty\n    } ++ {\n      kind map { k =>\n        if (k == \"sequence\" || k == \"copy\" || k == \"native\") Seq(s\"--$k\")\n        else Seq(\"--kind\", k)\n      } getOrElse Seq.empty\n    } ++ {\n      main.toSeq flatMap { p =>\n        Seq(\"--main\", p)\n      }\n    } ++ {\n      docker.toSeq flatMap { p =>\n        Seq(\"--docker\", p)\n      }\n    } ++ {\n      parameters flatMap { p =>\n        Seq(\"-p\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      annotations flatMap { p =>\n        Seq(\"-a\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      delAnnotations flatMap { p =>\n        Seq(\"--del-annotation\", p)\n      }\n    } ++ {\n      parameterFile map { pf =>\n        Seq(\"-P\", pf)\n      } getOrElse Seq.empty\n    } ++ {\n      annotationFile map { af =>\n        Seq(\"-A\", af)\n      } getOrElse Seq.empty\n    } ++ {\n      timeout map { t =>\n        Seq(\"-t\", t.toMillis.toString)\n      } getOrElse Seq.empty\n    } ++ {\n      memory map { m =>\n        Seq(\"-m\", m.toMB.toString)\n      } getOrElse Seq.empty\n    } ++ {\n      logsize map { l =>\n        Seq(\"-l\", l.toMB.toString)\n      } getOrElse Seq.empty\n    } ++ {\n      concurrency map { c =>\n        Seq(\"-c\", c.toString)\n      } getOrElse Seq.empty\n    } ++ {\n      shared map { s =>\n        Seq(\"--shared\", if (s) \"yes\" else \"no\")\n      } getOrElse Seq.empty\n    } ++ {\n      web map { w =>\n        Seq(\"--web\", w)\n      } getOrElse Seq.empty\n    } ++ {\n      websecure map { ws =>\n        Seq(\"--web-secure\", ws)\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n\n  /**\n   * Invokes action. Parameters mirror those available in the CLI.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def invoke(name: String,\n                      parameters: Map[String, JsValue] = Map.empty,\n                      parameterFile: Option[String] = None,\n                      blocking: Boolean = false,\n                      result: Boolean = false,\n                      expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"invoke\", \"--auth\", wp.authKey, fqn(name)) ++ {\n      parameters flatMap { p =>\n        Seq(\"-p\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      parameterFile map { pf =>\n        Seq(\"-P\", pf)\n      } getOrElse Seq.empty\n    } ++ { if (blocking) Seq(\"--blocking\") else Seq.empty } ++ { if (result) Seq(\"--result\") else Seq.empty }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n}\n\nclass CliTriggerOperations(override val wsk: RunCliCmd)\n    extends CliListOrGetFromCollectionOperations\n    with CliDeleteFromCollectionOperations\n    with HasActivation\n    with TriggerOperations {\n\n  override protected val noun = \"trigger\"\n\n  /**\n   * Creates trigger. Parameters mirror those available in the CLI.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(name: String,\n                      parameters: Map[String, JsValue] = Map.empty,\n                      annotations: Map[String, JsValue] = Map.empty,\n                      parameterFile: Option[String] = None,\n                      annotationFile: Option[String] = None,\n                      feed: Option[String] = None,\n                      shared: Option[Boolean] = None,\n                      update: Boolean = false,\n                      expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, if (!update) \"create\" else \"update\", \"--auth\", wp.authKey, fqn(name)) ++ {\n      feed map { f =>\n        Seq(\"--feed\", fqn(f))\n      } getOrElse Seq.empty\n    } ++ {\n      parameters flatMap { p =>\n        Seq(\"-p\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      annotations flatMap { p =>\n        Seq(\"-a\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      parameterFile map { pf =>\n        Seq(\"-P\", pf)\n      } getOrElse Seq.empty\n    } ++ {\n      annotationFile map { af =>\n        Seq(\"-A\", af)\n      } getOrElse Seq.empty\n    } ++ {\n      shared map { s =>\n        Seq(\"--shared\", if (s) \"yes\" else \"no\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n\n  /**\n   * Fires trigger. Parameters mirror those available in the CLI.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def fire(name: String,\n                    parameters: Map[String, JsValue] = Map.empty,\n                    parameterFile: Option[String] = None,\n                    expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"fire\", \"--auth\", wp.authKey, fqn(name)) ++ {\n      parameters flatMap { p =>\n        Seq(\"-p\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      parameterFile map { pf =>\n        Seq(\"-P\", pf)\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n}\n\nclass CliRuleOperations(override val wsk: RunCliCmd)\n    extends CliListOrGetFromCollectionOperations\n    with CliDeleteFromCollectionOperations\n    with WaitFor\n    with RuleOperations {\n\n  override protected val noun = \"rule\"\n\n  /**\n   * Creates rule. Parameters mirror those available in the CLI.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param trigger must be a simple name\n   * @param action must be a simple name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(name: String,\n                      trigger: String,\n                      action: String,\n                      annotations: Map[String, JsValue] = Map.empty,\n                      shared: Option[Boolean] = None,\n                      update: Boolean = false,\n                      expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, if (!update) \"create\" else \"update\", \"--auth\", wp.authKey, fqn(name), (trigger), (action)) ++ {\n      annotations flatMap { p =>\n        Seq(\"-a\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      shared map { s =>\n        Seq(\"--shared\", if (s) \"yes\" else \"no\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n\n  /**\n   * Deletes rule.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def delete(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    super.delete(name, expectedExitCode)\n  }\n\n  /**\n   * Enables rule.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def enable(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    wsk.cli(wp.overrides ++ Seq(noun, \"enable\", \"--auth\", wp.authKey, fqn(name)), expectedExitCode)\n  }\n\n  /**\n   * Disables rule.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def disable(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    wsk.cli(wp.overrides ++ Seq(noun, \"disable\", \"--auth\", wp.authKey, fqn(name)), expectedExitCode)\n  }\n\n  /**\n   * Checks state of rule.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def state(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    wsk.cli(wp.overrides ++ Seq(noun, \"status\", \"--auth\", wp.authKey, fqn(name)), expectedExitCode)\n  }\n}\n\nclass CliActivationOperations(val wsk: RunCliCmd) extends ActivationOperations with HasActivation with WaitFor {\n\n  protected val noun = \"activation\"\n\n  /**\n   * Activation polling console.\n   *\n   * @param duration exits console after duration\n   * @param since (optional) time travels back to activation since given duration\n   * @param actionName (optional) name of entity to filter activation records on.\n   */\n  override def console(duration: Duration,\n                       since: Option[Duration] = None,\n                       expectedExitCode: Int = SUCCESS_EXIT,\n                       actionName: Option[String] = None)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"poll\") ++ {\n      actionName map { name =>\n        Seq(name)\n      } getOrElse Seq.empty\n    } ++ Seq(\"--auth\", wp.authKey, \"--exit\", duration.toSeconds.toString) ++ {\n      since map { s =>\n        Seq(\"--since-seconds\", s.toSeconds.toString)\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n\n  /**\n   * Lists activations.\n   *\n   * @param filter (optional) if define, must be a simple entity name\n   * @param limit (optional) the maximum number of activation to return\n   * @param since (optional) only the activations since this timestamp are included\n   * @param skip (optional) the number of activations to skip\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  def list(filter: Option[String] = None,\n           limit: Option[Int] = None,\n           since: Option[Instant] = None,\n           skip: Option[Int] = None,\n           expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"list\", \"--auth\", wp.authKey) ++ { filter map { Seq(_) } getOrElse Seq.empty } ++ {\n      limit map { l =>\n        Seq(\"--limit\", l.toString)\n      } getOrElse Seq.empty\n    } ++ {\n      since map { i =>\n        Seq(\"--since\", i.toEpochMilli.toString)\n      } getOrElse Seq.empty\n    } ++ {\n      skip map { i =>\n        Seq(\"--skip\", i.toString)\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n\n  /**\n   * Parses result of WskActivation.list to extract sequence of activation ids.\n   *\n   * @param rr run result, should be from WhiskActivation.list otherwise behavior is undefined\n   * @return sequence of activations\n   */\n  def ids(rr: RunResult): Seq[String] = {\n    val lines = rr.stdout.split(\"\\n\")\n    val header = lines(0)\n    // old format has the activation id first, new format has activation id in third column\n    val column = if (header.startsWith(\"activations\")) 0 else 2\n    lines.drop(1).map(_.split(\" \")(column)) // drop the header and grab just the activationId column\n  }\n\n  /**\n   * Gets activation by id.\n   *\n   * @param activationId the activation id\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   * @param last retrieves latest acitvation\n   */\n  override def get(activationId: Option[String] = None,\n                   expectedExitCode: Int = SUCCESS_EXIT,\n                   fieldFilter: Option[String] = None,\n                   last: Option[Boolean] = None,\n                   summary: Option[Boolean] = None)(implicit wp: WskProps): RunResult = {\n    val params = {\n      activationId map { a =>\n        Seq(a)\n      } getOrElse Seq.empty\n    } ++ {\n      fieldFilter map { f =>\n        Seq(f)\n      } getOrElse Seq.empty\n    } ++ {\n      last map { l =>\n        Seq(\"--last\")\n      } getOrElse Seq.empty\n    } ++ {\n      summary map { s =>\n        Seq(\"--summary\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ Seq(noun, \"get\", \"--auth\", wp.authKey) ++ params, expectedExitCode)\n  }\n\n  /**\n   * Gets activation logs by id.\n   *\n   * @param activationId the activation id\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   * @param last retrieves latest acitvation\n   */\n  override def logs(activationId: Option[String] = None,\n                    expectedExitCode: Int = SUCCESS_EXIT,\n                    last: Option[Boolean] = None)(implicit wp: WskProps): RunResult = {\n    val params = {\n      activationId map { a =>\n        Seq(a)\n      } getOrElse Seq.empty\n    } ++ {\n      last map { l =>\n        Seq(\"--last\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ Seq(noun, \"logs\", \"--auth\", wp.authKey) ++ params, expectedExitCode)\n  }\n\n  /**\n   * Gets activation result by id.\n   *\n   * @param activationId the activation id\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   * @param last retrieves latest acitvation\n   */\n  override def result(activationId: Option[String] = None,\n                      expectedExitCode: Int = SUCCESS_EXIT,\n                      last: Option[Boolean] = None)(implicit wp: WskProps): RunResult = {\n    val params = {\n      activationId map { a =>\n        Seq(a)\n      } getOrElse Seq.empty\n    } ++ {\n      last map { l =>\n        Seq(\"--last\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ Seq(noun, \"result\", \"--auth\", wp.authKey) ++ params, expectedExitCode)\n  }\n\n  /**\n   * Polls activations list for at least N activations. The activations\n   * are optionally filtered for the given entity. Will return as soon as\n   * N activations are found. If after retry budget is exhausted, N activations\n   * are still not present, will return a partial result. Hence caller must\n   * check length of the result and not assume it is >= N.\n   *\n   * @param N the number of activations desired\n   * @param entity the name of the entity to filter from activation list\n   * @param limit the maximum number of entities to list (if entity name is not unique use Some(0))\n   * @param since (optional) only the activations since this timestamp are included\n   * @param skip (optional) the number of activations to skip\n   * @param retries the maximum retries (total timeout is retries + 1 seconds)\n   * @return activation ids found, caller must check length of sequence\n   */\n  override def pollFor(N: Int,\n                       entity: Option[String],\n                       limit: Option[Int] = None,\n                       since: Option[Instant] = None,\n                       skip: Option[Int] = Some(0),\n                       retries: Int = 10,\n                       pollPeriod: Duration = 1.second)(implicit wp: WskProps): Seq[String] = {\n    Try {\n      retry({\n        val result = ids(list(filter = entity, limit = limit, since = since, skip = skip))\n        if (result.length >= N) result else throw PartialResult(result)\n      }, retries, waitBeforeRetry = Some(pollPeriod))\n    } match {\n      case Success(ids)                => ids\n      case Failure(PartialResult(ids)) => ids\n      case _                           => Seq.empty\n    }\n  }\n\n  /**\n   * Polls for an activation matching the given id. If found\n   * return Right(activation) else Left(result of running CLI command).\n   *\n   * @return either Left(error message) or Right(activation as JsObject)\n   */\n  override def waitForActivation(activationId: String,\n                                 initialWait: Duration = 1 second,\n                                 pollPeriod: Duration = 1 second,\n                                 totalWait: Duration = 30 seconds)(implicit wp: WskProps): Either[String, JsObject] = {\n    val activation = waitfor(\n      () => {\n        val result =\n          wsk\n            .cli(wp.overrides ++ Seq(noun, \"get\", activationId, \"--auth\", wp.authKey), expectedExitCode = DONTCARE_EXIT)\n        if (result.exitCode == NOT_FOUND) {\n          null\n        } else if (result.exitCode == SUCCESS_EXIT) {\n          Right(result.stdout)\n        } else Left(s\"$result\")\n      },\n      initialWait,\n      pollPeriod,\n      totalWait)\n\n    Option(activation) map {\n      case Right(stdout) =>\n        Try {\n          // strip first line and convert the rest to JsObject\n          assert(stdout.startsWith(\"ok: got activation\"))\n          WskOperations.parseJsonString(stdout)\n        } map {\n          Right(_)\n        } getOrElse Left(s\"cannot parse activation from '$stdout'\")\n      case Left(error) => Left(error)\n    } getOrElse Left(s\"$activationId not found\")\n  }\n\n  /** Used in polling for activations to record partial results from retry poll. */\n  private case class PartialResult(ids: Seq[String]) extends Throwable\n}\n\nclass CliNamespaceOperations(override val wsk: RunCliCmd)\n    extends CliDeleteFromCollectionOperations\n    with NamespaceOperations {\n\n  protected val noun = \"namespace\"\n\n  /**\n   * Lists available namespaces for whisk key.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def list(expectedExitCode: Int = SUCCESS_EXIT, nameSort: Option[Boolean] = None)(\n    implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"list\", \"--auth\", wp.authKey) ++ {\n      nameSort map { n =>\n        Seq(\"--name-sort\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n\n  /**\n   * Looks up namespace for whisk props.\n   *\n   * @param wskprops instance of WskProps with an auth key to lookup\n   * @return namespace as string\n   */\n  override def whois()(implicit wskprops: WskProps): String = {\n    // the invariant that list() returns a conforming result is enforced in WskRestBasicTests\n    val ns = list().stdout.linesIterator.toSeq.last.trim\n    assert(ns != \"_\") // this is not permitted\n    ns\n  }\n\n  /**\n   * Gets entities in namespace.\n   *\n   * @param namespace (optional) if specified must be  fully qualified namespace\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  def get(namespace: Option[String] = None, expectedExitCode: Int, nameSort: Option[Boolean] = None)(\n    implicit wp: WskProps): RunResult = {\n    val params = {\n      nameSort map { n =>\n        Seq(\"--name-sort\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ Seq(noun, \"get\", resolve(namespace), \"--auth\", wp.authKey) ++ params, expectedExitCode)\n  }\n}\n\nclass CliPackageOperations(override val wsk: RunCliCmd)\n    extends CliListOrGetFromCollectionOperations\n    with CliDeleteFromCollectionOperations\n    with PackageOperations {\n  override protected val noun = \"package\"\n\n  /**\n   * Creates package. Parameters mirror those available in the CLI.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(name: String,\n                      parameters: Map[String, JsValue] = Map.empty,\n                      annotations: Map[String, JsValue] = Map.empty,\n                      parameterFile: Option[String] = None,\n                      annotationFile: Option[String] = None,\n                      shared: Option[Boolean] = None,\n                      update: Boolean = false,\n                      expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, if (!update) \"create\" else \"update\", \"--auth\", wp.authKey, fqn(name)) ++ {\n      parameters flatMap { p =>\n        Seq(\"-p\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      annotations flatMap { p =>\n        Seq(\"-a\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      parameterFile map { pf =>\n        Seq(\"-P\", pf)\n      } getOrElse Seq.empty\n    } ++ {\n      annotationFile map { af =>\n        Seq(\"-A\", af)\n      } getOrElse Seq.empty\n    } ++ {\n      shared map { s =>\n        Seq(\"--shared\", if (s) \"yes\" else \"no\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n\n  /**\n   * Binds package. Parameters mirror those available in the CLI.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def bind(provider: String,\n                    name: String,\n                    parameters: Map[String, JsValue] = Map.empty,\n                    annotations: Map[String, JsValue] = Map.empty,\n                    expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"bind\", \"--auth\", wp.authKey, fqn(provider), fqn(name)) ++ {\n      parameters flatMap { p =>\n        Seq(\"-p\", p._1, p._2.compactPrint)\n      }\n    } ++ {\n      annotations flatMap { p =>\n        Seq(\"-a\", p._1, p._2.compactPrint)\n      }\n    }\n    wsk.cli(wp.overrides ++ params, expectedExitCode)\n  }\n}\n\nclass CliGatewayOperations(val wsk: RunCliCmd) extends GatewayOperations {\n  protected val noun = \"api\"\n\n  /**\n   * Creates and API endpoint. Parameters mirror those available in the CLI.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(basepath: Option[String] = None,\n                      relpath: Option[String] = None,\n                      operation: Option[String] = None,\n                      action: Option[String] = None,\n                      apiname: Option[String] = None,\n                      swagger: Option[String] = None,\n                      responsetype: Option[String] = None,\n                      expectedExitCode: Int = SUCCESS_EXIT,\n                      cliCfgFile: Option[String] = None)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"create\", \"--auth\", wp.authKey) ++ {\n      basepath map { b =>\n        Seq(b)\n      } getOrElse Seq.empty\n    } ++ {\n      relpath map { r =>\n        Seq(r)\n      } getOrElse Seq.empty\n    } ++ {\n      operation map { o =>\n        Seq(o)\n      } getOrElse Seq.empty\n    } ++ {\n      action map { aa =>\n        Seq(aa)\n      } getOrElse Seq.empty\n    } ++ {\n      apiname map { a =>\n        Seq(\"--apiname\", a)\n      } getOrElse Seq.empty\n    } ++ {\n      swagger map { s =>\n        Seq(\"--config-file\", s)\n      } getOrElse Seq.empty\n    } ++ {\n      responsetype map { t =>\n        Seq(\"--response-type\", t)\n      } getOrElse Seq.empty\n    }\n    wsk.cli(\n      wp.overrides ++ params,\n      expectedExitCode,\n      showCmd = true,\n      env = Map(\"WSK_CONFIG_FILE\" -> cliCfgFile.getOrElse(\"\")))\n  }\n\n  /**\n   * Retrieve a list of API endpoints. Parameters mirror those available in the CLI.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def list(basepathOrApiName: Option[String] = None,\n                    relpath: Option[String] = None,\n                    operation: Option[String] = None,\n                    limit: Option[Int] = None,\n                    since: Option[Instant] = None,\n                    full: Option[Boolean] = None,\n                    nameSort: Option[Boolean] = None,\n                    expectedExitCode: Int = SUCCESS_EXIT,\n                    cliCfgFile: Option[String] = None)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"list\", \"--auth\", wp.authKey) ++ {\n      basepathOrApiName map { b =>\n        Seq(b)\n      } getOrElse Seq.empty\n    } ++ {\n      relpath map { r =>\n        Seq(r)\n      } getOrElse Seq.empty\n    } ++ {\n      operation map { o =>\n        Seq(o)\n      } getOrElse Seq.empty\n    } ++ {\n      limit map { l =>\n        Seq(\"--limit\", l.toString)\n      } getOrElse Seq.empty\n    } ++ {\n      since map { i =>\n        Seq(\"--since\", i.toEpochMilli.toString)\n      } getOrElse Seq.empty\n    } ++ {\n      full map { r =>\n        Seq(\"--full\")\n      } getOrElse Seq.empty\n    } ++ {\n      nameSort map { n =>\n        Seq(\"--name-sort\")\n      } getOrElse Seq.empty\n    }\n    wsk.cli(\n      wp.overrides ++ params,\n      expectedExitCode,\n      showCmd = true,\n      env = Map(\"WSK_CONFIG_FILE\" -> cliCfgFile.getOrElse(\"\")))\n  }\n\n  /**\n   * Retieves an API's configuration. Parameters mirror those available in the CLI.\n   * Runs a command wsk [params] where the arguments come in as a sequence.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def get(basepathOrApiName: Option[String] = None,\n                   full: Option[Boolean] = None,\n                   expectedExitCode: Int = SUCCESS_EXIT,\n                   cliCfgFile: Option[String] = None,\n                   format: Option[String] = None)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"get\", \"--auth\", wp.authKey) ++ {\n      basepathOrApiName map { b =>\n        Seq(b)\n      } getOrElse Seq.empty\n    } ++ {\n      full map { f =>\n        if (f) Seq(\"--full\") else Seq.empty\n      } getOrElse Seq.empty\n    } ++ {\n      format map { ft =>\n        Seq(\"--format\", ft)\n      } getOrElse Seq.empty\n    }\n    wsk.cli(\n      wp.overrides ++ params,\n      expectedExitCode,\n      showCmd = true,\n      env = Map(\"WSK_CONFIG_FILE\" -> cliCfgFile.getOrElse(\"\")))\n  }\n\n  /**\n   * Delete an entire API or a subset of API endpoints. Parameters mirror those available in the CLI.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def delete(basepathOrApiName: String,\n                      relpath: Option[String] = None,\n                      operation: Option[String] = None,\n                      expectedExitCode: Int = SUCCESS_EXIT,\n                      cliCfgFile: Option[String] = None)(implicit wp: WskProps): RunResult = {\n    val params = Seq(noun, \"delete\", \"--auth\", wp.authKey, basepathOrApiName) ++ {\n      relpath map { r =>\n        Seq(r)\n      } getOrElse Seq.empty\n    } ++ {\n      operation map { o =>\n        Seq(o)\n      } getOrElse Seq.empty\n    }\n    wsk.cli(\n      wp.overrides ++ params,\n      expectedExitCode,\n      showCmd = true,\n      env = Map(\"WSK_CONFIG_FILE\" -> cliCfgFile.getOrElse(\"\")))\n  }\n}\n\nobject Wsk {\n  val binaryName = \"wsk\"\n  val defaultCliPath = WhiskProperties.getCLIPath\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/WskOperations.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport java.io.BufferedWriter\nimport java.io.File\nimport java.io.FileWriter\nimport java.time.Instant\n\nimport scala.concurrent.duration.DurationInt\nimport scala.concurrent.duration.Duration\nimport scala.language.postfixOps\nimport TestUtils._\nimport spray.json._\nimport org.apache.openwhisk.core.entity.ByteSize\n\nimport scala.util.Try\n\ncase class WskProps(\n  authKey: String = WhiskProperties.getAuthKeyForTesting,\n  cert: String =\n    WhiskProperties.getFileRelativeToWhiskHome(\"ansible/roles/nginx/files/openwhisk-client-cert.pem\").getAbsolutePath,\n  key: String =\n    WhiskProperties.getFileRelativeToWhiskHome(\"ansible/roles/nginx/files/openwhisk-client-key.pem\").getAbsolutePath,\n  namespace: String = \"_\",\n  apiversion: String = \"v1\",\n  apihost: String = WhiskProperties.getEdgeHost,\n  token: String = \"\",\n  basicAuth: Boolean = true) {\n  def overrides = Seq(\"-i\", \"--apihost\", apihost, \"--apiversion\", apiversion)\n  def writeFile(propsfile: File) = {\n    val propsStr = s\"\"\"NAMESPACE=$namespace\n                  |APIVERSION=$apiversion\n                  |AUTH=$authKey\n                  |APIHOST=$apihost\n                  |APIGW_ACCESS_TOKEN=$token\"\"\".stripMargin\n    val bw = new BufferedWriter(new FileWriter(propsfile))\n    try {\n      bw.write(propsStr)\n    } finally {\n      bw.close()\n    }\n  }\n}\n\ntrait WaitFor {\n\n  /**\n   * Waits up to totalWait seconds for a 'step' to return value.\n   * Often tests call this routine immediately after starting work.\n   * Performs an initial wait before entering poll loop.\n   */\n  def waitfor[T](step: () => T,\n                 initialWait: Duration = 1 second,\n                 pollPeriod: Duration = 1 second,\n                 totalWait: Duration = 30 seconds): T = {\n    Thread.sleep(initialWait.toMillis)\n    val endTime = System.currentTimeMillis() + totalWait.toMillis\n    while (System.currentTimeMillis() < endTime) {\n      val predicate = step()\n      predicate match {\n        case (t: Boolean) if t =>\n          return predicate\n        case (t: Any) if t != null && !t.isInstanceOf[Boolean] =>\n          return predicate\n        case _ if System.currentTimeMillis() >= endTime =>\n          return predicate\n        case _ =>\n          Thread.sleep(pollPeriod.toMillis)\n      }\n    }\n    null.asInstanceOf[T]\n  }\n}\n\ntrait HasActivation {\n\n  /**\n   * Extracts activation id from invoke (action or trigger) or activation get\n   */\n  def extractActivationId(result: RunResult): Option[String] = {\n    Try {\n      // try to interpret the run result as the result of an invoke\n      extractActivationIdFromInvoke(result) getOrElse extractActivationIdFromActivation(result).get\n    } toOption\n  }\n\n  /**\n   * Extracts activation id from 'wsk activation get' run result\n   */\n  private def extractActivationIdFromActivation(result: RunResult): Option[String] = {\n    Try {\n      // a characteristic string that comes right before the activationId\n      val idPrefix = \"ok: got activation \"\n      val output = if (result.exitCode != SUCCESS_EXIT) result.stderr else result.stdout\n      assert(output.contains(idPrefix), output)\n      extractActivationId(idPrefix, output).get\n    } toOption\n  }\n\n  /**\n   * Extracts activation id from 'wsk action invoke' or 'wsk trigger invoke'\n   */\n  private def extractActivationIdFromInvoke(result: RunResult): Option[String] = {\n    Try {\n      val output = if (result.exitCode != SUCCESS_EXIT) result.stderr else result.stdout\n      assert(output.contains(\"ok: invoked\") || output.contains(\"ok: triggered\"), output)\n      // a characteristic string that comes right before the activationId\n      val idPrefix = \"with id \"\n      extractActivationId(idPrefix, output).get\n    } toOption\n  }\n\n  /**\n   * Extracts activation id preceded by a prefix (idPrefix) from a string (output)\n   *\n   * @param idPrefix the prefix of the activation id\n   * @param output the string to be used in the extraction\n   * @return an option containing the id as a string or None if the extraction failed for any reason\n   */\n  private def extractActivationId(idPrefix: String, output: String): Option[String] = {\n    Try {\n      val start = output.indexOf(idPrefix) + idPrefix.length\n      var end = start\n      assert(start > 0)\n      while (end < output.length && output.charAt(end) != '\\n') end = end + 1\n      output.substring(start, end) // a uuid\n    } toOption\n  }\n}\n\ntrait WskOperations {\n  val action: ActionOperations\n  val trigger: TriggerOperations\n  val rule: RuleOperations\n  val activation: ActivationOperations\n  val pkg: PackageOperations\n  val namespace: NamespaceOperations\n  val api: GatewayOperations\n\n  /**\n   * Utility function which strips the leading line if it ends in a newline (present when output is from\n   * wsk CLI) and parses the rest as a JSON object.\n   */\n  def parseJsonString(jsonStr: String): JsObject = WskOperations.parseJsonString(jsonStr)\n}\n\nobject WskOperations {\n\n  /**\n   * Utility function which strips the leading line if it ends in a newline (present when output is from\n   * wsk CLI) and parses the rest as a JSON object.\n   */\n  def parseJsonString(jsonStr: String): JsObject = {\n    jsonStr.substring(jsonStr.indexOf(\"\\n\") + 1).parseJson.asJsObject // Skip optional status line before parsing\n  }\n}\n\ntrait ListOrGetFromCollectionOperations {\n\n  protected val noun: String\n\n  /**\n   * List entities in collection.\n   *\n   * @param namespace (optional) if specified must be  fully qualified namespace\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  def list(namespace: Option[String] = None,\n           limit: Option[Int] = None,\n           nameSort: Option[Boolean] = None,\n           expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n\n  /**\n   * Gets entity from collection.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  def get(name: String,\n          expectedExitCode: Int = SUCCESS_EXIT,\n          summary: Boolean = false,\n          fieldFilter: Option[String] = None,\n          url: Option[Boolean] = None,\n          save: Option[Boolean] = None,\n          saveAs: Option[String] = None)(implicit wp: WskProps): RunResult\n}\n\ntrait DeleteFromCollectionOperations {\n\n  protected val noun: String\n\n  /**\n   * Deletes entity from collection.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  def delete(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n\n  /**\n   * Deletes entity from collection but does not assert that the command succeeds.\n   * Use this if deleting an entity that may not exist and it is OK if it does not.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   */\n  def sanitize(name: String)(implicit wp: WskProps): RunResult\n}\n\ntrait ActionOperations extends DeleteFromCollectionOperations with ListOrGetFromCollectionOperations {\n\n  def create(name: String,\n             artifact: Option[String],\n             kind: Option[String] = None,\n             main: Option[String] = None,\n             docker: Option[String] = None,\n             parameters: Map[String, JsValue] = Map.empty,\n             annotations: Map[String, JsValue] = Map.empty,\n             delAnnotations: Array[String] = Array(),\n             parameterFile: Option[String] = None,\n             annotationFile: Option[String] = None,\n             timeout: Option[Duration] = None,\n             memory: Option[ByteSize] = None,\n             logsize: Option[ByteSize] = None,\n             concurrency: Option[Int] = None,\n             shared: Option[Boolean] = None,\n             update: Boolean = false,\n             web: Option[String] = None,\n             websecure: Option[String] = None,\n             expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n\n  def invoke(name: String,\n             parameters: Map[String, JsValue] = Map.empty,\n             parameterFile: Option[String] = None,\n             blocking: Boolean = false,\n             result: Boolean = false,\n             expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n}\n\ntrait PackageOperations extends DeleteFromCollectionOperations with ListOrGetFromCollectionOperations {\n\n  def create(name: String,\n             parameters: Map[String, JsValue] = Map.empty,\n             annotations: Map[String, JsValue] = Map.empty,\n             parameterFile: Option[String] = None,\n             annotationFile: Option[String] = None,\n             shared: Option[Boolean] = None,\n             update: Boolean = false,\n             expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n\n  def bind(provider: String,\n           name: String,\n           parameters: Map[String, JsValue] = Map.empty,\n           annotations: Map[String, JsValue] = Map.empty,\n           expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n}\n\ntrait TriggerOperations extends DeleteFromCollectionOperations with ListOrGetFromCollectionOperations {\n\n  def create(name: String,\n             parameters: Map[String, JsValue] = Map.empty,\n             annotations: Map[String, JsValue] = Map.empty,\n             parameterFile: Option[String] = None,\n             annotationFile: Option[String] = None,\n             feed: Option[String] = None,\n             shared: Option[Boolean] = None,\n             update: Boolean = false,\n             expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n\n  def fire(name: String,\n           parameters: Map[String, JsValue] = Map.empty,\n           parameterFile: Option[String] = None,\n           expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n}\n\ntrait RuleOperations extends DeleteFromCollectionOperations with ListOrGetFromCollectionOperations {\n\n  def create(name: String,\n             trigger: String,\n             action: String,\n             annotations: Map[String, JsValue] = Map.empty,\n             shared: Option[Boolean] = None,\n             update: Boolean = false,\n             expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n\n  def enable(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n\n  def disable(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n\n  def state(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult\n}\n\ntrait ActivationOperations {\n\n  def extractActivationId(result: RunResult): Option[String]\n\n  def pollFor(N: Int,\n              entity: Option[String],\n              limit: Option[Int] = None,\n              since: Option[Instant] = None,\n              skip: Option[Int] = None,\n              retries: Int,\n              pollPeriod: Duration = 1.second)(implicit wp: WskProps): Seq[String]\n\n  def waitForActivation(activationId: String, initialWait: Duration, pollPeriod: Duration, totalWait: Duration)(\n    implicit wp: WskProps): Either[String, JsObject]\n\n  def get(activationId: Option[String] = None,\n          expectedExitCode: Int = SUCCESS_EXIT,\n          fieldFilter: Option[String] = None,\n          last: Option[Boolean] = None,\n          summary: Option[Boolean] = None)(implicit wp: WskProps): RunResult\n\n  def console(duration: Duration,\n              since: Option[Duration] = None,\n              expectedExitCode: Int = SUCCESS_EXIT,\n              actionName: Option[String] = None)(implicit wp: WskProps): RunResult\n\n  def logs(activationId: Option[String] = None, expectedExitCode: Int = SUCCESS_EXIT, last: Option[Boolean] = None)(\n    implicit wp: WskProps): RunResult\n\n  def result(activationId: Option[String] = None, expectedExitCode: Int = SUCCESS_EXIT, last: Option[Boolean] = None)(\n    implicit wp: WskProps): RunResult\n}\n\ntrait NamespaceOperations {\n\n  def list(expectedExitCode: Int = SUCCESS_EXIT, nameSort: Option[Boolean] = None)(implicit wp: WskProps): RunResult\n\n  def whois()(implicit wskprops: WskProps): String\n}\n\ntrait GatewayOperations {\n\n  def create(basepath: Option[String] = None,\n             relpath: Option[String] = None,\n             operation: Option[String] = None,\n             action: Option[String] = None,\n             apiname: Option[String] = None,\n             swagger: Option[String] = None,\n             responsetype: Option[String] = None,\n             expectedExitCode: Int = SUCCESS_EXIT,\n             cliCfgFile: Option[String] = None)(implicit wp: WskProps): RunResult\n\n  def list(basepathOrApiName: Option[String] = None,\n           relpath: Option[String] = None,\n           operation: Option[String] = None,\n           limit: Option[Int] = None,\n           since: Option[Instant] = None,\n           full: Option[Boolean] = None,\n           nameSort: Option[Boolean] = None,\n           expectedExitCode: Int = SUCCESS_EXIT,\n           cliCfgFile: Option[String] = None)(implicit wp: WskProps): RunResult\n\n  def get(basepathOrApiName: Option[String] = None,\n          full: Option[Boolean] = None,\n          expectedExitCode: Int = SUCCESS_EXIT,\n          cliCfgFile: Option[String] = None,\n          format: Option[String] = None)(implicit wp: WskProps): RunResult\n\n  def delete(basepathOrApiName: String,\n             relpath: Option[String] = None,\n             operation: Option[String] = None,\n             expectedExitCode: Int = SUCCESS_EXIT,\n             cliCfgFile: Option[String] = None)(implicit wp: WskProps): RunResult\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/WskTestHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport java.time.Instant\n\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.collection.mutable.ListBuffer\nimport scala.util.Failure\nimport scala.util.Try\nimport scala.concurrent.duration.Duration\nimport scala.concurrent.duration.DurationInt\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\nimport TestUtils.RunResult\nimport TestUtils.SUCCESS_EXIT\nimport TestUtils.CONFLICT\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\n\nobject FullyQualifiedNames {\n\n  /**\n   * Fully qualifies the name of an entity with its namespace.\n   * If the name already starts with the PATHSEP character, then\n   * it already is fully qualified. Otherwise (package name or\n   * basic entity name) it is prefixed with the namespace. The\n   * namespace is derived from the implicit whisk properties.\n   *\n   * @param name to fully qualify iff it is not already fully qualified\n   * @param wp whisk properties\n   * @return name if it is fully qualified else a name fully qualified for a namespace\n   */\n  def fqn(name: String)(implicit wp: WskProps) = {\n    val sep = \"/\" // Namespace.PATHSEP\n    if (name.startsWith(sep) || name.count(_ == sep(0)) == 2) name\n    else s\"$sep${wp.namespace}$sep$name\"\n  }\n\n  /**\n   * Resolves a namespace. If argument is defined, it takes precedence.\n   * else resolve to namespace in implicit WskProps.\n   *\n   * @param namespace an optional namespace\n   * @param wp whisk properties\n   * @return resolved namespace\n   */\n  def resolve(namespace: Option[String])(implicit wp: WskProps) = {\n    val sep = \"/\" // Namespace.PATHSEP\n    namespace getOrElse s\"$sep${wp.namespace}\"\n  }\n}\n\n/**\n * An arbitrary response of a whisk action. Includes the result as a JsObject as the\n * structure of \"result\" is not defined.\n *\n * @param result a JSON object used to save the result of the execution of the action\n * @param status a string used to indicate the status of the action\n * @param success a boolean value used to indicate whether the action is executed successfully or not\n */\ncase class ActivationResponse(result: Option[JsValue], status: String, success: Boolean)\n\nobject ActivationResponse extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat3(ActivationResponse.apply)\n}\n\n/**\n * Activation record as it is returned from the OpenWhisk service.\n *\n * @param activationId a String to save the ID of the activation\n * @param logs a list of String to save the logs of the activation\n * @param response an Object of ActivationResponse to save the response of the activation\n * @param start an Instant to save the start time of activation\n * @param end an Instant to save the end time of activation\n * @param duration a Long to save the duration of the activation\n * @param cause String to save the cause of failure if the activation fails\n * @param annotations a list of JSON objects to save the annotations of the activation\n */\ncase class ActivationResult(activationId: String,\n                            namespace: String,\n                            logs: Option[List[String]],\n                            response: ActivationResponse,\n                            start: Instant,\n                            end: Instant,\n                            duration: Long,\n                            cause: Option[String],\n                            annotations: Option[List[JsObject]]) {\n\n  def getAnnotationValue(key: String): Option[JsValue] =\n    annotations\n      .flatMap(_.find(_.fields(\"key\").convertTo[String] == key))\n      .map(_.fields(\"value\"))\n}\n\nobject ActivationResult extends DefaultJsonProtocol {\n  private implicit val instantSerdes = new RootJsonFormat[Instant] {\n    def write(t: Instant) = t.toEpochMilli.toJson\n\n    def read(value: JsValue) =\n      Try {\n        value match {\n          case JsNumber(i) => Instant.ofEpochMilli(i.bigDecimal.longValue)\n          case _           => deserializationError(\"timestamp malformed\")\n        }\n      } getOrElse deserializationError(\"timestamp malformed 2\")\n  }\n\n  implicit val serdes = new RootJsonFormat[ActivationResult] {\n    private val format = jsonFormat9(ActivationResult.apply)\n\n    def write(result: ActivationResult) = format.write(result)\n\n    def read(value: JsValue) = {\n      val obj = value.asJsObject\n      obj.getFields(\"activationId\", \"namespace\", \"response\", \"start\") match {\n        case Seq(JsString(activationId), JsString(namespace), response, start) =>\n          Try {\n            val logs = obj.fields.get(\"logs\").map(_.convertTo[List[String]])\n            val end = obj.fields.get(\"end\").map(_.convertTo[Instant]).getOrElse(Instant.EPOCH)\n            val duration = obj.fields.get(\"duration\").map(_.convertTo[Long]).getOrElse(0L)\n            val cause = obj.fields.get(\"cause\").map(_.convertTo[String])\n            val annotations = obj.fields.get(\"annotations\").map(_.convertTo[List[JsObject]])\n            new ActivationResult(\n              activationId,\n              namespace,\n              logs,\n              response.convertTo[ActivationResponse],\n              start.convertTo[Instant],\n              end,\n              duration,\n              cause,\n              annotations)\n          } getOrElse deserializationError(\"Failed to deserialize the activation result.\")\n        case _ => deserializationError(\"Failed to deserialize the activation ID, response or start.\")\n      }\n    }\n  }\n}\n\n/** The result of a rule-activation written into the trigger activation */\ncase class RuleActivationResult(statusCode: Int, success: Boolean, activationId: String, action: String)\nobject RuleActivationResult extends DefaultJsonProtocol {\n  implicit val serdes = jsonFormat4(RuleActivationResult.apply)\n}\n\n/**\n * Test fixture to ease cleaning of whisk entities created during testing.\n *\n * The fixture records the entities created during a test and when the test\n * completed, will delete them all.\n */\ntrait WskTestHelpers extends Matchers {\n  type Assets = ListBuffer[(DeleteFromCollectionOperations, String, Boolean)]\n\n  /**\n   * Helper to register an entity to delete once a test completes.\n   * The helper sanitizes (deletes) a previous instance of the entity if it exists\n   * in given collection.\n   *\n   */\n  class AssetCleaner(assetsToDeleteAfterTest: Assets, wskprops: WskProps) {\n    def withCleaner[T <: DeleteFromCollectionOperations](cli: T, name: String, confirmDelete: Boolean = true)(\n      cmd: (T, String) => RunResult): RunResult = {\n      // sanitize (delete) if asset exists\n      cli.sanitize(name)(wskprops)\n\n      assetsToDeleteAfterTest += ((cli, name, confirmDelete))\n      cmd(cli, name)\n    }\n  }\n\n  /**\n   * Creates a test closure which records all entities created inside the test into a\n   * list that is iterated at the end of the test so that these entities are deleted\n   * (from most recently created to oldest).\n   */\n  def withAssetCleaner[T](wskprops: WskProps)(test: (WskProps, AssetCleaner) => T): T = {\n    // create new asset list to track what must be deleted after test completes\n    val assetsToDeleteAfterTest = new Assets()\n\n    try {\n      test(wskprops, new AssetCleaner(assetsToDeleteAfterTest, wskprops))\n    } catch {\n      case t: Throwable =>\n        // log the exception that occurred in the test and rethrow it\n        println(s\"Exception occurred during test execution: $t\")\n        t.printStackTrace()\n        throw t\n    } finally {\n      // delete assets in reverse order so that was created last is deleted first\n      val deletedAll = assetsToDeleteAfterTest.reverse map {\n        case (cli, n, delete) =>\n          n -> Try {\n            cli match {\n              case _: PackageOperations if delete =>\n                // sanitize ignores the exit code, so we can inspect the actual result and retry accordingly\n                val rr = cli.sanitize(n)(wskprops)\n                rr.exitCode match {\n                  case CONFLICT | StatusCodes.Conflict.intValue =>\n                    org.apache.openwhisk.utils.retry({\n                      println(\"package deletion conflict, view computation delay likely, retrying...\")\n                      cli.delete(n)(wskprops)\n                    }, 5, Some(1.second))\n                  case _ => rr\n                }\n              case _ => if (delete) cli.delete(n)(wskprops) else cli.sanitize(n)(wskprops)\n            }\n          }\n      } forall {\n        case (n, Failure(t)) =>\n          println(s\"ERROR: deleting asset failed for $n: $t\")\n          false\n        case _ =>\n          true\n      }\n      assert(deletedAll, \"some assets were not deleted\")\n    }\n  }\n\n  /**\n   * Extracts an activation id from a wsk command producing a RunResult with such an id.\n   * If id is found, polls activations until one matching id is found. If found, pass\n   * the activation to the post processor which then check for expected values.\n   */\n  def withActivation(\n    wsk: ActivationOperations,\n    run: RunResult,\n    initialWait: Duration = 1.second,\n    pollPeriod: Duration = 1.second,\n    totalWait: Duration = 120.seconds)(check: ActivationResult => Unit)(implicit wskprops: WskProps): Unit = {\n    val activationId = wsk.extractActivationId(run)\n\n    withClue(s\"did not find an activation id in '$run'\") {\n      activationId shouldBe a[Some[_]]\n    }\n\n    withActivation(wsk, activationId.get, initialWait, pollPeriod, totalWait)(check)\n  }\n\n  /**\n   * Polls activations until one matching id is found. If found, pass\n   * the activation to the post processor which then check for expected values.\n   */\n  def withActivation(wsk: ActivationOperations,\n                     activationId: String,\n                     initialWait: Duration,\n                     pollPeriod: Duration,\n                     totalWait: Duration)(check: ActivationResult => Unit)(implicit wskprops: WskProps): Unit = {\n    val id = activationId\n    val activation = wsk.waitForActivation(id, initialWait, pollPeriod, totalWait)\n\n    activation match {\n      case Left(reason) => fail(s\"error waiting for activation $id for $totalWait: $reason\")\n      case Right(result) =>\n        withRethrowingPrint(s\"check failed for activation $id: $result\") {\n          check(result.convertTo[ActivationResult])\n        }\n    }\n  }\n  def withActivation(wsk: ActivationOperations, activationId: String)(check: ActivationResult => Unit)(\n    implicit wskprops: WskProps): Unit = {\n    withActivation(wsk, activationId, 1.second, 1.second, 120.seconds)(check)\n  }\n\n  /**\n   * In the case that test throws an exception, print stderr and stdout\n   * from the provided RunResult.\n   */\n  def withPrintOnFailure(runResult: RunResult)(test: () => Unit): Unit = {\n    try {\n      test()\n    } catch {\n      case error: Throwable =>\n        println(s\"[stderr] ${runResult.stderr}\")\n        println(s\"[stdout] ${runResult.stdout}\")\n        throw error\n    }\n  }\n\n  /**\n   * Prints the given information iff the inner test fails. Rethrows the tests exception to get a meaningful\n   * stacktrace.\n   *\n   * @param information additional information to print\n   * @param test test to run\n   */\n  def withRethrowingPrint(information: String)(test: => Unit): Unit = {\n    try test\n    catch {\n      case error: Throwable =>\n        println(information)\n        throw error\n    }\n  }\n\n  def getAdditionalTestSubject(newUser: String): WskProps = {\n    import WskAdmin.wskadmin\n    WskProps(namespace = newUser, authKey = wskadmin.cli(Seq(\"user\", \"create\", newUser)).stdout.trim)\n  }\n\n  def disposeAdditionalTestSubject(subject: String, expectedExitCode: Int = SUCCESS_EXIT): Unit = {\n    import WskAdmin.wskadmin\n    withClue(s\"failed to delete temporary subject $subject\") {\n      wskadmin.cli(Seq(\"user\", \"delete\", subject), expectedExitCode).stdout should include(\"Subject deleted\")\n    }\n  }\n\n  /** Appends the current timestamp in ms. */\n  def withTimestamp(text: String) = s\"${text}-${System.currentTimeMillis}\"\n\n  /** Strips the first line if it ends in a new line as is common for CLI output. */\n  def removeCLIHeader(response: String): String = {\n    if (response.contains(\"\\n\")) response.substring(response.indexOf(\"\\n\")) else response\n  }\n\n  // using annotation will cause compile errors because we use -Xfatal-warnings\n  // @deprecated(message = \"use wsk.parseJsonString instead\", since = \"pr #3741\")\n  def getJSONFromResponse(response: String, isCli: Boolean = false): JsObject = {\n    println(\"!!! WARNING: method is deprecated; use wsk.parseJsonString instead\")\n    if (isCli) removeCLIHeader(response).parseJson.asJsObject else response.parseJson.asJsObject\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/WskTracingTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common\n\nimport io.opentracing.Span\nimport io.opentracing.mock.{MockSpan, MockTracer}\nimport com.github.benmanes.caffeine.cache.Ticker\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.common.{LoggingMarkers, TransactionId}\nimport org.apache.openwhisk.common.tracing.{OpenTracer, TracingConfig}\nimport org.apache.openwhisk.core.ConfigKeys\n\nimport scala.ref.WeakReference\nimport org.scalatest.TestData\nimport org.scalatest.matchers.should.Matchers\nimport scala.collection.JavaConverters._\n\n@RunWith(classOf[JUnitRunner])\nclass WskTracingTests extends TestHelpers with Matchers {\n\n  val tracer: MockTracer = new MockTracer()\n  val tracingConfig = loadConfigOrThrow[TracingConfig](ConfigKeys.tracing)\n  val ticker = new FakeTicker(System.nanoTime())\n  val openTracer = new OpenTracer(tracer, tracingConfig, ticker)\n\n  override def beforeEach(td: TestData): Unit = {\n    super.beforeEach(td)\n    tracer.reset()\n  }\n\n  it should \"create span and context and invalidate cache after expiry\" in {\n    val transactionId: TransactionId = TransactionId.testing\n    var list: List[WeakReference[Span]] = List.empty\n\n    openTracer.startSpan(LoggingMarkers.CONTROLLER_ACTIVATION, transactionId)\n    var ctx = openTracer.getTraceContext(transactionId)\n    openTracer.setTraceContext(transactionId, ctx)\n    ctx should be(defined)\n\n    //advance ticker\n    ticker.time = System.nanoTime() + (tracingConfig.cacheExpiry.toNanos + 100)\n    ctx = openTracer.getTraceContext(transactionId)\n    ctx should not be (defined)\n    openTracer.startSpan(LoggingMarkers.CONTROLLER_KAFKA, transactionId)\n    openTracer.finishSpan(transactionId)\n    val finishedSpans = tracer.finishedSpans()\n    finishedSpans should have size 1\n    //no parent for new span as cache expiry cleared spanMap and contextMap\n    finishedSpans.get(0).parentId() should be(0)\n  }\n\n  it should \"create a finished span\" in {\n    val transactionId: TransactionId = TransactionId.testing\n    openTracer.startSpan(LoggingMarkers.CONTROLLER_ACTIVATION, transactionId)\n    openTracer.finishSpan(transactionId)\n    val finishedSpans = tracer.finishedSpans()\n    finishedSpans should have size 1\n  }\n\n  it should \"put error message into span\" in {\n    val transactionId = TransactionId.testing\n    val errorMessage = \"dummy error message\"\n    openTracer.startSpan(LoggingMarkers.CONTROLLER_ACTIVATION, transactionId)\n    openTracer.error(transactionId, errorMessage)\n    val errorSpans = tracer.finishedSpans()\n    errorSpans should have size 1\n    val spanMap = errorSpans.get(0).tags().asScala\n    spanMap.get(\"error\") should be(Some(true))\n    spanMap.get(\"message\") should be(Some(errorMessage))\n  }\n\n  it should \"create a child span\" in {\n    val transactionId: TransactionId = TransactionId.testing\n    openTracer.startSpan(LoggingMarkers.CONTROLLER_ACTIVATION, transactionId)\n    openTracer.startSpan(LoggingMarkers.CONTROLLER_KAFKA, transactionId)\n    openTracer.finishSpan(transactionId)\n    openTracer.finishSpan(transactionId)\n    val finishedSpans = tracer.finishedSpans()\n    finishedSpans should have size 2\n    val parent: MockSpan = finishedSpans.get(1)\n    val child: MockSpan = finishedSpans.get(0)\n    child.parentId should be(parent.context().spanId)\n  }\n\n  it should \"create a span with tag\" in {\n    val transactionId: TransactionId = TransactionId.testing\n    openTracer.startSpan(LoggingMarkers.CONTROLLER_ACTIVATION, transactionId)\n    openTracer.finishSpan(transactionId)\n    val finishedSpans = tracer.finishedSpans()\n    finishedSpans should have size 1\n    val mockSpan: MockSpan = finishedSpans.get(0)\n    mockSpan.tags should not be null\n    mockSpan.tags should have size 1\n  }\n\n  it should \"create a valid trace context and use it\" in {\n    val transactionId: TransactionId = TransactionId.testing\n    openTracer.startSpan(LoggingMarkers.CONTROLLER_ACTIVATION, transactionId)\n    val context = openTracer.getTraceContext(transactionId)\n    openTracer.finishSpan(transactionId)\n    tracer.reset()\n    //use context for new span\n    openTracer.setTraceContext(transactionId, context)\n    openTracer.startSpan(LoggingMarkers.CONTROLLER_KAFKA, transactionId)\n    openTracer.finishSpan(transactionId)\n    val finishedSpans = tracer.finishedSpans()\n    finishedSpans should have size 1\n    val child: MockSpan = finishedSpans.get(0)\n    //This child span should have a parent as we have set trace context\n    child.parentId should be > 0L\n  }\n}\n\nclass FakeTicker(var time: Long) extends Ticker {\n  override def read() = {\n    time\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/rest/SwaggerValidator.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common.rest\n\nimport scala.collection.JavaConverters._\n\nimport org.apache.pekko.http.scaladsl.model.HttpEntity\nimport org.apache.pekko.http.scaladsl.model.HttpRequest\nimport org.apache.pekko.http.scaladsl.model.HttpResponse\nimport com.atlassian.oai.validator.SwaggerRequestResponseValidator\nimport com.atlassian.oai.validator.model.SimpleRequest\nimport com.atlassian.oai.validator.model.SimpleResponse\nimport com.atlassian.oai.validator.report.ValidationReport\nimport com.atlassian.oai.validator.whitelist.ValidationErrorsWhitelist\nimport com.atlassian.oai.validator.whitelist.rule.WhitelistRules\nimport com.fasterxml.jackson.core.StreamReadConstraints\n\ntrait SwaggerValidator {\n  // Configure Jackson's default constraints globally for the test JVM\n  // Jackson 2.15+ has a 20MB default limit, but OpenWhisk allows 48MB action code\n  // Set to 100MB for safety margin - this applies to all Jackson instances in tests\n  StreamReadConstraints.overrideDefaultStreamReadConstraints(\n    StreamReadConstraints\n      .builder()\n      .maxStringLength(104857600) // 100MB\n      .build())\n\n  private val specWhitelist = ValidationErrorsWhitelist\n    .create()\n    .withRule(\n      \"Ignore action and trigger payloads\",\n      WhitelistRules.allOf(\n        WhitelistRules.messageContains(\"Object instance has properties which are not allowed by the schema\"),\n        WhitelistRules.anyOf(\n          WhitelistRules.pathContains(\"/web/\"),\n          WhitelistRules.pathContains(\"/actions/\"),\n          WhitelistRules.pathContains(\"/triggers/\")),\n        WhitelistRules.methodIs(io.swagger.models.HttpMethod.POST)))\n    .withRule(\n      \"Ignore invalid action kinds\",\n      WhitelistRules.allOf(\n        WhitelistRules.messageContains(\"kind\"),\n        WhitelistRules.messageContains(\"Instance value\"),\n        WhitelistRules.messageContains(\"not found\"),\n        WhitelistRules.pathContains(\"/actions/\"),\n        WhitelistRules.methodIs(io.swagger.models.HttpMethod.PUT)))\n    .withRule(\n      \"Ignore tests that check for invalid DELETEs and PUTs on actions\",\n      WhitelistRules.anyOf(\n        WhitelistRules.messageContains(\"DELETE operation not allowed on path '/api/v1/namespaces/_/actions/'\"),\n        WhitelistRules.messageContains(\"PUT operation not allowed on path '/api/v1/namespaces/_/actions/'\")))\n\n  private val specValidator = SwaggerRequestResponseValidator\n    .createFor(\"apiv1swagger.json\")\n    .withWhitelist(specWhitelist)\n    .build()\n\n  /**\n   * Validate a HTTP request and response against the Swagger spec. Request\n   * and response bodies are passed separately so that this validation\n   * does not have to consume the body content directly from the request\n   * and response, which would prevent callers from later consuming it.\n   *\n   * @param request the HttpRequest\n   * @param response the HttpResponse\n   * @return The list of validation error messages, if any\n   */\n  def validateRequestAndResponse(request: HttpRequest, response: HttpResponse): Seq[String] = {\n    val specRequest = {\n      val builder = new SimpleRequest.Builder(request.method.value, request.uri.path.toString())\n      val body = strictEntityBodyAsString(request.entity)\n      val withBody =\n        if (body.isEmpty) builder\n        else\n          builder\n            .withBody(body)\n            .withHeader(\"content-type\", request.entity.contentType.value)\n      val withHeaders = request.headers.foldLeft(builder)((b, header) => b.withHeader(header.name, header.value))\n      val andQuery =\n        request.uri.query().foldLeft(withHeaders) { case (b, (key, value)) => b.withQueryParam(key, value) }\n      andQuery.build()\n    }\n\n    val specResponse = {\n      val builder = SimpleResponse.Builder\n        .status(response.status.intValue)\n      val body = strictEntityBodyAsString(response.entity)\n      val withBody =\n        if (body.isEmpty) builder\n        else\n          builder\n            .withBody(body)\n            .withHeader(\"content-type\", response.entity.contentType.value)\n      val withHeaders = response.headers.foldLeft(builder)((b, header) => b.withHeader(header.name, header.value))\n      withHeaders.build()\n    }\n\n    specValidator\n      .validate(specRequest, specResponse)\n      .getMessages\n      .asScala\n      .filter(m => m.getLevel == ValidationReport.Level.ERROR)\n      .map(_.toString)\n      .toSeq\n  }\n\n  def strictEntityBodyAsString(entity: HttpEntity): String = entity match {\n    case s: HttpEntity.Strict => s.data.utf8String\n    case _                    => \"\"\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/common/rest/WskRestOperations.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage common.rest\n\nimport java.io.{File, FileInputStream}\nimport java.nio.charset.StandardCharsets\nimport java.security.KeyStore\nimport java.security.cert.X509Certificate\nimport java.time.Instant\nimport java.util.Base64\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.{Http, HttpsConnectionContext}\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.{DELETE, GET, POST, PUT}\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.{Accepted, NotFound, OK}\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.Uri.{Path, Query}\nimport org.apache.pekko.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials, OAuth2BearerToken}\nimport org.apache.pekko.util.ByteString\nimport common.TestUtils.{ANY_ERROR_EXIT, DONTCARE_EXIT, RunResult, SUCCESS_EXIT}\nimport common.rest.SSL.httpsConfig\nimport common.{\n  DeleteFromCollectionOperations,\n  HasActivation,\n  ListOrGetFromCollectionOperations,\n  WaitFor,\n  WhiskProperties,\n  WskProps,\n  _\n}\nimport javax.net.ssl._\nimport org.apache.commons.io.{FileUtils, FilenameUtils}\nimport org.apache.openwhisk.common.Https.HttpsConfig\nimport org.apache.openwhisk.common.{Https, PekkoLogging, TransactionId}\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.utils.retry\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatest.time.Span.convertDurationToSpan\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.collection.immutable.Seq\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.duration.{Duration, DurationInt, FiniteDuration}\nimport scala.language.postfixOps\nimport scala.util.{Failure, Success, Try}\n\nclass AcceptAllHostNameVerifier extends HostnameVerifier {\n  override def verify(s: String, sslSession: SSLSession): Boolean = true\n}\n\nobject SSL {\n\n  lazy val httpsConfig: HttpsConfig = loadConfigOrThrow[HttpsConfig](\"whisk.controller.https\")\n\n  def keyManagers(clientAuth: Boolean): Array[KeyManager] = {\n    if (clientAuth)\n      keyManagersForClientAuth\n    else\n      Array.empty\n  }\n\n  def keyManagersForClientAuth: Array[KeyManager] = {\n    val keyFactoryType = \"SunX509\"\n    val keystorePassword = httpsConfig.keystorePassword.toCharArray\n    val ks: KeyStore = KeyStore.getInstance(httpsConfig.keystoreFlavor)\n    ks.load(new FileInputStream(httpsConfig.keystorePath), httpsConfig.keystorePassword.toCharArray)\n    val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(keyFactoryType)\n    keyManagerFactory.init(ks, keystorePassword)\n    keyManagerFactory.getKeyManagers\n  }\n\n  def nonValidatingContext(clientAuth: Boolean = false): SSLContext = {\n    class IgnoreX509TrustManager extends X509TrustManager {\n      def checkClientTrusted(chain: Array[X509Certificate], authType: String): Unit = ()\n      def checkServerTrusted(chain: Array[X509Certificate], authType: String): Unit = ()\n      def getAcceptedIssuers: Array[X509Certificate] = Array.empty\n    }\n\n    val context = SSLContext.getInstance(\"TLS\")\n\n    context.init(keyManagers(clientAuth), Array(new IgnoreX509TrustManager), null)\n    context\n  }\n\n  def httpsConnectionContext(implicit system: ActorSystem): HttpsConnectionContext = {\n    Https.connectionContextClient(httpsConfig, true)\n  }\n}\n\nobject HttpConnection {\n\n  /**\n   * Returns either the https context that is tailored for self-signed certificates on the controller, or\n   * a default connection context used in Http.SingleRequest\n   *\n   * @param protocol protocol used to communicate with controller API\n   * @param system actor system\n   * @return https connection context\n   */\n  def getContext(protocol: String)(implicit system: ActorSystem): HttpsConnectionContext = {\n    if (protocol == \"https\") {\n      SSL.httpsConnectionContext\n    } else {\n      // supports http\n      Http().defaultClientHttpsContext\n    }\n  }\n}\n\nclass WskRestOperations(implicit actorSytem: ActorSystem) extends WskOperations {\n  override implicit val action: RestActionOperations = new RestActionOperations\n  override implicit val trigger: RestTriggerOperations = new RestTriggerOperations\n  override implicit val rule: RestRuleOperations = new RestRuleOperations\n  override implicit val activation: RestActivationOperations = new RestActivationOperations\n  override implicit val pkg: RestPackageOperations = new RestPackageOperations\n  override implicit val namespace: RestNamespaceOperations = new RestNamespaceOperations\n  override implicit val api: RestGatewayOperations = new RestGatewayOperations\n}\n\ntrait RestListOrGetFromCollectionOperations extends ListOrGetFromCollectionOperations with RunRestCmd {\n  import FullyQualifiedNames.resolve\n\n  /**\n   * List entities in collection.\n   *\n   * @param namespace (optional) if specified must be  fully qualified namespace\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def list(namespace: Option[String] = None,\n                    limit: Option[Int] = None,\n                    nameSort: Option[Boolean] = None,\n                    expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {\n\n    val entPath = namespace map { ns =>\n      val (nspace, name) = getNamespaceEntityName(resolve(namespace))\n      if (name.isEmpty) Path(s\"$basePath/namespaces/$nspace/$noun\")\n      else Path(s\"$basePath/namespaces/$nspace/$noun/$name/\")\n    } getOrElse Path(s\"$basePath/namespaces/${wp.namespace}/$noun\")\n\n    val paramMap: Map[String, String] = Map(\"skip\" -> \"0\", \"docs\" -> true.toString) ++\n      limit.map(l => Map(\"limit\" -> l.toString)).getOrElse(Map.empty)\n\n    val resp = requestEntity(GET, entPath, paramMap)\n    val r = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    validateStatusCode(expectedExitCode, r.statusCode.intValue)\n    r\n  }\n\n  /**\n   * Gets entity from collection.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def get(name: String,\n                   expectedExitCode: Int = OK.intValue,\n                   summary: Boolean = false,\n                   fieldFilter: Option[String] = None,\n                   url: Option[Boolean] = None,\n                   save: Option[Boolean] = None,\n                   saveAs: Option[String] = None)(implicit wp: WskProps): RestResult = {\n    val (ns, entity) = getNamespaceEntityName(name)\n    val entPath = Path(s\"$basePath/namespaces/$ns/$noun/$entity\")\n    val resp = requestEntity(GET, entPath)(wp)\n    val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    validateStatusCode(expectedExitCode, rr.statusCode.intValue)\n    rr\n  }\n}\n\ntrait RestDeleteFromCollectionOperations extends DeleteFromCollectionOperations with RunRestCmd {\n\n  /**\n   * Deletes entity from collection.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def delete(name: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {\n    val (ns, entityName) = getNamespaceEntityName(name)\n    val path = Path(s\"$basePath/namespaces/$ns/$noun/$entityName\")\n    val resp = requestEntity(DELETE, path)(wp)\n    val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    validateStatusCode(expectedExitCode, rr.statusCode.intValue)\n    rr\n  }\n\n  /**\n   * Deletes entity from collection but does not assert that the command succeeds.\n   * Use this if deleting an entity that may not exist and it is OK if it does not.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   */\n  override def sanitize(name: String)(implicit wp: WskProps): RestResult = {\n    delete(name, DONTCARE_EXIT)\n  }\n}\n\ntrait RestActivation extends HasActivation {\n\n  /**\n   * Extracts activation id from invoke (action or trigger) or activation get\n   */\n  override def extractActivationId(result: RunResult): Option[String] = {\n    extractActivationIdFromInvoke(result.asInstanceOf[RestResult])\n  }\n\n  /**\n   * Extracts activation id from 'wsk action invoke' or 'wsk trigger invoke'\n   */\n  private def extractActivationIdFromInvoke(result: RestResult): Option[String] = {\n    if ((result.statusCode == OK) || (result.statusCode == Accepted))\n      Some(result.getField(\"activationId\"))\n    else\n      None\n  }\n}\n\nclass RestActionOperations(implicit val actorSystem: ActorSystem)\n    extends RestListOrGetFromCollectionOperations\n    with RestDeleteFromCollectionOperations\n    with RestActivation\n    with ActionOperations {\n\n  override protected val noun = \"actions\"\n\n  /**\n   * Creates action. Parameters mirror those available in the REST.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(\n    name: String,\n    artifact: Option[String],\n    kind: Option[String] = None, // one of docker, copy, sequence or none for autoselect else an explicit type\n    main: Option[String] = None,\n    docker: Option[String] = None,\n    parameters: Map[String, JsValue] = Map.empty,\n    annotations: Map[String, JsValue] = Map.empty,\n    delAnnotations: Array[String] = Array(),\n    parameterFile: Option[String] = None,\n    annotationFile: Option[String] = None,\n    timeout: Option[Duration] = None,\n    memory: Option[ByteSize] = None,\n    logsize: Option[ByteSize] = None,\n    concurrency: Option[Int] = None,\n    shared: Option[Boolean] = None,\n    update: Boolean = false,\n    web: Option[String] = None,\n    websecure: Option[String] = None,\n    expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {\n\n    val (namespace, actionName) = getNamespaceEntityName(name)\n    val (paramsInput, annosInput) = getParamsAnnos(parameters, annotations, parameterFile, annotationFile, web = web)\n    val (params: Array[JsValue], annos: Array[JsValue], exec: Map[String, JsValue]) = kind match {\n      case Some(k) =>\n        k match {\n          case \"copy\" =>\n            require(artifact.isDefined, \"copy requires an artifact name\")\n            val actionName = entityName(artifact.get)\n            val actionPath = Path(s\"$basePath/namespaces/$namespace/$noun/$actionName\")\n            val resp = requestEntity(GET, actionPath)\n            val result = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n            val params = result.getFieldListJsObject(\"parameters\").toArray[JsValue]\n            val annos = result.getFieldListJsObject(\"annotations\").toArray[JsValue]\n            val exec = result.getFieldJsObject(\"exec\").fields\n            (paramsInput ++ params, annosInput ++ annos, exec)\n\n          case \"sequence\" =>\n            require(artifact.isDefined, \"sequence requires a component list\")\n            val comps = convertIntoComponents(artifact.get)\n            val exec =\n              if (comps.nonEmpty) Map(\"components\" -> comps.toJson, \"kind\" -> k.toJson)\n              else Map(\"kind\" -> k.toJson)\n            (paramsInput, annosInput, exec)\n\n          case _ =>\n            val code = readCodeFromFile(artifact).map(c => Map(\"code\" -> c.toJson)).getOrElse(Map.empty)\n            val exec: Map[String, JsValue] = if (k == \"native\" || k == \"docker\") {\n              require(k == \"native\" && docker.isEmpty || k == \"docker\" && docker.isDefined)\n              Map(\"kind\" -> \"blackbox\".toJson, \"image\" -> docker.getOrElse(\"openwhisk/dockerskeleton\").toJson) ++ code\n            } else {\n              require(artifact.isDefined, \"file name required as an artifact\")\n              Map(\"kind\" -> k.toJson) ++ code\n            }\n\n            (paramsInput, annosInput, exec)\n        }\n\n      case None =>\n        docker\n          .map(_ => \"blackbox\")\n          .orElse {\n            artifact.map { file =>\n              getExt(file) match {\n                case \"js\"    => \"nodejs:default\"\n                case \"py\"    => \"python:default\"\n                case \"swift\" => \"swift:default\"\n                case \"jar\"   => \"java:default\"\n                case _ =>\n                  throw new IllegalStateException(s\"Extension for $file not recognized and kind cannot be inferred.\")\n              }\n            }\n          }\n          .map { k =>\n            val code = readCodeFromFile(artifact).map(c => Map(\"code\" -> c.toJson)).getOrElse(Map.empty)\n            val image = docker.map(i => Map(\"image\" -> i.toJson)).getOrElse(Map.empty)\n            (paramsInput, annosInput, Map(\"kind\" -> k.toJson) ++ code ++ image)\n          }\n          .getOrElse {\n            if (!update && artifact.isDefined)\n              throw new IllegalStateException(\n                s\"Extension for ${artifact.get} not recognized and kind cannot be inferred.\")\n            else (paramsInput, annosInput, Map.empty)\n          }\n    }\n\n    val limits: Map[String, JsValue] = {\n      timeout.map(t => Map(\"timeout\" -> t.toMillis.toJson)).getOrElse(Map.empty) ++\n        logsize.map(log => Map(\"logs\" -> log.toMB.toJson)).getOrElse(Map.empty) ++\n        memory.map(m => Map(\"memory\" -> m.toMB.toJson)).getOrElse(Map.empty) ++\n        concurrency.map(c => Map(\"concurrency\" -> c.toJson)).getOrElse(Map.empty)\n    }\n\n    val body: Map[String, JsValue] = if (!update) {\n      require(exec.nonEmpty, \"exec cannot be empty on create\")\n      Map(\n        \"exec\" -> main.map(m => exec ++ Map(\"main\" -> m.toJson)).getOrElse(exec).toJson,\n        \"parameters\" -> params.toJson,\n        \"annotations\" -> annos.toJson) ++ Map(\"limits\" -> limits.toJson)\n    } else {\n      var content: Map[String, JsValue] = Map.empty\n      if (exec.nonEmpty)\n        content = Map(\"exec\" -> main.map(m => exec ++ Map(\"main\" -> m.toJson)).getOrElse(exec).toJson)\n      if (params.nonEmpty)\n        content = content + (\"parameters\" -> params.toJson)\n      if (annos.nonEmpty)\n        content = content + (\"annotations\" -> annos.toJson)\n      if (limits.nonEmpty)\n        content = content + (\"limits\" -> limits.toJson)\n      if (delAnnotations.nonEmpty)\n        content = content + (\"delAnnotations\" -> delAnnotations.toJson)\n      content\n    }\n\n    val path = Path(s\"$basePath/namespaces/$namespace/$noun/$actionName\")\n    val resp =\n      if (update) requestEntity(PUT, path, Map(\"overwrite\" -> \"true\"), Some(JsObject(body).toString))\n      else requestEntity(PUT, path, body = Some(JsObject(body).toString))\n    val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    validateStatusCode(expectedExitCode, rr.statusCode.intValue)\n    rr\n  }\n\n  override def invoke(name: String,\n                      parameters: Map[String, JsValue] = Map.empty,\n                      parameterFile: Option[String] = None,\n                      blocking: Boolean = false,\n                      result: Boolean = false,\n                      expectedExitCode: Int = Accepted.intValue)(implicit wp: WskProps): RestResult = {\n    super.invokeAction(name, parameters, parameterFile, blocking, result, expectedExitCode = expectedExitCode)\n  }\n\n  private def readCodeFromFile(artifact: Option[String]): Option[String] = {\n    artifact.map { file =>\n      val ext = getExt(file)\n      val isBinary = ext == \"zip\" || ext == \"jar\" || ext == \"bin\"\n      if (!isBinary) {\n        FileUtils.readFileToString(new File(file), StandardCharsets.UTF_8)\n      } else {\n        val zip = FileUtils.readFileToByteArray(new File(file))\n        Base64.getEncoder.encodeToString(zip)\n      }\n    }\n  }\n}\n\nclass RestTriggerOperations(implicit val actorSystem: ActorSystem)\n    extends RestListOrGetFromCollectionOperations\n    with RestDeleteFromCollectionOperations\n    with RestActivation\n    with TriggerOperations {\n\n  override protected val noun = \"triggers\"\n\n  /**\n   * Creates trigger. Parameters mirror those available in the REST.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(name: String,\n                      parameters: Map[String, JsValue] = Map.empty,\n                      annotations: Map[String, JsValue] = Map.empty,\n                      parameterFile: Option[String] = None,\n                      annotationFile: Option[String] = None,\n                      feed: Option[String] = None,\n                      shared: Option[Boolean] = None,\n                      update: Boolean = false,\n                      expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {\n\n    val (ns, triggerName) = getNamespaceEntityName(name)\n    val path = Path(s\"$basePath/namespaces/$ns/$noun/$triggerName\")\n    val (params, annos) = getParamsAnnos(parameters, annotations, parameterFile, annotationFile, feed)\n    var bodyContent: Map[String, JsValue] = Map.empty\n\n    if (!update) {\n      bodyContent =\n        Map(\"publish\" -> shared.getOrElse(false).toJson, \"parameters\" -> params.toJson, \"annotations\" -> annos.toJson)\n    } else {\n      shared.foreach { p =>\n        bodyContent = Map(\"publish\" -> p.toJson)\n      }\n\n      val inputParams = convertMapIntoKeyValue(parameters)\n      if (inputParams.nonEmpty) {\n        bodyContent = bodyContent + (\"parameters\" -> params.toJson)\n      }\n      val inputAnnos = convertMapIntoKeyValue(annotations)\n      if (inputAnnos.nonEmpty) {\n        bodyContent = bodyContent + (\"annotations\" -> annos.toJson)\n      }\n    }\n\n    val resp =\n      if (update) requestEntity(PUT, path, Map(\"overwrite\" -> \"true\"), Some(JsObject(bodyContent).toString))\n      else requestEntity(PUT, path, body = Some(JsObject(bodyContent).toString))\n    val result = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    if (result.statusCode != OK) {\n      validateStatusCode(expectedExitCode, result.statusCode.intValue)\n    }\n    val rr = feed map { f =>\n      // Invoke the feed\n      val (nsFeed, feedName) = getNamespaceEntityName(f)\n      val path = Path(s\"$basePath/namespaces/$nsFeed/actions/$feedName\")\n      val paramMap = Map(\"blocking\" -> \"true\", \"result\" -> \"false\")\n      var body: Map[String, JsValue] = Map(\n        \"lifecycleEvent\" -> \"CREATE\".toJson,\n        \"triggerName\" -> s\"/$ns/$triggerName\".toJson,\n        \"authKey\" -> s\"${wp.authKey}\".toJson)\n      body = body ++ parameters\n      val resp = requestEntity(POST, path, paramMap, Some(body.toJson.toString))\n      val resultInvoke = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n      if ((expectedExitCode != DONTCARE_EXIT) && (expectedExitCode != ANY_ERROR_EXIT))\n        expectedExitCode shouldBe resultInvoke.statusCode.intValue\n      if (resultInvoke.statusCode != OK) {\n        // Remove the trigger, because the feed failed to invoke.\n        delete(triggerName)\n        new RestResult(NotFound, getTransactionId(resp))\n      } else {\n        result\n      }\n    } getOrElse {\n      validateStatusCode(expectedExitCode, result.statusCode.intValue)\n      result\n    }\n    rr\n  }\n\n  /**\n   * Fires trigger. Parameters mirror those available in the REST.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def fire(name: String,\n                    parameters: Map[String, JsValue] = Map.empty,\n                    parameterFile: Option[String] = None,\n                    expectedExitCode: Int = Accepted.intValue)(implicit wp: WskProps): RestResult = {\n    val path = getNamePath(wp.namespace, noun, name)\n    val params = parameterFile map { l =>\n      val input = FileUtils.readFileToString(new File(l), StandardCharsets.UTF_8)\n      input.parseJson.convertTo[Map[String, JsValue]]\n    } getOrElse parameters\n    val resp =\n      if (params.isEmpty) requestEntity(POST, path)\n      else requestEntity(POST, path, body = Some(params.toJson.toString))\n    new RestResult(resp.status.intValue, getTransactionId(resp), getRespData(resp))\n  }\n}\n\nclass RestRuleOperations(implicit val actorSystem: ActorSystem)\n    extends RestListOrGetFromCollectionOperations\n    with RestDeleteFromCollectionOperations\n    with WaitFor\n    with RuleOperations {\n\n  override protected val noun = \"rules\"\n\n  /**\n   * Creates rule. Parameters mirror those available in the REST.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param trigger must be a simple name\n   * @param action must be a simple name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(name: String,\n                      trigger: String,\n                      action: String,\n                      annotations: Map[String, JsValue] = Map.empty,\n                      shared: Option[Boolean] = None,\n                      update: Boolean = false,\n                      expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RestResult = {\n    val path = getNamePath(wp.namespace, noun, name)\n    val annos = convertMapIntoKeyValue(annotations)\n    val published = shared.getOrElse(false)\n    val bodyContent = JsObject(\n      \"trigger\" -> fullEntityName(trigger).toJson,\n      \"action\" -> fullEntityName(action).toJson,\n      \"publish\" -> published.toJson,\n      \"status\" -> \"\".toJson,\n      \"annotations\" -> annos.toJson)\n\n    val resp =\n      if (update) requestEntity(PUT, path, Map(\"overwrite\" -> \"true\"), Some(bodyContent.toString))\n      else requestEntity(PUT, path, body = Some(bodyContent.toString))\n    new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n  }\n\n  /**\n   * Enables rule.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def enable(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RestResult = {\n    changeRuleState(name)\n  }\n\n  /**\n   * Disables rule.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def disable(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RestResult = {\n    changeRuleState(name, \"inactive\")\n  }\n\n  /**\n   * Checks state of rule.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def state(name: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {\n    get(name, expectedExitCode = expectedExitCode)\n  }\n\n  def changeRuleState(ruleName: String, state: String = \"active\")(implicit wp: WskProps): RestResult = {\n    val enName = entityName(ruleName)\n    val path = getNamePath(wp.namespace, noun, enName)\n    val bodyContent = JsObject(\"status\" -> state.toJson)\n    val resp = requestEntity(POST, path, body = Some(bodyContent.toString))\n    new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n  }\n}\n\nclass RestActivationOperations(implicit val actorSystem: ActorSystem)\n    extends ActivationOperations\n    with RunRestCmd\n    with RestActivation\n    with WaitFor {\n\n  protected val noun = \"activations\"\n\n  /**\n   * Activation polling console.\n   *\n   * @param duration exits console after duration\n   * @param since (optional) time travels back to activation since given duration\n   */\n  override def console(duration: Duration,\n                       since: Option[Duration] = None,\n                       expectedExitCode: Int = SUCCESS_EXIT,\n                       actionName: Option[String] = None)(implicit wp: WskProps): RestResult = {\n    require(duration > 1.second, \"duration must be at least 1 second\")\n    val sinceTime = {\n      val now = System.currentTimeMillis\n      since.map(s => now - s.toMillis).getOrElse(now)\n    }\n\n    retry({\n      val result = listActivation(since = Some(Instant.ofEpochMilli(sinceTime)))(wp)\n      if (result.stdout != \"[]\") result else throw new Throwable()\n    }, (duration / 1.second).toInt, Some(1.second))\n  }\n\n  /**\n   * Lists activations.\n   *\n   * @param filter (optional) if define, must be a simple entity name\n   * @param limit (optional) the maximum number of activation to return\n   * @param since (optional) only the activations since this timestamp are included\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  def listActivation(filter: Option[String] = None,\n                     limit: Option[Int] = None,\n                     since: Option[Instant] = None,\n                     skip: Option[Int] = None,\n                     docs: Boolean = true,\n                     expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RestResult = {\n    val entityPath = Path(s\"$basePath/namespaces/${wp.namespace}/$noun\")\n    val paramMap = Map(\"docs\" -> docs.toString) ++\n      skip.map(s => Map(\"skip\" -> s.toString)).getOrElse(Map.empty) ++\n      limit.map(l => Map(\"limit\" -> l.toString)).getOrElse(Map.empty) ++\n      filter.map(f => Map(\"name\" -> f.toString)).getOrElse(Map.empty) ++\n      since.map(s => Map(\"since\" -> s.toEpochMilli.toString)).getOrElse(Map.empty)\n    val resp = requestEntity(GET, entityPath, paramMap)\n    new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n  }\n\n  /**\n   * Parses result of WskActivation.list to extract sequence of activation ids.\n   *\n   * @param rr run result, should be from WhiskActivation.list otherwise behavior is undefined\n   * @return sequence of activations\n   */\n  def idsActivation(rr: RestResult): Seq[String] = {\n    rr.getBodyListJsObject.map(r => RestResult.getField(r, \"activationId\").toString)\n  }\n\n  /**\n   * Gets activation logs by id.\n   *\n   * @param activationId the activation id\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  def activationLogs(activationId: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {\n    val path = Path(s\"$basePath/namespaces/${wp.namespace}/$noun/$activationId/logs\")\n    val resp = requestEntity(GET, path)\n    val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    validateStatusCode(expectedExitCode, rr.statusCode.intValue)\n    rr\n  }\n\n  /**\n   * Gets activation result by id.\n   *\n   * @param activationId the activation id\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  def activationResult(activationId: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {\n    val path = Path(s\"$basePath/namespaces/${wp.namespace}/$noun/$activationId/result\")\n    val resp = requestEntity(GET, path)\n    val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    validateStatusCode(expectedExitCode, rr.statusCode.intValue)\n    rr\n  }\n\n  /**\n   * Polls activations list for at least N activations. The activations\n   * are optionally filtered for the given entity. Will return as soon as\n   * N activations are found. If after retry budget is exhausted, N activations\n   * are still not present, will return a partial result. Hence caller must\n   * check length of the result and not assume it is >= N.\n   *\n   * @param N the number of activations desired\n   * @param entity the name of the entity to filter from activation list\n   * @param limit the maximum number of entities to list (if entity name is not unique use Some(0))\n   * @param since (optional) only the activations since this timestamp are included\n   * @param skip (optional) the number of activations to skip\n   * @param retries the maximum retries (total timeout is retries + 1 seconds)\n   * @return activation ids found, caller must check length of sequence\n   */\n  override def pollFor(N: Int,\n                       entity: Option[String],\n                       limit: Option[Int] = Some(30),\n                       since: Option[Instant] = None,\n                       skip: Option[Int] = Some(0),\n                       retries: Int = 10,\n                       pollPeriod: Duration = 1.second)(implicit wp: WskProps): Seq[String] = {\n    Try {\n      retry({\n        val result =\n          idsActivation(listActivation(filter = entity, limit = limit, since = since, skip = skip, docs = false))\n        if (result.length >= N) result else throw PartialResult(result)\n      }, retries, waitBeforeRetry = Some(pollPeriod))\n    } match {\n      case Success(ids)                => ids\n      case Failure(PartialResult(ids)) => ids\n      case _                           => Seq.empty\n    }\n  }\n\n  override def get(activationId: Option[String],\n                   expectedExitCode: Int = OK.intValue,\n                   fieldFilter: Option[String] = None,\n                   last: Option[Boolean] = None,\n                   summary: Option[Boolean] = None)(implicit wp: WskProps): RestResult = {\n    val actId = activationId match {\n      case Some(_) => activationId\n      case None =>\n        last match {\n          case Some(true) =>\n            val activations = pollFor(N = 1, entity = None, limit = Some(1))\n            require(activations.size <= 1)\n            activations.headOption\n          case _ => None\n        }\n    }\n    val rr = actId match {\n      case Some(id) =>\n        val resp = requestEntity(GET, getNamePath(wp.namespace, noun, id))\n        new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n      case None => new RestResult(NotFound, \"\")\n    }\n    validateStatusCode(expectedExitCode, rr.statusCode.intValue)\n    rr\n  }\n\n  /**\n   * Polls for an activation matching the given id. If found\n   * return Right(activation) else Left(result of calling REST API).\n   *\n   * @return either Left(error message) or Right(activation as JsObject)\n   */\n  override def waitForActivation(activationId: String,\n                                 initialWait: Duration = 1 second,\n                                 pollPeriod: Duration = 1 second,\n                                 totalWait: Duration = 30 seconds)(implicit wp: WskProps): Either[String, JsObject] = {\n    val activation = waitfor(() => {\n      val result = get(Some(activationId), expectedExitCode = DONTCARE_EXIT)(wp)\n      if (result.statusCode == NotFound) {\n        null\n      } else result\n    }, initialWait, pollPeriod, totalWait)\n    Try {\n      assert(activation.statusCode == OK)\n      assert(activation.getField(\"activationId\") != \"\")\n      activation.respBody\n    } map {\n      Right(_)\n    } getOrElse Left(s\"No activation record for'$activationId'\")\n\n  }\n\n  override def logs(activationId: Option[String] = None,\n                    expectedExitCode: Int = OK.intValue,\n                    last: Option[Boolean] = None)(implicit wp: WskProps): RestResult = {\n    val rr = activationId match {\n      case Some(id) =>\n        val resp = requestEntity(GET, getNamePath(wp.namespace, noun, s\"$id/logs\"))\n        new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n\n      case None =>\n        new RestResult(NotFound, \"\")\n    }\n    validateStatusCode(expectedExitCode, rr.statusCode.intValue)\n    rr\n  }\n\n  override def result(activationId: Option[String] = None,\n                      expectedExitCode: Int = OK.intValue,\n                      last: Option[Boolean] = None)(implicit wp: WskProps): RestResult = {\n    val rr = activationId match {\n      case Some(id) =>\n        val resp = requestEntity(GET, getNamePath(wp.namespace, noun, s\"$id/result\"))\n        new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n\n      case None =>\n        new RestResult(NotFound, \"\")\n    }\n    validateStatusCode(expectedExitCode, rr.statusCode.intValue)\n    rr\n  }\n\n  /** Used in polling for activations to record partial results from retry poll. */\n  private case class PartialResult(ids: Seq[String]) extends Throwable\n}\n\nclass RestNamespaceOperations(implicit val actorSystem: ActorSystem) extends NamespaceOperations with RunRestCmd {\n\n  protected val noun = \"namespaces\"\n\n  /**\n   * Lists available namespaces for whisk key.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def list(expectedExitCode: Int = OK.intValue, nameSort: Option[Boolean] = None)(implicit\n                                                                                           wp: WskProps): RestResult = {\n    val entPath = Path(s\"$basePath/namespaces\")\n    val resp = requestEntity(GET, entPath)\n    val result = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    validateStatusCode(expectedExitCode, result.statusCode.intValue)\n    result\n  }\n\n  /**\n   * Looks up namespace for whisk props.\n   *\n   * @param wskprops instance of WskProps with an auth key to lookup\n   * @return namespace as string\n   */\n  override def whois()(implicit wskprops: WskProps): String = {\n    val ns = list().getBodyListString\n    ns.headOption.map(_.toString).getOrElse(\"\")\n  }\n}\n\nclass RestPackageOperations(implicit val actorSystem: ActorSystem)\n    extends RestListOrGetFromCollectionOperations\n    with RestDeleteFromCollectionOperations\n    with PackageOperations {\n\n  override protected val noun = \"packages\"\n\n  /**\n   * Creates package. Parameters mirror those available in the REST.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(name: String,\n                      parameters: Map[String, JsValue] = Map.empty,\n                      annotations: Map[String, JsValue] = Map.empty,\n                      parameterFile: Option[String] = None,\n                      annotationFile: Option[String] = None,\n                      shared: Option[Boolean] = None,\n                      update: Boolean = false,\n                      expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {\n    val path = getNamePath(wp.namespace, noun, name)\n    var bodyContent: Map[String, JsValue] = Map.empty\n\n    val (params, annos) = getParamsAnnos(parameters, annotations, parameterFile, annotationFile)\n    if (!update) {\n      val published = shared.getOrElse(false)\n      bodyContent = Map(\"publish\" -> published.toJson, \"parameters\" -> params.toJson, \"annotations\" -> annos.toJson)\n    } else {\n      shared.foreach { s =>\n        bodyContent = bodyContent + (\"publish\" -> s.toJson)\n      }\n\n      val inputParams = convertMapIntoKeyValue(parameters)\n      if (inputParams.nonEmpty) {\n        bodyContent = bodyContent + (\"parameters\" -> params.toJson)\n      }\n      val inputAnnos = convertMapIntoKeyValue(annotations)\n      if (inputAnnos.nonEmpty) {\n        bodyContent = bodyContent + (\"annotations\" -> annos.toJson)\n      }\n    }\n\n    val resp =\n      if (update) requestEntity(PUT, path, Map(\"overwrite\" -> \"true\"), Some(JsObject(bodyContent).toString))\n      else requestEntity(PUT, path, body = Some(JsObject(bodyContent).toString))\n    val r = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    validateStatusCode(expectedExitCode, r.statusCode.intValue)\n    r\n  }\n\n  /**\n   * Binds package. Parameters mirror those available in the REST.\n   *\n   * @param name either a fully qualified name or a simple entity name\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def bind(provider: String,\n                    name: String,\n                    parameters: Map[String, JsValue] = Map.empty,\n                    annotations: Map[String, JsValue] = Map.empty,\n                    expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {\n    val params = convertMapIntoKeyValue(parameters)\n    val annos = convertMapIntoKeyValue(annotations)\n\n    val (ns, packageName) = getNamespaceEntityName(provider)\n    val path = getNamePath(wp.namespace, noun, name)\n    val binding = JsObject(\"namespace\" -> ns.toJson, \"name\" -> packageName.toJson)\n    val bodyContent =\n      JsObject(\"binding\" -> binding.toJson, \"parameters\" -> params.toJson, \"annotations\" -> annos.toJson)\n    val resp = requestEntity(PUT, path, Map(\"overwrite\" -> \"false\"), Some(bodyContent.toString))\n    val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))\n    validateStatusCode(expectedExitCode, rr.statusCode.intValue)\n    rr\n  }\n\n}\n\nclass RestGatewayOperations(implicit val actorSystem: ActorSystem) extends GatewayOperations with RunRestCmd {\n  protected val noun = \"apis\"\n\n  /**\n   * Creates and API endpoint. Parameters mirror those available in the REST.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def create(basepath: Option[String] = None,\n                      relpath: Option[String] = None,\n                      operation: Option[String] = None,\n                      action: Option[String] = None,\n                      apiname: Option[String] = None,\n                      swagger: Option[String] = None,\n                      responsetype: Option[String] = None,\n                      expectedExitCode: Int = SUCCESS_EXIT,\n                      cliCfgFile: Option[String] = None)(implicit wp: WskProps): RestResult = {\n    val r = action match {\n      case Some(action) => {\n        val (ns, actionName) = getNamespaceEntityName(action)\n        val actionUrl = s\"${WhiskProperties.getApiHostForAction}$basePath/web/$ns/default/$actionName.http\"\n        val actionAuthKey = wp.authKey\n        val testaction = Some(\n          new ApiAction(name = actionName, namespace = ns, backendUrl = actionUrl, authkey = actionAuthKey))\n        val parms = Map(\"namespace\" -> ns.toJson) ++ {\n          basepath map { b =>\n            Map(\"gatewayBasePath\" -> b.toJson)\n          } getOrElse Map.empty\n        } ++ {\n          relpath map { r =>\n            Map(\"gatewayPath\" -> r.toJson)\n          } getOrElse Map.empty\n        } ++ {\n          operation map { o =>\n            Map(\"gatewayMethod\" -> o.toJson)\n          } getOrElse Map.empty\n        } ++ {\n          apiname map { an =>\n            Map(\"apiName\" -> an.toJson)\n          } getOrElse Map.empty\n        } ++ {\n          testaction map { a =>\n            Map(\"action\" -> a.toJson)\n          } getOrElse Map.empty\n        } ++ {\n          swagger map { s =>\n            val swaggerFile = FileUtils.readFileToString(new File(s), StandardCharsets.UTF_8)\n            Map(\"swagger\" -> swaggerFile.toJson)\n          } getOrElse Map.empty\n        }\n\n        val spaceguid = if (wp.authKey.contains(\":\")) wp.authKey.split(\":\")(0) else wp.authKey\n\n        val parm = Map[String, JsValue](\"apidoc\" -> JsObject(parms)) ++ {\n          responsetype.map(r => Map(\"responsetype\" -> r.toJson)).getOrElse(Map.empty)\n        } ++ {\n          Map(\"accesstoken\" -> wp.authKey.toJson)\n        } ++ {\n          Map(\"spaceguid\" -> spaceguid.toJson)\n        }\n\n        invokeAction(\n          name = \"apimgmt/createApi\",\n          parameters = parm,\n          blocking = true,\n          result = true,\n          web = true,\n          expectedExitCode = expectedExitCode)(wp)\n      }\n      case None =>\n        swagger match {\n          case Some(swaggerFile) =>\n            var file = \"\"\n            val fileName = swaggerFile.toString\n            try {\n              file = FileUtils.readFileToString(new File(fileName), StandardCharsets.UTF_8)\n            } catch {\n              case _: Throwable =>\n                return new RestResult(\n                  NotFound,\n                  \"\",\n                  JsObject(\"error\" -> s\"Error reading swagger file '$fileName'\".toJson).toString)\n            }\n            val parms = Map(\"namespace\" -> s\"${wp.namespace}\".toJson, \"swagger\" -> file.toJson)\n            val parm = Map[String, JsValue](\"apidoc\" -> JsObject(parms)) ++ {\n              responsetype.map(r => Map(\"responsetype\" -> r.toJson)).getOrElse(Map.empty)\n            } ++ {\n              Map(\"accesstoken\" -> wp.authKey.toJson)\n            } ++ {\n              Map(\"spaceguid\" -> wp.authKey.split(\":\")(0).toJson)\n            }\n            invokeAction(\n              name = \"apimgmt/createApi\",\n              parameters = parm,\n              blocking = true,\n              result = true,\n              web = true,\n              expectedExitCode = expectedExitCode)(wp)\n          case None => new RestResult(NotFound, \"\")\n        }\n    }\n    r\n  }\n\n  /**\n   * Retrieve a list of API endpoints. Parameters mirror those available in the REST.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def list(basepathOrApiName: Option[String] = None,\n                    relpath: Option[String] = None,\n                    operation: Option[String] = None,\n                    limit: Option[Int] = None,\n                    since: Option[Instant] = None,\n                    full: Option[Boolean] = None,\n                    nameSort: Option[Boolean] = None,\n                    expectedExitCode: Int = SUCCESS_EXIT,\n                    cliCfgFile: Option[String] = None)(implicit wp: WskProps): RestResult = {\n\n    val parms = {\n      basepathOrApiName map { b =>\n        Map(\"basepath\" -> b.toJson)\n      } getOrElse Map.empty\n    } ++ {\n      relpath map { r =>\n        Map(\"relpath\" -> r.toJson)\n      } getOrElse Map.empty\n    } ++ {\n      operation map { o =>\n        Map(\"operation\" -> o.toJson)\n      } getOrElse Map.empty\n    } ++ {\n      Map(\"accesstoken\" -> wp.authKey.toJson)\n    } ++ {\n      Map(\"spaceguid\" -> wp.authKey.split(\":\")(0).toJson)\n    }\n\n    invokeAction(\n      name = \"apimgmt/getApi\",\n      parameters = parms,\n      blocking = true,\n      result = true,\n      web = true,\n      expectedExitCode = OK.intValue)(wp)\n  }\n\n  /**\n   * Retieves an API's configuration. Parameters mirror those available in the REST.\n   * Runs a command wsk [params] where the arguments come in as a sequence.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def get(basepathOrApiName: Option[String] = None,\n                   full: Option[Boolean] = None,\n                   expectedExitCode: Int = SUCCESS_EXIT,\n                   cliCfgFile: Option[String] = None,\n                   format: Option[String] = None)(implicit wp: WskProps): RestResult = {\n    val parms = {\n      basepathOrApiName map { b =>\n        Map(\"basepath\" -> b.toJson)\n      } getOrElse Map.empty\n    } ++ {\n      Map(\"accesstoken\" -> wp.authKey.toJson)\n    } ++ {\n      Map(\"spaceguid\" -> wp.authKey.split(\":\")(0).toJson)\n    }\n\n    invokeAction(\n      name = \"apimgmt/getApi\",\n      parameters = parms,\n      blocking = true,\n      result = true,\n      web = true,\n      expectedExitCode = OK.intValue)(wp)\n  }\n\n  /**\n   * Delete an entire API or a subset of API endpoints. Parameters mirror those available in the REST.\n   *\n   * @param expectedExitCode (optional) the expected exit code for the command\n   * if the code is anything but DONTCARE_EXIT, assert the code is as expected\n   */\n  override def delete(basepathOrApiName: String,\n                      relpath: Option[String] = None,\n                      operation: Option[String] = None,\n                      expectedExitCode: Int = SUCCESS_EXIT,\n                      cliCfgFile: Option[String] = None)(implicit wp: WskProps): RestResult = {\n    val parms = Map(\"basepath\" -> basepathOrApiName.toJson) ++ {\n      relpath map { r =>\n        Map(\"relpath\" -> r.toJson)\n      } getOrElse Map.empty\n    } ++ {\n      operation map { o =>\n        Map(\"operation\" -> o.toJson)\n      } getOrElse Map.empty\n    } ++ {\n      Map(\"accesstoken\" -> wp.authKey.toJson)\n    } ++ {\n      Map(\"spaceguid\" -> wp.authKey.split(\":\")(0).toJson)\n    }\n\n    invokeAction(\n      name = \"apimgmt/deleteApi\",\n      parameters = parms,\n      blocking = true,\n      result = true,\n      web = true,\n      expectedExitCode = expectedExitCode)(wp)\n  }\n}\n\ntrait RunRestCmd extends Matchers with ScalaFutures with SwaggerValidator {\n\n  val protocol: String = loadConfigOrThrow[String](\"whisk.controller.protocol\")\n  val idleTimeout: FiniteDuration = 90 seconds\n  val toStrictTimeout: FiniteDuration = 30 seconds\n  val queueSize = 10\n  val maxOpenRequest = 1024\n  val basePath = Path(\"/api/v1\")\n  val systemNamespace = \"whisk.system\"\n  val logger = new PekkoLogging(actorSystem.log)\n\n  implicit val config: PatienceConfig = PatienceConfig(100 seconds, 15 milliseconds)\n  implicit val actorSystem: ActorSystem\n  lazy implicit val executionContext: ExecutionContext = actorSystem.dispatcher\n\n  lazy val connectionContext =\n    Https.connectionContextClient(SSL.nonValidatingContext(httpsConfig.clientAuth.toBoolean), true)\n\n  def isStatusCodeExpected(expectedExitCode: Int, statusCode: Int): Boolean = {\n    if ((expectedExitCode != DONTCARE_EXIT) && (expectedExitCode != ANY_ERROR_EXIT))\n      statusCode == expectedExitCode\n    else true\n  }\n\n  def validateStatusCode(expectedExitCode: Int, statusCode: Int): Unit = {\n    if ((expectedExitCode != DONTCARE_EXIT) && (expectedExitCode != ANY_ERROR_EXIT))\n      if (!isStatusCodeExpected(expectedExitCode, statusCode)) {\n        statusCode shouldBe expectedExitCode\n      }\n  }\n\n  def getNamePath(ns: String, noun: String, name: String) = Path(s\"$basePath/namespaces/$ns/$noun/$name\")\n\n  def getExt(filePath: String): String = Option(FilenameUtils.getExtension(filePath)).getOrElse(\"\")\n\n  def requestEntity(method: HttpMethod,\n                    path: Path,\n                    params: Map[String, String] = Map.empty,\n                    body: Option[String] = None)(implicit wp: WskProps): HttpResponse = {\n\n    val creds = getHttpCredentials(wp)\n\n    // startsWith(http) includes https\n    val hostWithScheme = if (wp.apihost.startsWith(\"http\")) {\n      Uri(wp.apihost)\n    } else {\n      Uri().withScheme(\"https\").withHost(wp.apihost)\n    }\n\n    val request = HttpRequest(\n      method,\n      hostWithScheme.withPath(path).withQuery(Query(params)),\n      List(Authorization(creds)),\n      entity =\n        body.map(b => HttpEntity.Strict(ContentTypes.`application/json`, ByteString(b))).getOrElse(HttpEntity.Empty))\n    val response = Http().singleRequest(request, connectionContext).flatMap { _.toStrict(toStrictTimeout) }.futureValue\n\n    logger.debug(this, s\"Request: $request\")\n    logger.debug(this, s\"Response: $response\")\n\n    val validationErrors = validateRequestAndResponse(request, response)\n    if (validationErrors.nonEmpty) {\n      fail(\n        s\"HTTP request or response did not match the Swagger spec.\\nRequest: $request\\n\" +\n          s\"Response: $response\\nValidation Error: $validationErrors\")\n    }\n    response\n  }\n\n  private def getHttpCredentials(wp: WskProps) = {\n    if (wp.authKey.contains(\":\")) {\n      val authKey = wp.authKey.split(\":\")\n      new BasicHttpCredentials(authKey(0), authKey(1))\n    } else {\n      if (wp.basicAuth) {\n        new BasicHttpCredentials(wp.authKey, wp.authKey)\n      } else {\n        OAuth2BearerToken(wp.authKey)\n      }\n    }\n  }\n\n  def getParamsAnnos(parameters: Map[String, JsValue] = Map.empty,\n                     annotations: Map[String, JsValue] = Map.empty,\n                     parameterFile: Option[String] = None,\n                     annotationFile: Option[String] = None,\n                     feed: Option[String] = None,\n                     web: Option[String] = None): (Array[JsValue], Array[JsValue]) = {\n    val params = parameterFile.map(convertStringIntoKeyValue(_)).getOrElse(convertMapIntoKeyValue(parameters))\n\n    val annos = annotationFile\n      .map(convertStringIntoKeyValue(_, feed, web))\n      .getOrElse(convertMapIntoKeyValue(annotations, feed, web))\n\n    (params, annos)\n  }\n\n  def convertStringIntoKeyValue(file: String,\n                                feed: Option[String] = None,\n                                web: Option[String] = None): Array[JsValue] = {\n    val input = FileUtils.readFileToString(new File(file), StandardCharsets.UTF_8)\n    val in = input.parseJson.convertTo[Map[String, JsValue]]\n    convertMapIntoKeyValue(in, feed, web)\n  }\n\n  def convertMapIntoKeyValue(params: Map[String, JsValue],\n                             feed: Option[String] = None,\n                             web: Option[String] = None,\n                             oldParams: List[JsObject] = List.empty): Array[JsValue] = {\n    val newParams =\n      params\n        .map { case (key, value) => JsObject(\"key\" -> key.toJson, \"value\" -> value) } ++ feed.map(f =>\n        JsObject(\"key\" -> \"feed\".toJson, \"value\" -> f.toJson))\n\n    val paramsList = {\n      if (newParams.nonEmpty) newParams\n      else oldParams\n    }\n\n    val webOpt = web.map {\n      case \"true\" | \"yes\" =>\n        Seq(\n          JsObject(\"key\" -> \"web-export\".toJson, \"value\" -> true.toJson),\n          JsObject(\"key\" -> \"raw-http\".toJson, \"value\" -> false.toJson),\n          JsObject(\"key\" -> \"final\".toJson, \"value\" -> true.toJson))\n      case \"false\" | \"no\" =>\n        Seq(\n          JsObject(\"key\" -> \"web-export\".toJson, \"value\" -> false.toJson),\n          JsObject(\"key\" -> \"raw-http\".toJson, \"value\" -> false.toJson),\n          JsObject(\"key\" -> \"final\".toJson, \"value\" -> false.toJson))\n      case \"raw\" =>\n        Seq(\n          JsObject(\"key\" -> \"web-export\".toJson, \"value\" -> true.toJson),\n          JsObject(\"key\" -> \"raw-http\".toJson, \"value\" -> true.toJson),\n          JsObject(\"key\" -> \"final\".toJson, \"value\" -> true.toJson))\n      case _ =>\n        Seq.empty\n    }\n\n    webOpt\n      .map(paramsList ++ _)\n      .getOrElse(paramsList)\n      .toArray\n  }\n\n  def entityName(name: String)(implicit wp: WskProps): String = {\n    val sep = \"/\"\n    if (name.startsWith(sep)) name.substring(name.indexOf(sep, name.indexOf(sep) + 1) + 1, name.length)\n    else name\n  }\n\n  def fullEntityName(name: String)(implicit wp: WskProps): String = {\n    val (ns, rest) = getNamespaceEntityName(name)\n    if (rest.nonEmpty) s\"/$ns/$rest\"\n    else s\"/$ns\"\n  }\n\n  def convertIntoComponents(comps: String)(implicit wp: WskProps): Array[JsValue] = {\n    comps.split(\",\").filter(_.nonEmpty).map(comp => fullEntityName(comp).toJson)\n  }\n\n  def getRespData(resp: HttpResponse): String = {\n    val timeout = toStrictTimeout\n    Try(resp.entity.toStrict(timeout).map { _.data }.map(_.utf8String).futureValue).getOrElse(\"\")\n  }\n\n  def getTransactionId(resp: HttpResponse): String = {\n    val tidHeader = resp.headers.find(_.is(TransactionId.generatorConfig.lowerCaseHeader))\n    withClue(\n      s\"The header ${TransactionId.generatorConfig} is not set. This means that the request did not reach nginx (or the controller if nginx is skipped in that test).\") {\n      tidHeader shouldBe defined\n    }\n    tidHeader.get.value\n  }\n\n  def getNamespaceEntityName(name: String)(implicit wp: WskProps): (String, String) = {\n    name.split(\"/\") match {\n      // Example: /namespace/package_name/entity_name\n      case Array(empty, namespace, packageName, entityName) if empty.isEmpty => (namespace, s\"$packageName/$entityName\")\n      // Example: /namespace/entity_name\n      case Array(empty, namespace, entityName) if empty.isEmpty => (namespace, entityName)\n      // Example: namespace/package_name/entity_name\n      case Array(namespace, packageName, entityName) => (namespace, s\"$packageName/$entityName\")\n      // Example: /namespace\n      case Array(empty, namespace) if empty.isEmpty => (namespace, \"\")\n      // Example: package_name/entity_name\n      case Array(packageName, entityName) if !packageName.isEmpty => (wp.namespace, s\"$packageName/$entityName\")\n      // Example: entity_name\n      case Array(entityName) => (wp.namespace, entityName)\n      case _                 => (wp.namespace, name)\n    }\n  }\n\n  def invokeAction(name: String,\n                   parameters: Map[String, JsValue] = Map.empty,\n                   parameterFile: Option[String] = None,\n                   blocking: Boolean = false,\n                   result: Boolean = false,\n                   web: Boolean = false,\n                   expectedExitCode: Int = Accepted.intValue)(implicit wp: WskProps): RestResult = {\n    val (ns, actName) = getNamespaceEntityName(name)\n\n    val path =\n      if (web) Path(s\"$basePath/web/$systemNamespace/$actName.http\")\n      else Path(s\"$basePath/namespaces/$ns/actions/$actName\")\n\n    val paramMap = Map(\"blocking\" -> blocking.toString, \"result\" -> result.toString)\n\n    val input = parameterFile map { pf =>\n      Some(FileUtils.readFileToString(new File(pf), StandardCharsets.UTF_8))\n    } getOrElse Some(parameters.toJson.toString)\n\n    val resp = requestEntity(POST, path, paramMap, input)\n\n    val rr = new RestResult(resp.status.intValue, getTransactionId(resp), getRespData(resp), blocking)\n\n    // If the statusCode does not not equal to expectedExitCode, it is acceptable that the statusCode\n    // equals to 200 for the case that either blocking or result is set to true.\n    if (!isStatusCodeExpected(expectedExitCode, rr.statusCode.intValue)) {\n      if (blocking || result) {\n        validateStatusCode(OK.intValue, rr.statusCode.intValue)\n      } else {\n        rr.statusCode.intValue shouldBe expectedExitCode\n      }\n    }\n\n    rr\n  }\n}\n\nobject RestResult {\n  def getField(obj: JsObject, key: String): String = {\n    obj.fields.get(key).map(_.convertTo[String]).getOrElse(\"\")\n  }\n\n  def getFieldJsObject(obj: JsObject, key: String): JsObject = {\n    obj.fields.get(key).map(_.asJsObject).getOrElse(JsObject.empty)\n  }\n\n  def getFieldJsValue(obj: JsObject, key: String): JsValue = {\n    obj.fields.getOrElse(key, JsObject.empty)\n  }\n\n  def getFieldListJsObject(obj: JsObject, key: String): Vector[JsObject] = {\n    obj.fields.get(key).map(_.convertTo[Vector[JsObject]]).getOrElse(Vector(JsObject.empty))\n  }\n\n  def convertStausCodeToExitCode(statusCode: StatusCode, blocking: Boolean = false): Int = {\n    if ((statusCode == OK) || (!blocking && (statusCode == Accepted))) 0\n    else statusCode.intValue % 256\n  }\n\n  def convertHttpResponseToStderr(respData: String): String = {\n    Try(getField(respData.parseJson.asJsObject, \"error\")).getOrElse(\"\")\n  }\n}\n\nclass RestResult(val statusCode: StatusCode, val tid: String, val respData: String = \"\", blocking: Boolean = false)\n    extends RunResult(\n      RestResult.convertStausCodeToExitCode(statusCode, blocking),\n      respData,\n      RestResult.convertHttpResponseToStderr(respData)) {\n\n  override def toString: String = {\n    super.toString + s\"\"\"statusCode: $statusCode\n       |tid: $tid\n       |respData: $respData\n       |blocking: $blocking\"\"\".stripMargin\n  }\n\n  def respBody: JsObject = respData.parseJson.asJsObject\n\n  def getField(key: String): String = {\n    RestResult.getField(respBody, key)\n  }\n\n  def getFieldJsObject(key: String): JsObject = {\n    RestResult.getFieldJsObject(respBody, key)\n  }\n\n  def getFieldJsValue(key: String): JsValue = {\n    RestResult.getFieldJsValue(respBody, key)\n  }\n\n  def getFieldListJsObject(key: String): Vector[JsObject] = {\n    RestResult.getFieldListJsObject(respBody, key)\n  }\n\n  def getBodyListJsObject: Vector[JsObject] = {\n    respData.parseJson.convertTo[Vector[JsObject]]\n  }\n\n  def getBodyListString: Vector[String] = {\n    respData.parseJson.convertTo[Vector[String]]\n  }\n}\n\nclass ApiAction(val name: String,\n                val namespace: String,\n                val backendMethod: String = \"POST\",\n                val backendUrl: String,\n                val authkey: String) {\n  def toJson: JsObject = {\n    JsObject(\n      \"name\" -> name.toJson,\n      \"namespace\" -> namespace.toJson,\n      \"backendMethod\" -> backendMethod.toJson,\n      \"backendUrl\" -> backendUrl.toJson,\n      \"authkey\" -> authkey.toJson)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/ha/CacheInvalidationTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage ha\n\nimport scala.concurrent.Await\nimport scala.concurrent.duration.DurationInt\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.marshalling.Marshal\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.headers.Authorization\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport common.WhiskProperties\nimport common.WskActorSystem\nimport common.WskTestHelpers\nimport common.rest.HttpConnection\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport pureconfig._\n\n@RunWith(classOf[JUnitRunner])\nclass CacheInvalidationTests extends AnyFlatSpec with Matchers with WskTestHelpers with WskActorSystem {\n\n  val hosts = WhiskProperties.getProperty(\"controller.hosts\").split(\",\")\n\n  val controllerProtocol = loadConfigOrThrow[String](\"whisk.controller.protocol\")\n  val connectionContext = HttpConnection.getContext(controllerProtocol)\n\n  def ports(instance: Int) = WhiskProperties.getControllerBasePort + instance\n\n  def controllerUri(instance: Int) = {\n    require(instance >= 0 && instance < hosts.length, \"Controller instance not known.\")\n    Uri()\n      .withScheme(controllerProtocol)\n      .withHost(hosts(instance))\n      .withPort(ports(instance))\n  }\n\n  def actionPath(name: String) = Uri.Path(s\"/api/v1/namespaces/_/actions/$name\")\n\n  val Array(username, password) = WhiskProperties.readAuthKey(WhiskProperties.getAuthFileForTesting).split(\":\")\n  val authHeader = Authorization(BasicHttpCredentials(username, password))\n\n  val timeout = 15.seconds\n\n  def retry[T](fn: => T) = org.apache.openwhisk.utils.retry(fn, 15, Some(1.second))\n\n  def updateAction(name: String, code: String, controllerInstance: Int = 0) = {\n    val body = JsObject(\n      \"namespace\" -> JsString(\"_\"),\n      \"name\" -> JsString(name),\n      \"exec\" -> JsObject(\"kind\" -> JsString(\"nodejs:default\"), \"code\" -> JsString(code)))\n\n    val request = Marshal(body).to[RequestEntity].flatMap { entity =>\n      Http()\n        .singleRequest(\n          HttpRequest(\n            method = HttpMethods.PUT,\n            uri = controllerUri(controllerInstance)\n              .withPath(actionPath(name))\n              .withQuery(Uri.Query(\"overwrite\" -> true.toString)),\n            headers = List(authHeader),\n            entity = entity),\n          connectionContext = connectionContext)\n        .flatMap { response =>\n          Unmarshal(response).to[JsObject].map { resBody =>\n            withClue(s\"Error in Body: $resBody\")(response.status shouldBe StatusCodes.OK)\n            resBody\n          }\n        }\n    }\n\n    Await.result(request, timeout)\n  }\n\n  def getAction(name: String, controllerInstance: Int = 0, expectedCode: StatusCode = StatusCodes.OK) = {\n    val request = Http()\n      .singleRequest(\n        HttpRequest(\n          method = HttpMethods.GET,\n          uri = controllerUri(controllerInstance).withPath(actionPath(name)),\n          headers = List(authHeader)),\n        connectionContext = connectionContext)\n      .flatMap { response =>\n        Unmarshal(response).to[JsObject].map { resBody =>\n          withClue(s\"Wrong statuscode from controller. Body is: $resBody\")(response.status shouldBe expectedCode)\n          resBody\n        }\n      }\n\n    Await.result(request, timeout)\n  }\n\n  def deleteAction(name: String,\n                   controllerInstance: Int = 0,\n                   expectedCode: Option[StatusCode] = Some(StatusCodes.OK)) = {\n    val request = Http()\n      .singleRequest(\n        HttpRequest(\n          method = HttpMethods.DELETE,\n          uri = controllerUri(controllerInstance).withPath(actionPath(name)),\n          headers = List(authHeader)),\n        connectionContext = connectionContext)\n      .flatMap { response =>\n        Unmarshal(response).to[JsObject].map { resBody =>\n          expectedCode.map { code =>\n            withClue(s\"Wrong statuscode from controller. Body is: $resBody\")(response.status shouldBe code)\n          }\n          resBody\n        }\n      }\n\n    Await.result(request, timeout)\n  }\n\n  behavior of \"The cache\"\n\n  if (WhiskProperties.getControllerInstances >= 2) {\n\n    it should \"be invalidated on updating an entity\" in {\n      val actionName = \"invalidateRemoteCacheOnUpdate\"\n\n      deleteAction(actionName, 0, None)\n      deleteAction(actionName, 1, None)\n\n      try {\n        // Create an action on controller0\n        val createdAction = updateAction(actionName, \"CODE_CODE_CODE\", 0)\n\n        // Get action from controller1\n        val actionFromController1 = getAction(actionName, 1)\n        createdAction shouldBe actionFromController1\n\n        // Update the action on controller0\n        val updatedAction = updateAction(actionName, \"CODE_CODE\", 0)\n\n        retry({\n          // Get action from controller1\n          val updatedActionFromController1 = getAction(actionName, 1)\n          updatedAction shouldBe updatedActionFromController1\n        })\n      } finally {\n        deleteAction(actionName, 0, None)\n      }\n    }\n\n    it should \"be invalidated on deleting an entity\" in {\n      val actionName = \"invalidateRemoteCacheOnDelete\"\n\n      deleteAction(actionName, 0, None)\n      deleteAction(actionName, 1, None)\n\n      // Create an action on controller0\n      val createdAction = updateAction(actionName, \"CODE_CODE_CODE\", 0)\n      // Get action from controller1 (Now its in the cache of controller 0 and 1)\n      val actionFromController1 = getAction(actionName, 1)\n      createdAction shouldBe actionFromController1\n\n      // Delete the action on controller0 (It should be deleted automatically from the cache of controller1)\n      val updatedAction = deleteAction(actionName, 0)\n\n      retry({\n        // Get action from controller1 should fail with 404\n        getAction(actionName, 1, StatusCodes.NotFound)\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/ha/ShootComponentsTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage ha\n\nimport java.io.File\nimport java.time.Instant\n\nimport scala.concurrent.{Await, Future}\nimport scala.concurrent.duration.DurationInt\nimport scala.util.Try\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.model.HttpRequest\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport common._\nimport common.rest.{HttpConnection, WskRestOperations}\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.CouchDbConfig\nimport org.apache.openwhisk.core.database.test.ExtendedCouchDbRestClient\nimport org.apache.openwhisk.utils.retry\n\n@RunWith(classOf[JUnitRunner])\nclass ShootComponentsTests\n    extends AnyFlatSpec\n    with Matchers\n    with WskTestHelpers\n    with ScalaFutures\n    with WskActorSystem\n    with StreamLogging\n    with ShootComponentUtils {\n\n  implicit val wskprops = WskProps()\n  val wsk = new WskRestOperations\n  val defaultAction = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n\n  implicit val testConfig = PatienceConfig(1.minute)\n\n  val controllerProtocol = loadConfigOrThrow[String](\"whisk.controller.protocol\")\n\n  // Throttle requests to the remaining controllers to avoid getting 429s. (60 req/min)\n  val amountOfControllers = WhiskProperties.getControllerInstances\n  val limit = WhiskProperties.getProperty(WhiskConfig.actionInvokePerMinuteLimit).toDouble\n  val limitPerController = limit / amountOfControllers\n  val allowedRequestsPerMinute = (amountOfControllers - 1.0) * limitPerController\n  val timeBeweenRequests = 60.seconds / allowedRequestsPerMinute\n\n  val controller0DockerHost = WhiskProperties.getBaseControllerHost()\n  val couchDB0DockerHost = WhiskProperties.getBaseDBHost()\n\n  val dbConfig = loadConfigOrThrow[CouchDbConfig](ConfigKeys.couchdb)\n  val dbProtocol = dbConfig.protocol\n  val dbHostsList = WhiskProperties.getDBHosts\n  val dbPort = dbConfig.port\n  val dbUsername = dbConfig.username\n  val dbPassword = dbConfig.password\n  val dbWhiskAuth = dbConfig.databases.get(\"WhiskAuth\").get\n\n  def ping(host: String, port: Int, path: String = \"/\") = {\n\n    val connectionContext = HttpConnection.getContext(controllerProtocol)\n\n    val response = Try {\n      Http()\n        .singleRequest(\n          HttpRequest(uri = s\"$controllerProtocol://$host:$port$path\"),\n          connectionContext = connectionContext)\n        .futureValue\n    }.toOption\n\n    response.map { res =>\n      (res.status, Unmarshal(res).to[String].futureValue)\n    }\n  }\n\n  def isControllerAlive(instance: Int): Boolean = {\n    require(instance >= 0 && instance < 2, \"Controller instance not known.\")\n\n    val host = WhiskProperties.getProperty(\"controller.hosts\").split(\",\")(instance)\n    val port = WhiskProperties.getControllerBasePort + instance\n\n    val res = ping(host, port, \"/ping\")\n    res == Some((StatusCodes.OK, \"pong\"))\n  }\n\n  def isDBAlive(instance: Int): Boolean = {\n    require(instance >= 0 && instance < 2, \"DB instance not known.\")\n\n    val host = WhiskProperties.getProperty(\"db.hosts\").split(\",\")(instance)\n    val port = dbPort + instance\n\n    val res = ping(host, port)\n    res == Some(\n      (\n        StatusCodes.OK,\n        \"{\\\"couchdb\\\":\\\"Welcome\\\",\\\"version\\\":\\\"2.1.1\\\",\\\"features\\\":[\\\"scheduler\\\"],\\\"vendor\\\":{\\\"name\\\":\\\"The Apache Software Foundation\\\"}}\\n\"))\n  }\n\n  def doRequests(amount: Int, actionName: String): Seq[(Int, Int)] = {\n    (0 until amount).map { i =>\n      val start = Instant.now\n\n      // Do POSTs and GETs\n      val invokeExit = Future {\n        wsk.action.invoke(actionName, expectedExitCode = TestUtils.DONTCARE_EXIT).exitCode\n      }\n      val getExit = Future {\n        wsk.action.get(actionName, expectedExitCode = TestUtils.DONTCARE_EXIT).exitCode\n      }\n\n      println(s\"Done rerquests with responses: invoke: ${invokeExit.futureValue} and get: ${getExit.futureValue}\")\n\n      val remainingWait = timeBeweenRequests.toMillis - (Instant.now.toEpochMilli - start.toEpochMilli)\n      Thread.sleep(if (remainingWait < 0) 0L else remainingWait)\n      (invokeExit.futureValue, getExit.futureValue)\n    }\n  }\n\n  behavior of \"Controllers hot standby\"\n\n  it should \"use controller1 if controller0 goes down\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    if (amountOfControllers >= 2) {\n      val actionName = \"shootcontroller\"\n\n      assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n        action.create(actionName, defaultAction)\n      }\n\n      // Produce some load on the system for 100 seconds. Kill the controller after 4 requests\n      val totalRequests = (100.seconds / timeBeweenRequests).toInt\n\n      val requestsBeforeRestart = doRequests(4, actionName)\n\n      // Kill the controller\n      restartComponent(controller0DockerHost, \"controller0\")\n      // Wait until down\n      retry({\n        isControllerAlive(0) shouldBe false\n      }, 100, Some(100.milliseconds))\n      // Check that second controller is still up\n      isControllerAlive(1) shouldBe true\n\n      val requestsAfterRestart = doRequests(totalRequests - 4, actionName)\n\n      val requests = requestsBeforeRestart ++ requestsAfterRestart\n\n      val unsuccessfulInvokes = requests.map(_._1).count(_ != TestUtils.SUCCESS_EXIT)\n      // Allow 5 failures for the 100 seconds\n      unsuccessfulInvokes should be <= 5\n\n      val unsuccessfulGets = requests.map(_._2).count(_ != TestUtils.SUCCESS_EXIT)\n      // Allow no failures in GET requests, because they are idempotent and they should be passed to the next controller if one crashes\n      unsuccessfulGets shouldBe 0\n\n      // Check that both controllers are up\n      // controller0\n      isControllerAlive(0) shouldBe true\n      //controller1\n      isControllerAlive(1) shouldBe true\n    }\n  }\n\n  behavior of \"CouchDB HA\"\n\n  it should \"be able to retrieve documents from couchdb1 if couchdb0 goes down\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      if (WhiskProperties.getProperty(WhiskConfig.dbInstances).toInt >= 2) {\n\n        val dbName: String = dbWhiskAuth\n        val db1 = new ExtendedCouchDbRestClient(\n          dbProtocol,\n          dbHostsList.split(\",\")(0),\n          dbPort.toInt,\n          dbUsername,\n          dbPassword,\n          dbName)\n        val db2 = new ExtendedCouchDbRestClient(\n          dbProtocol,\n          dbHostsList.split(\",\")(1),\n          dbPort.toInt,\n          dbUsername,\n          dbPassword,\n          dbName)\n\n        println(\"Creating test document\")\n        val docId = \"couchdb-ha-test\"\n        val testDocument = JsObject(\n          \"_id\" -> docId.toJson,\n          \"namespaces\" -> JsArray(\n            JsObject(\n              \"name\" -> docId.toJson,\n              \"uuid\" -> \"789c46b1-71f6-4ed5-8c54-816aa4f8c502\".toJson,\n              \"key\" -> \"abczO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\".toJson)),\n          \"subject\" -> docId.toJson)\n\n        val docId2 = \"couchdb-ha-test2\"\n        val testDocument2 = JsObject(\n          \"_id\" -> docId2.toJson,\n          \"namespaces\" -> JsArray(\n            JsObject(\n              \"name\" -> docId2.toJson,\n              \"uuid\" -> \"789c46b1-71f6-4ed5-8c54-816aa4f8c502\".toJson,\n              \"key\" -> \"abczO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP\".toJson)),\n          \"subject\" -> docId2.toJson)\n\n        isDBAlive(0) shouldBe true\n\n        retry(db1.putDoc(docId, testDocument))\n\n        stopComponent(couchDB0DockerHost, \"couchdb\")\n\n        retry({\n          isDBAlive(0) shouldBe false\n        }, 100, Some(100.milliseconds))\n\n        retry({\n          val result = Await.result(db2.getDoc(docId), 15.seconds)\n          result should be('right)\n          result.right.get.getFields(\"_id\") shouldBe testDocument.getFields(\"_id\")\n        })\n\n        retry(\n          {\n            val result = Await.result(\n              db2.executeView(\"subjects\", \"identities\")(startKey = List(docId), endKey = List(docId)),\n              15.seconds)\n            result should be('right)\n            result.right.get.getFields(\"_id\") shouldBe testDocument.getFields(\"namespace\")\n          },\n          100,\n          Some(100.milliseconds))\n\n        retry(db2.putDoc(docId2, testDocument2))\n\n        isDBAlive(0) shouldBe false\n\n        startComponent(couchDB0DockerHost, \"couchdb\")\n\n        retry({\n          isDBAlive(0) shouldBe true\n        }, 100, Some(100.milliseconds))\n\n        retry({\n          val result = Await.result(db1.getDoc(docId2), 15.seconds)\n          result should be('right)\n          result.right.get.getFields(\"_id\") shouldBe testDocument2.getFields(\"_id\")\n        })\n\n        retry(\n          {\n            val result = Await.result(\n              db1.executeView(\"subjects\", \"identities\")(startKey = List(docId2), endKey = List(docId2)),\n              15.seconds)\n            result should be('right)\n            result.right.get.getFields(\"_id\") shouldBe testDocument2.getFields(\"namespace\")\n          },\n          100,\n          Some(100.milliseconds))\n\n        val doc1Result = Await.result(db1.getDoc(docId), 15.seconds)\n        val doc2Result = Await.result(db1.getDoc(docId2), 15.seconds)\n        val rev1 = doc1Result.right.get.fields.get(\"_rev\").get.convertTo[String]\n        val rev2 = doc2Result.right.get.fields.get(\"_rev\").get.convertTo[String]\n        Await.result(db1.deleteDoc(docId, rev1), 15.seconds)\n        Await.result(db1.deleteDoc(docId2, rev2), 15.seconds)\n\n        retry({\n          val result = Await.result(db1.getDoc(docId), 15.seconds)\n          result should be('left)\n        })\n        retry({\n          val result = Await.result(db1.getDoc(docId2), 15.seconds)\n          result should be('left)\n        })\n      }\n  }\n}\n\ntrait ShootComponentUtils {\n  private def getDockerCommand(host: String, component: String, cmd: String) = {\n    def file(path: String) = Try(new File(path)).filter(_.exists).map(_.getAbsolutePath).toOption\n\n    val docker = (file(\"/usr/bin/docker\") orElse file(\"/usr/local/bin/docker\")).getOrElse(\"docker\")\n    val dockerPort = WhiskProperties.getProperty(WhiskConfig.dockerPort)\n\n    Seq(docker, \"--host\", host + \":\" + dockerPort, cmd, component)\n  }\n\n  def restartComponent(host: String, component: String) = {\n    val cmd: Seq[String] = getDockerCommand(host, component, \"restart\")\n    println(s\"Running command: ${cmd.mkString(\" \")}\")\n\n    TestUtils.runCmd(0, new File(\".\"), cmd: _*)\n  }\n\n  def stopComponent(host: String, component: String) = {\n    val cmd: Seq[String] = getDockerCommand(host, component, \"stop\")\n    println(s\"Running command: ${cmd.mkString(\" \")}\")\n\n    TestUtils.runCmd(0, new File(\".\"), cmd: _*)\n  }\n\n  def startComponent(host: String, component: String) = {\n    val cmd: Seq[String] = getDockerCommand(host, component, \"start\")\n    println(s\"Running command: ${cmd.mkString(\" \")}\")\n\n    TestUtils.runCmd(0, new File(\".\"), cmd: _*)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/invokerShoot/ShootInvokerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage invokerShoot\n\nimport java.io.File\nimport java.nio.charset.StandardCharsets\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common._\nimport common.rest.WskRestOperations\nimport org.apache.openwhisk.core.entity.{Annotations}\nimport org.apache.commons.io.FileUtils\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n@RunWith(classOf[JUnitRunner])\nclass ShootInvokerTests extends TestHelpers with WskTestHelpers with JsHelpers with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  // wsk must have type WskOperations so that tests using CLI (class Wsk)\n  // instead of REST (WskRestOperations) still work.\n  val wsk: WskOperations = new WskRestOperations\n\n  val testString = \"this is a test\"\n  val testResult = JsObject(\"count\" -> testString.split(\" \").length.toJson)\n  val guestNamespace = wskprops.namespace\n\n  behavior of \"Whisk actions\"\n\n  it should \"create an action with an empty file\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"empty\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"empty.js\")))\n    }\n  }\n\n  it should \"invoke an action returning a promise\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"hello promise\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"helloPromise.js\")))\n    }\n\n    val run = wsk.action.invoke(name)\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"done\" -> true.toJson))\n      activation.logs.get.mkString(\" \") shouldBe empty\n    }\n  }\n\n  it should \"invoke an action with a space in the name\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"hello Async\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"helloAsync.js\")))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(testResult)\n      activation.logs.get.mkString(\" \") should include(testString)\n    }\n  }\n\n  it should \"invoke an action that throws an uncaught exception and returns correct status code\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val name = \"throwExceptionAction\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"runexception.js\")))\n    }\n\n    withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>\n      val response = activation.response\n      activation.response.status shouldBe \"action developer error\"\n      activation.response.result shouldBe Some(\n        JsObject(\"error\" -> \"An error has occurred: Extraordinary exception\".toJson))\n    }\n  }\n\n  it should \"pass parameters bound on creation-time to the action\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"printParams\"\n    val params = Map(\"param1\" -> \"test1\", \"param2\" -> \"test2\")\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(\n        name,\n        Some(TestUtils.getTestActionFilename(\"printParams.js\")),\n        parameters = params.mapValues(_.toJson).toMap)\n    }\n\n    val invokeParams = Map(\"payload\" -> testString)\n    val run = wsk.action.invoke(name, invokeParams.mapValues(_.toJson).toMap)\n    withActivation(wsk.activation, run) { activation =>\n      val logs = activation.logs.get.mkString(\" \")\n\n      (params ++ invokeParams).foreach {\n        case (key, value) =>\n          logs should include(s\"params.$key: $value\")\n      }\n    }\n  }\n\n  it should \"copy an action and invoke it successfully\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"copied\"\n    val packageName = \"samples\"\n    val actionName = \"wordcount\"\n    val fullQualifiedName = s\"/$guestNamespace/$packageName/$actionName\"\n\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, shared = Some(true))\n    }\n\n    assetHelper.withCleaner(wsk.action, fullQualifiedName) {\n      val file = Some(TestUtils.getTestActionFilename(\"wc.js\"))\n      (action, _) =>\n        action.create(fullQualifiedName, file)\n    }\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(fullQualifiedName), Some(\"copy\"))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(testResult)\n      activation.logs.get.mkString(\" \") should include(testString)\n    }\n  }\n\n  it should \"copy an action and ensure exec, parameters, and annotations copied\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val origActionName = \"origAction\"\n      val copiedActionName = \"copiedAction\"\n      val params = Map(\"a\" -> \"A\".toJson)\n      val annots = Map(\"b\" -> \"B\".toJson)\n\n      assetHelper.withCleaner(wsk.action, origActionName) {\n        val file = Some(TestUtils.getTestActionFilename(\"wc.js\"))\n        (action, _) =>\n          action.create(origActionName, file, parameters = params, annotations = annots)\n      }\n\n      assetHelper.withCleaner(wsk.action, copiedActionName) { (action, _) =>\n        action.create(copiedActionName, Some(origActionName), Some(\"copy\"))\n      }\n\n      val copiedAction = wsk.parseJsonString(wsk.action.get(copiedActionName).stdout)\n      val origAction = wsk.parseJsonString(wsk.action.get(copiedActionName).stdout)\n\n      copiedAction.fields(\"annotations\") shouldBe origAction.fields(\"annotations\")\n      copiedAction.fields(\"parameters\") shouldBe origAction.fields(\"parameters\")\n      copiedAction.fields(\"exec\") shouldBe origAction.fields(\"exec\")\n      copiedAction.fields(\"version\") shouldBe JsString(\"0.0.1\")\n  }\n\n  it should \"add new parameters and annotations while copying an action\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val runtime = \"nodejs:default\"\n      val origName = \"origAction\"\n      val copiedName = \"copiedAction\"\n      val origParams = Map(\"origParam1\" -> \"origParamValue1\".toJson, \"origParam2\" -> 999.toJson)\n      val copiedParams = Map(\"copiedParam1\" -> \"copiedParamValue1\".toJson, \"copiedParam2\" -> 123.toJson)\n      val origAnnots = Map(\"origAnnot1\" -> \"origAnnotValue1\".toJson, \"origAnnot2\" -> true.toJson)\n      val copiedAnnots = Map(\"copiedAnnot1\" -> \"copiedAnnotValue1\".toJson, \"copiedAnnot2\" -> false.toJson)\n      val resParams = Seq(\n        JsObject(\"key\" -> JsString(\"copiedParam1\"), \"value\" -> JsString(\"copiedParamValue1\")),\n        JsObject(\"key\" -> JsString(\"copiedParam2\"), \"value\" -> JsNumber(123)),\n        JsObject(\"key\" -> JsString(\"origParam1\"), \"value\" -> JsString(\"origParamValue1\")),\n        JsObject(\"key\" -> JsString(\"origParam2\"), \"value\" -> JsNumber(999)))\n      val resAnnots = Seq(\n        JsObject(\"key\" -> JsString(\"origAnnot1\"), \"value\" -> JsString(\"origAnnotValue1\")),\n        JsObject(\"key\" -> JsString(\"copiedAnnot2\"), \"value\" -> JsFalse),\n        JsObject(\"key\" -> JsString(\"copiedAnnot1\"), \"value\" -> JsString(\"copiedAnnotValue1\")),\n        JsObject(\"key\" -> JsString(\"origAnnot2\"), \"value\" -> JsTrue),\n        JsObject(\"key\" -> Annotations.ProvideApiKeyAnnotationName.toJson, \"value\" -> JsFalse))\n\n      assetHelper.withCleaner(wsk.action, origName) {\n        val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n        (action, _) =>\n          action.create(origName, file, parameters = origParams, annotations = origAnnots, kind = Some(runtime))\n      }\n\n      assetHelper.withCleaner(wsk.action, copiedName) { (action, _) =>\n        println(\"created copied \")\n        action.create(copiedName, Some(origName), Some(\"copy\"), parameters = copiedParams, annotations = copiedAnnots)\n      }\n\n      val copiedAction = wsk.parseJsonString(wsk.action.get(copiedName).stdout)\n\n      // first we check the returned execution runtime for 'nodejs:*'\n      copiedAction\n        .fields(\"annotations\")\n        .convertTo[Seq[JsObject]]\n        .find(_.fields(\"key\").convertTo[String] == \"exec\")\n        .map(_.fields(\"value\"))\n        .map(exec => { exec.convertTo[String] should startWith(\"nodejs:\") })\n        .getOrElse(fail())\n\n      // CLI does not guarantee order of annotations and parameters so do a diff to compare the values\n      copiedAction.fields(\"parameters\").convertTo[Seq[JsObject]] diff resParams shouldBe List.empty\n\n      // for the anotations we ignore the exec field here, since we already compared it above\n      copiedAction\n        .fields(\"annotations\")\n        .convertTo[Seq[JsObject]]\n        .filter(annotation => annotation.fields(\"key\").convertTo[String] != \"exec\") diff resAnnots shouldBe List.empty\n  }\n\n  it should \"recreate and invoke a new action with different code\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"recreatedAction\"\n    assetHelper.withCleaner(wsk.action, name, false) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"wc.js\")))\n    }\n\n    val run1 = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson))\n    withActivation(wsk.activation, run1) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.logs.get.mkString(\" \") should include(s\"The message '$testString' has\")\n    }\n\n    wsk.action.delete(name)\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    val run2 = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson))\n    withActivation(wsk.activation, run2) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.logs.get.mkString(\" \") should include(s\"hello, $testString\")\n    }\n  }\n\n  it should \"fail to invoke an action with an empty file\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"empty\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"empty.js\")))\n    }\n    val run = wsk.action.invoke(name)\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"action developer error\"\n      activation.response.result shouldBe Some(JsObject(\"error\" -> \"Missing main/no code to execute.\".toJson))\n    }\n  }\n\n  it should \"blocking invoke of nested blocking actions\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"nestedBlockingAction\"\n    val child = \"wc\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(\n        name,\n        Some(TestUtils.getTestActionFilename(\"wcbin.js\")),\n        annotations = Map(Annotations.ProvideApiKeyAnnotationName -> JsTrue))\n    }\n    assetHelper.withCleaner(wsk.action, child) { (action, _) =>\n      action.create(child, Some(TestUtils.getTestActionFilename(\"wc.js\")))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson), blocking = true)\n    val activation = wsk.parseJsonString(run.stdout).convertTo[ActivationResult]\n\n    withClue(s\"check failed for activation: $activation\") {\n      val wordCount = testString.split(\" \").length\n      activation.response.result.get shouldBe JsObject(\"binaryCount\" -> s\"${wordCount.toBinaryString} (base 2)\".toJson)\n    }\n  }\n\n  it should \"blocking invoke an asynchronous action\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"helloAsync\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"helloAsync.js\")))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson), blocking = true)\n    val activation = wsk.parseJsonString(run.stdout).convertTo[ActivationResult]\n\n    withClue(s\"check failed for activation: $activation\") {\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(testResult)\n      activation.logs shouldBe Some(List.empty)\n    }\n  }\n\n  it should \"not be able to use 'ping' in an action\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"ping\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"ping.js\")))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> \"google.com\".toJson))\n    withActivation(wsk.activation, run) { activation =>\n      val result = activation.response.result.get\n      result.asJsObject.getFields(\"stdout\", \"code\") match {\n        case Seq(JsString(stdout), JsNumber(code)) =>\n          stdout should not include \"bytes from\"\n          code.intValue should not be 0\n        case _ => fail(s\"fields 'stdout' or 'code' where not of the expected format, was $result\")\n      }\n    }\n  }\n\n  it should \"support UTF-8 as input and output format\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"utf8Test\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    val utf8 = \"«ταБЬℓσö»: 1<2 & 4+1>³, now 20%€§$ off!\"\n    val run = wsk.action.invoke(name, Map(\"payload\" -> utf8.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.logs.get.mkString(\" \") should include(s\"hello, $utf8\")\n    }\n  }\n\n  it should \"invoke action with large code\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"big-hello\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      val filePath = TestUtils.getTestActionFilename(\"hello.js\")\n      val code = FileUtils.readFileToString(new File(filePath), StandardCharsets.UTF_8)\n      val largeCode = code + \" \" * (WhiskProperties.getMaxActionSizeMB * FileUtils.ONE_MB).toInt\n      val tmpFile = File.createTempFile(\"whisk\", \".js\")\n      FileUtils.write(tmpFile, largeCode, StandardCharsets.UTF_8)\n      val result = action.create(name, Some(tmpFile.getAbsolutePath))\n      tmpFile.delete()\n      result\n    }\n\n    val hello = \"hello\"\n    val run = wsk.action.invoke(name, Map(\"payload\" -> hello.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.logs.get.mkString(\" \") should include(s\"hello, $hello\")\n    }\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/limits/ThrottleTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage limits\n\nimport java.time.Instant\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.TooManyRequests\n\nimport scala.concurrent.{Await, Future, Promise}\nimport scala.concurrent.duration._\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport common._\nimport common.TestUtils._\nimport common.rest.WskRestOperations\nimport common.WskAdmin.wskadmin\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.http.Messages._\nimport org.apache.openwhisk.utils.ExecutionContextFactory\nimport org.apache.openwhisk.utils.retry\n\nimport scala.util.{Success, Try}\n\nprotected[limits] trait LocalHelper {\n  def prefix(msg: String) = msg.substring(0, msg.indexOf('('))\n}\n\n@RunWith(classOf[JUnitRunner])\nclass ThrottleTests\n    extends AnyFlatSpec\n    with TestHelpers\n    with WskTestHelpers\n    with WskActorSystem\n    with ScalaFutures\n    with Matchers\n    with LocalHelper {\n\n  // use an infinite thread pool so that activations do not wait to send the activation requests\n  override implicit val executionContext = ExecutionContextFactory.makeCachedThreadPoolExecutionContext()\n\n  implicit val testConfig = PatienceConfig(5.minutes)\n  implicit val wskprops = WskProps()\n  val wsk = new WskRestOperations\n  val defaultAction = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n\n  val throttleWindow = 1.minute\n\n  // Due to the overhead of the per minute limit in the controller, we add this overhead here as well.\n  val overhead = if (WhiskProperties.getControllerHosts.split(\",\").length > 1) 1.2 else 1.0\n  val maximumInvokesPerMinute = math.ceil(getLimit(\"limits.actions.invokes.perMinute\") * overhead).toInt\n  val maximumFiringsPerMinute = math.ceil(getLimit(\"limits.triggers.fires.perMinute\") * overhead).toInt\n  val maximumConcurrentInvokes = getLimit(\"limits.actions.invokes.concurrent\")\n\n  println(s\"maximumInvokesPerMinute  = $maximumInvokesPerMinute\")\n  println(s\"maximumFiringsPerMinute  = $maximumFiringsPerMinute\")\n  println(s\"maximumConcurrentInvokes = $maximumConcurrentInvokes\")\n\n  /*\n   * Retrieve a numeric limit for the key from the property set.\n   */\n  def getLimit(key: String) = WhiskProperties.getProperty(key).toInt\n\n  /**\n   * Extracts the number of throttled results from a sequence of <code>RunResult</code>\n   *\n   * @param results the sequence of results\n   * @param message the message to determine the type of throttling\n   */\n  def throttledActivations(results: List[RunResult], message: String) = {\n    val count = results.count { result =>\n      result.exitCode == TestUtils.THROTTLED && result.stderr.contains(prefix(message))\n    }\n    println(s\"number of throttled activations: $count out of ${results.length}\")\n    count\n  }\n\n  /**\n   * Waits until all successful activations are finished. Used to prevent the testcases from\n   * leaking activations.\n   *\n   * @param results the sequence of results from invocations or firings\n   */\n  def waitForActivations(results: Seq[RunResult]) = {\n    val done = results.map { result =>\n      if (result.exitCode == SUCCESS_EXIT) {\n        Future(withActivation(wsk.activation, result, totalWait = 5.minutes)(_ => ()))\n      } else {\n        Future.successful(())\n      }\n    }\n    Await.result(Future.sequence(done), 5.minutes)\n  }\n\n  /**\n   * Settles throttles of 1 minute. Waits up to 1 minute depending on the time already waited.\n   *\n   * @param waitedAlready the time already gone after the last invoke or fire\n   */\n  def settleThrottles(waitedAlready: FiniteDuration) = {\n    val timeToWait = (throttleWindow - waitedAlready).max(Duration.Zero)\n    println(s\"Waiting for ${timeToWait.toSeconds} seconds, already waited for ${waitedAlready.toSeconds} seconds\")\n    Thread.sleep(timeToWait.toMillis)\n  }\n\n  /**\n   * Calculates the <code>Duration</code> between two <code>Instant</code>\n   *\n   * @param start the Instant something started\n   * @param end the Instant something ended\n   */\n  def durationBetween(start: Instant, end: Instant) = Duration.fromNanos(java.time.Duration.between(start, end).toNanos)\n\n  /**\n   * Invokes the given action up to 'count' times until one of the invokes is throttled.\n   *\n   * @param count maximum invocations to make\n   */\n  def untilThrottled(count: Int, retries: Int = 3)(run: () => RunResult): List[RunResult] = {\n    val p = Promise[Unit]\n\n    val results = List.fill(count)(Future {\n      if (!p.isCompleted) {\n        val rr = run()\n        if (rr.exitCode != SUCCESS_EXIT) {\n          println(s\"exitCode = ${rr.exitCode}   stderr = ${rr.stderr.trim}\")\n        }\n        if (rr.exitCode == THROTTLED) {\n          p.trySuccess(())\n        }\n        Some(rr)\n      } else {\n        println(\"already throttled, skipping additional runs\")\n        None\n      }\n    })\n\n    val finished = Future.sequence(results).futureValue.flatten\n    // some activations may need to be retried\n    val failed = finished filter { rr =>\n      rr.exitCode != SUCCESS_EXIT && rr.exitCode != THROTTLED\n    }\n\n    println(\n      s\"Executed ${finished.length} requests, maximum was $count, need to retry ${failed.length} (retries left: $retries)\")\n    if (failed.isEmpty || retries <= 0) {\n      finished\n    } else {\n      finished ++ untilThrottled(failed.length, retries - 1)(run)\n    }\n  }\n\n  behavior of \"Throttles\"\n\n  it should \"throttle multiple activations of one action\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"checkPerMinuteActionThrottle\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, defaultAction)\n    }\n\n    // Three things to be careful of:\n    //   1) We do not know the minute boundary so we perform twice max so that it will trigger no matter where they fall\n    //   2) We cannot issue too quickly or else the concurrency throttle will be triggered\n    //   3) In the worst case, we do about almost the limit in the first min and just exceed the limit in the second min.\n    val totalInvokes = 2 * maximumInvokesPerMinute\n    val numGroups = (totalInvokes / maximumConcurrentInvokes) + 1\n    val invokesPerGroup = (totalInvokes / numGroups) + 1\n    val interGroupSleep = 5.seconds\n    val results = (1 to numGroups).flatMap { i =>\n      if (i != 1) { Thread.sleep(interGroupSleep.toMillis) }\n      untilThrottled(invokesPerGroup) { () =>\n        wsk.action.invoke(name, Map(\"payload\" -> \"testWord\".toJson), expectedExitCode = DONTCARE_EXIT)\n      }\n    }.toList\n    val afterInvokes = Instant.now\n\n    try {\n      val throttledCount = throttledActivations(results, tooManyRequests(0, 0))\n      throttledCount should be > 0\n    } finally {\n      val alreadyWaited = durationBetween(afterInvokes, Instant.now)\n      settleThrottles(alreadyWaited)\n      println(\"clearing activations\")\n    }\n    // wait for the activations last, if these fail, the throttle should be settled\n    // and this gives the activations time to complete and may avoid unnecessarily polling\n    println(\"waiting for activations to complete\")\n    waitForActivations(results)\n  }\n\n  it should \"throttle multiple activations of one trigger\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"checkPerMinuteTriggerThrottle\"\n    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n      trigger.create(name)\n    }\n\n    // invokes per minute * 2 because the current minute could advance which resets the throttle\n    val results = untilThrottled(maximumFiringsPerMinute * 2 + 1) { () =>\n      wsk.trigger.fire(name, Map(\"payload\" -> \"testWord\".toJson), expectedExitCode = DONTCARE_EXIT)\n    }\n    val afterFirings = Instant.now\n\n    try {\n      val throttledCount = throttledActivations(results, tooManyRequests(0, 0))\n      throttledCount should be > 0\n    } finally {\n      // no need to wait for activations of triggers since they consume no resources\n      // (because there is no rule attached in this test)\n      val alreadyWaited = durationBetween(afterFirings, Instant.now)\n      settleThrottles(alreadyWaited)\n    }\n  }\n}\n\n@RunWith(classOf[JUnitRunner])\nclass NamespaceSpecificThrottleTests\n    extends AnyFlatSpec\n    with TestHelpers\n    with WskTestHelpers\n    with WskActorSystem\n    with Matchers\n    with BeforeAndAfterAll\n    with LocalHelper {\n\n  val defaultAction = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n\n  val wsk = new WskRestOperations\n\n  def sanitizeNamespaces(namespaces: Seq[String], expectedExitCode: Int = SUCCESS_EXIT): Unit = {\n    val deletions = namespaces.map { ns =>\n      Try {\n        disposeAdditionalTestSubject(ns, expectedExitCode)\n        withClue(s\"failed to delete temporary limits for $ns\") {\n          wskadmin.cli(Seq(\"limits\", \"delete\", ns), expectedExitCode)\n        }\n      }\n    }\n    if (expectedExitCode == SUCCESS_EXIT) every(deletions) shouldBe a[Success[_]]\n  }\n\n  sanitizeNamespaces(\n    Seq(\"zeroSubject\", \"zeroConcSubject\", \"oneSubject\", \"oneSequenceSubject\", \"activationDisabled\"),\n    expectedExitCode = DONTCARE_EXIT)\n\n  // Create a subject with rate limits == 0\n  val zeroProps = getAdditionalTestSubject(\"zeroSubject\")\n  wskadmin.cli(\n    Seq(\n      \"limits\",\n      \"set\",\n      zeroProps.namespace,\n      \"--invocationsPerMinute\",\n      \"0\",\n      \"--firesPerMinute\",\n      \"0\",\n      \"--concurrentInvocations\",\n      \"0\"))\n\n  // Create a subject where only the concurrency limit is set to 0\n  val zeroConcProps = getAdditionalTestSubject(\"zeroConcSubject\")\n  wskadmin.cli(Seq(\"limits\", \"set\", zeroConcProps.namespace, \"--concurrentInvocations\", \"0\"))\n\n  // Create a subject where the rate limits are set to 1\n  val oneProps = getAdditionalTestSubject(\"oneSubject\")\n  wskadmin.cli(Seq(\"limits\", \"set\", oneProps.namespace, \"--invocationsPerMinute\", \"1\", \"--firesPerMinute\", \"1\"))\n\n  // Create a subject where the rate limits are set to 1 for testing sequences\n  val oneSequenceProps = getAdditionalTestSubject(\"oneSequenceSubject\")\n  wskadmin.cli(Seq(\"limits\", \"set\", oneSequenceProps.namespace, \"--invocationsPerMinute\", \"1\", \"--firesPerMinute\", \"1\"))\n\n  // Create a subject where storing of activations in activationstore is disabled.\n  val activationDisabled = getAdditionalTestSubject(\"activationDisabled\")\n  wskadmin.cli(Seq(\"limits\", \"set\", activationDisabled.namespace, \"--storeActivations\", \"false\"))\n\n  override def afterAll() = {\n    sanitizeNamespaces(Seq(zeroProps, zeroConcProps, oneProps, oneSequenceProps, activationDisabled).map(_.namespace))\n  }\n\n  behavior of \"Namespace-specific throttles\"\n\n  it should \"respect overridden rate-throttles of 0\" in withAssetCleaner(zeroProps) { (wp, assetHelper) =>\n    implicit val props = wp\n    val triggerName = \"zeroTrigger\"\n    val actionName = \"zeroAction\"\n\n    assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n      action.create(actionName, defaultAction)\n    }\n    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>\n      trigger.create(triggerName)\n    }\n\n    wsk.action.invoke(actionName, expectedExitCode = TooManyRequests.intValue).stderr should {\n      include(prefix(tooManyRequests(0, 0))) and include(\"allowed: 0\")\n    }\n    wsk.trigger.fire(triggerName, expectedExitCode = TooManyRequests.intValue).stderr should {\n      include(prefix(tooManyRequests(0, 0))) and include(\"allowed: 0\")\n    }\n  }\n\n  it should \"respect overridden rate-throttles of 1\" in withAssetCleaner(oneProps) { (wp, assetHelper) =>\n    implicit val props = wp\n    val triggerName = \"oneTrigger\"\n    val actionName = \"oneAction\"\n\n    assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n      action.create(actionName, defaultAction)\n    }\n    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>\n      trigger.create(triggerName)\n    }\n\n    val deployedControllers = WhiskProperties.getControllerHosts.split(\",\").length\n\n    // One invoke should be allowed, the second one throttled.\n    // Due to the current implementation of the rate throttling,\n    // it is possible that the counter gets deleted, because the minute switches.\n    retry({\n      val results = (1 to deployedControllers + 1).map { _ =>\n        wsk.action.invoke(actionName, expectedExitCode = TestUtils.DONTCARE_EXIT)\n      }\n      results.map(_.exitCode) should contain(TestUtils.THROTTLED)\n      results.map(_.stderr).mkString should {\n        include(prefix(tooManyRequests(0, 0))) and include(\"allowed: 1\")\n      }\n    }, 2, Some(1.second))\n\n    // One fire should be allowed, the second one throttled.\n    // Due to the current implementation of the rate throttling,\n    // it is possible, that the counter gets deleted, because the minute switches.\n    retry({\n      val results = (1 to deployedControllers + 1).map { _ =>\n        wsk.trigger.fire(triggerName, expectedExitCode = TestUtils.DONTCARE_EXIT)\n      }\n      results.map(_.exitCode) should contain(TestUtils.THROTTLED)\n      results.map(_.stderr).mkString should {\n        include(prefix(tooManyRequests(0, 0))) and include(\"allowed: 1\")\n      }\n    }, 2, Some(1.second))\n  }\n\n  // One sequence invocation should count as one invocation for rate throttling purposes.\n  // This is independent of the number of actions in the sequences.\n  it should \"respect overridden rate-throttles of 1 for sequences\" in withAssetCleaner(oneSequenceProps) {\n    (wp, assetHelper) =>\n      implicit val props = wp\n\n      val actionName = \"oneAction\"\n      val sequenceName = \"oneSequence\"\n\n      assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n        action.create(actionName, defaultAction)\n      }\n\n      assetHelper.withCleaner(wsk.action, sequenceName) { (action, _) =>\n        action.create(sequenceName, Some(s\"$actionName,$actionName\"), kind = Some(\"sequence\"))\n      }\n\n      val deployedControllers = WhiskProperties.getControllerHosts.split(\",\").length\n\n      // One invoke should be allowed.\n      wsk.action\n        .invoke(sequenceName, expectedExitCode = TestUtils.DONTCARE_EXIT)\n        .exitCode shouldBe TestUtils.SUCCESS_EXIT\n\n      // One invoke should be allowed, the second one throttled.\n      // Due to the current implementation of the rate throttling,\n      // it is possible that the counter gets deleted, because the minute switches.\n      retry({\n        val results = (1 to deployedControllers + 1).map { _ =>\n          wsk.action.invoke(sequenceName, expectedExitCode = TestUtils.DONTCARE_EXIT)\n        }\n        results.map(_.exitCode) should contain(TestUtils.THROTTLED)\n        results.map(_.stderr).mkString should {\n          include(prefix(tooManyRequests(0, 0))) and include(\"allowed: 1\")\n        }\n      }, 2, Some(1.second))\n  }\n\n  it should \"not store an activation if disabled for this namespace\" in withAssetCleaner(activationDisabled) {\n    (wp, assetHelper) =>\n      implicit val props = wp\n      val actionName = \"activationDisabled\"\n\n      assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n        action.create(actionName, defaultAction)\n      }\n\n      val runResult = wsk.action.invoke(actionName)\n      val activationId = wsk.activation.extractActivationId(runResult)\n      withClue(s\"did not find an activation id in '$runResult'\") {\n        activationId shouldBe a[Some[_]]\n      }\n\n      val activation = wsk.activation.waitForActivation(activationId.get)\n\n      activation shouldBe 'Left\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/ConfigMXBeanTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\nimport java.lang.management.ManagementFactory\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\n@RunWith(classOf[JUnitRunner])\nclass ConfigMXBeanTests extends AnyFlatSpec with Matchers {\n  behavior of \"ConfigMBean\"\n\n  it should \"return config at path\" in {\n    ConfigMXBean.register()\n    val config = ManagementFactory.getPlatformMBeanServer.invoke(\n      ConfigMXBean.name,\n      \"getConfig\",\n      Array(\"whisk.spi\", java.lang.Boolean.FALSE),\n      Array(\"java.lang.String\", \"boolean\"))\n    config.asInstanceOf[String] should include(\"ArtifactStoreProvider\")\n    ConfigMXBean.unregister()\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/ConfigMapValueTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.nio.file.Files\n\nimport com.typesafe.config.ConfigFactory\nimport org.apache.commons.io.FileUtils\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport pureconfig._\nimport pureconfig.generic.auto._\n\n@RunWith(classOf[JUnitRunner])\nclass ConfigMapValueTests extends AnyFlatSpec with Matchers {\n  behavior of \"ConfigMapValue\"\n\n  case class ValueTest(template: ConfigMapValue, count: Int)\n\n  it should \"read from string\" in {\n    val config = ConfigFactory.parseString(\"\"\"\n       |whisk {\n       |  value-test {\n       |    template = \"test string\"\n       |    count = 42\n       |  }\n       |}\"\"\".stripMargin)\n\n    val valueTest = readValueTest(config)\n    valueTest.template.value shouldBe \"test string\"\n  }\n\n  it should \"read from file reference\" in {\n    val file = Files.createTempFile(\"whisk\", null).toFile\n    FileUtils.write(file, \"test string\", UTF_8)\n\n    val config = ConfigFactory.parseString(s\"\"\"\n       |whisk {\n       |  value-test {\n       |    template = \"${file.toURI}\"\n       |    count = 42\n       |  }\n       |}\"\"\".stripMargin)\n\n    val valueTest = readValueTest(config)\n    valueTest.template.value shouldBe \"test string\"\n\n    file.delete()\n  }\n\n  private def readValueTest(config: com.typesafe.config.Config) = {\n    loadConfigOrThrow[ValueTest](config.getConfig(\"whisk.value-test\"))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/ConfigTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.StreamLogging\n\n@RunWith(classOf[JUnitRunner])\nclass ConfigTests extends AnyFlatSpec with Matchers with StreamLogging {\n\n  \"Config\" should \"gets default value\" in {\n    val config = new Config(Map(\"a\" -> \"A\"))(Map.empty)\n    assert(config.isValid && config(\"a\") == \"A\")\n  }\n\n  it should \"get value from environment\" in {\n    val config = new Config(Map(\"a\" -> null, \"b\" -> \"\"))(Map(\"A\" -> \"xyz\"))\n    assert(config.isValid && config(\"a\") == \"xyz\" && config(\"b\") == \"\")\n  }\n\n  it should \"not be valid when environment does not provide value\" in {\n    val config = new Config(Map(\"a\" -> null))(Map.empty)\n    assert(!config.isValid && config(\"a\") == null)\n  }\n\n  it should \"be invalid if same property is required and optional and still not defined\" in {\n    val config = new Config(Map(\"a\" -> null), optionalProperties = Set(\"a\"))(Map.empty)\n    assert(!config.isValid)\n  }\n\n  it should \"read optional value\" in {\n    val config = new Config(Map(\"a\" -> \"A\", \"x\" -> \"X\"), optionalProperties = Set(\"b\", \"c\", \"x\"))(Map(\"B\" -> \"B\"))\n    assert(config.isValid)\n    assert(config(\"a\") == \"A\")\n    assert(config(\"b\") == \"B\")\n    assert(config(\"c\") == \"\")\n    assert(config(\"x\") == \"X\")\n  }\n\n  it should \"override a value with optional value\" in {\n    val config =\n      new Config(Map(\"a\" -> null, \"x\" -> \"X\"), optionalProperties = Set(\"b\", \"c\", \"x\"))(Map(\"A\" -> \"A\", \"B\" -> \"B\"))\n    assert(config.isValid && config(\"a\") == \"A\" && config(\"b\") == \"B\")\n    assert(config(\"a\", \"b\") == \"B\")\n    assert(config(\"a\", \"c\") == \"A\")\n    assert(config(\"c\") == \"\")\n    assert(config(\"x\") == \"X\")\n    assert(config(\"x\", \"c\") == \"X\")\n    assert(config(\"x\", \"d\") == \"X\")\n    assert(config(\"d\", \"x\") == \"X\")\n    assert(config(\"c\", \"x\") == \"X\")\n    assert(config(\"c\", \"d\") == \"\")\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/ForcibleSemaphoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport common.ConcurrencyHelpers\nimport org.apache.openwhisk.utils.ExecutionContextFactory\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration.DurationInt\n\n@RunWith(classOf[JUnitRunner])\nclass ForcibleSemaphoreTests extends AnyFlatSpec with Matchers with ConcurrencyHelpers {\n  // use an infinite thread pool to allow for maximum concurrency\n  implicit val executionContext = ExecutionContextFactory.makeCachedThreadPoolExecutionContext()\n\n  behavior of \"ForcableSemaphore\"\n\n  it should \"not allow to acquire, force or release negative amounts of permits\" in {\n    val s = new ForcibleSemaphore(2)\n    an[IllegalArgumentException] should be thrownBy s.tryAcquire(0)\n    an[IllegalArgumentException] should be thrownBy s.tryAcquire(-1)\n\n    an[IllegalArgumentException] should be thrownBy s.forceAcquire(0)\n    an[IllegalArgumentException] should be thrownBy s.forceAcquire(-1)\n\n    an[IllegalArgumentException] should be thrownBy s.release(0)\n    an[IllegalArgumentException] should be thrownBy s.release(-1)\n  }\n\n  it should \"allow to acquire the defined amount of permits only\" in {\n    val s = new ForcibleSemaphore(2)\n    s.tryAcquire() shouldBe true // 1 permit left\n    s.tryAcquire() shouldBe true // 0 permits left\n    s.tryAcquire() shouldBe false\n\n    val s2 = new ForcibleSemaphore(4)\n    s2.tryAcquire(5) shouldBe false // only 4 permits available\n    s2.tryAcquire(3) shouldBe true // 1 permit left\n    s2.tryAcquire(2) shouldBe false // only 1 permit available\n    s2.tryAcquire() shouldBe true\n  }\n\n  it should \"allow to release permits again\" in {\n    val s = new ForcibleSemaphore(2)\n    s.tryAcquire() shouldBe true // 1 permit left\n    s.tryAcquire() shouldBe true // 0 permits left\n    s.tryAcquire() shouldBe false\n    s.release() // 1 permit left\n    s.tryAcquire() shouldBe true\n    s.release(2) // 1 permit left\n    s.tryAcquire(2) shouldBe true\n  }\n\n  it should \"allow to force permits, delaying the acceptance of 'usual' permits until all of forced permits are released\" in {\n    val s = new ForcibleSemaphore(2)\n    s.tryAcquire(2) shouldBe true // 0 permits left\n    s.forceAcquire(5) // -5 permits left\n    s.tryAcquire() shouldBe false\n    s.release(4) // -1 permits left\n    s.tryAcquire() shouldBe false\n    s.release() // 0 permits left\n    s.tryAcquire() shouldBe false\n    s.release() // 1 permit left\n    s.tryAcquire() shouldBe true\n  }\n\n  it should \"not give away more permits even under concurrent load\" in {\n    // 100 iterations of this test\n    (0 until 100).foreach { _ =>\n      val s = new ForcibleSemaphore(32)\n      // try to acquire more permits than allowed in parallel\n      val acquires = concurrently(64, 1.minute)(s.tryAcquire())\n\n      val result = Seq.fill(32)(true) ++ Seq.fill(32)(false)\n      acquires should contain theSameElementsAs result\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/NestedSemaphoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport common.ConcurrencyHelpers\nimport org.apache.openwhisk.utils.ExecutionContextFactory\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration.DurationInt\n\n@RunWith(classOf[JUnitRunner])\nclass NestedSemaphoreTests extends AnyFlatSpec with Matchers with ConcurrencyHelpers {\n  // use an infinite thread pool to allow for maximum concurrency\n  implicit val executionContext = ExecutionContextFactory.makeCachedThreadPoolExecutionContext()\n  val acquireTimeout = 1.minute\n\n  behavior of \"NestedSemaphore\"\n\n  it should \"allow acquire of concurrency permits before acquire of memory permits\" in {\n    val s = new NestedSemaphore[String](20)\n    s.availablePermits shouldBe 20\n\n    val actionId = \"action1\"\n    val actionConcurrency = 5\n    val actionMemory = 3\n    //use all concurrency on a single slot\n    concurrently(5, acquireTimeout) {\n      s.tryAcquireConcurrent(actionId, actionConcurrency, actionMemory)\n    } should contain only true\n    s.availablePermits shouldBe 20 - 3 //we used a single container (memory == 3)\n    s.concurrentState(actionId).availablePermits shouldBe 0\n\n    //use up all the remaining memory (17) and concurrency slots (17 / 3 * 5 = 25)\n    concurrently(25, acquireTimeout) {\n      s.tryAcquireConcurrent(actionId, actionConcurrency, actionMemory)\n    } should contain only true\n\n    s.availablePermits shouldBe 2 //we used 18 (20/3 = 6, 6*3=18)\n    s.concurrentState(actionId).availablePermits shouldBe 0\n    s.tryAcquireConcurrent(\"action1\", actionConcurrency, actionMemory) shouldBe false\n\n  }\n\n  it should \"not give away more permits even under concurrent load\" in {\n    // 100 iterations of this test\n    (0 until 100).foreach { _ =>\n      val s = new NestedSemaphore(32)\n      // try to acquire more permits than allowed in parallel\n      val acquires = concurrently(64, acquireTimeout)(s.tryAcquire())\n\n      val result = Seq.fill(32)(true) ++ Seq.fill(32)(false)\n      acquires should contain theSameElementsAs result\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/PrometheusTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\nimport org.apache.pekko.http.scaladsl.coding.{Coders}\nimport org.apache.pekko.http.scaladsl.model.{HttpCharsets, HttpResponse}\nimport org.apache.pekko.http.scaladsl.model.headers.HttpEncodings.gzip\nimport org.apache.pekko.http.scaladsl.model.headers.{`Accept-Encoding`, `Content-Encoding`, HttpEncoding, HttpEncodings}\nimport org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport com.typesafe.config.ConfigFactory\nimport kamon.Kamon\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.matchers.Matcher\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass PrometheusTests\n    extends AnyFlatSpec\n    with Matchers\n    with ScalatestRouteTest\n    with BeforeAndAfterAll\n    with ScalaFutures {\n  behavior of \"Prometheus\"\n\n  override protected def beforeAll(): Unit = {\n    super.beforeAll()\n    //Modify Kamon to have a very small tick interval\n    val newConfig = ConfigFactory.parseString(\"\"\"kamon {\n      |  metric {\n      |    tick-interval = 50 ms\n      |    optimistic-tick-alignment = no\n      |  }\n      |}\"\"\".stripMargin).withFallback(ConfigFactory.load())\n    Kamon.reconfigure(newConfig)\n  }\n\n  override protected def afterAll(): Unit = {\n    super.afterAll()\n    Kamon.reconfigure(ConfigFactory.load())\n  }\n\n  it should \"respond to /metrics\" in {\n    val api = new KamonPrometheus\n    Kamon.counter(\"foo_bar\").withoutTags().increment(42)\n\n    //Sleep to ensure that Kamon metrics are pushed to reporters\n    Thread.sleep(2.seconds.toMillis)\n    Get(\"/metrics\") ~> `Accept-Encoding`(gzip) ~> api.route ~> check {\n      // Check that response confirms to what Prometheus scrapper accepts\n      contentType.charsetOption shouldBe Some(HttpCharsets.`UTF-8`)\n      contentType.mediaType.params(\"version\") shouldBe \"0.0.4\"\n      response should haveContentEncoding(gzip)\n\n      val responseText = Unmarshal(Coders.Gzip.decodeMessage(response)).to[String].futureValue\n      withClue(responseText) {\n        responseText should include(\"foo_bar\")\n      }\n    }\n    api.close()\n  }\n\n  it should \"not be enabled by default\" in {\n    Get(\"/metrics\") ~> MetricsRoute() ~> check {\n      handled shouldBe false\n    }\n  }\n\n  private def haveContentEncoding(encoding: HttpEncoding): Matcher[HttpResponse] =\n    be(encoding) compose {\n      (_: HttpResponse).header[`Content-Encoding`].map(_.encodings.head).getOrElse(HttpEncodings.identity)\n    }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/ResizableSemaphoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport common.ConcurrencyHelpers\nimport org.apache.openwhisk.utils.ExecutionContextFactory\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration.DurationInt\n\n@RunWith(classOf[JUnitRunner])\nclass ResizableSemaphoreTests extends AnyFlatSpec with Matchers with ConcurrencyHelpers {\n  // use an infinite thread pool to allow for maximum concurrency\n  implicit val executionContext = ExecutionContextFactory.makeCachedThreadPoolExecutionContext()\n  val acquireTimeout = 1.minute\n\n  behavior of \"ResizableSemaphore\"\n\n  it should \"not allow to acquire, force or release negative amounts of permits\" in {\n    val s = new ResizableSemaphore(2, 5)\n    an[IllegalArgumentException] should be thrownBy s.tryAcquire(0)\n    an[IllegalArgumentException] should be thrownBy s.tryAcquire(-1)\n\n    an[IllegalArgumentException] should be thrownBy s.release(0, true)\n    an[IllegalArgumentException] should be thrownBy s.release(-1, true)\n  }\n\n  it should \"allow to acquire the defined amount of permits only\" in {\n    val s = new ResizableSemaphore(2, 5)\n    s.tryAcquire() shouldBe true // 1 permit left\n    s.tryAcquire() shouldBe true // 0 permits left\n    s.tryAcquire() shouldBe false\n\n    val s2 = new ForcibleSemaphore(4)\n    s2.tryAcquire(5) shouldBe false // only 4 permits available\n    s2.tryAcquire(3) shouldBe true // 1 permit left\n    s2.tryAcquire(2) shouldBe false // only 1 permit available\n    s2.tryAcquire() shouldBe true\n  }\n\n  it should \"allow to release permits again\" in {\n    val s = new ForcibleSemaphore(2)\n    s.tryAcquire() shouldBe true // 1 permit left\n    s.tryAcquire() shouldBe true // 0 permits left\n    s.tryAcquire() shouldBe false\n    s.release() // 1 permit left\n    s.tryAcquire() shouldBe true\n    s.release(2) // 1 permit left\n    s.tryAcquire(2) shouldBe true\n  }\n\n  it should \"allow to resize permits when factor of reductionSize is reached during release\" in {\n    val s = new ResizableSemaphore(2, 5)\n    s.tryAcquire() shouldBe true // 1 permit left\n    s.counter shouldBe 1\n    s.tryAcquire() shouldBe true // 0 permits left\n    s.counter shouldBe 2\n    s.tryAcquire() shouldBe false\n    s.counter shouldBe 2\n    s.release(4, false) shouldBe (false, false) // 4 permits left\n    s.counter shouldBe 3\n\n    s.tryAcquire(4) shouldBe true\n    s.counter shouldBe 4\n\n    s.tryAcquire() shouldBe false\n    s.counter shouldBe 4\n    s.release(5, false) shouldBe (true, false) // 0 permits left (5 permits reduced to 0)\n    s.counter shouldBe 5\n    s.tryAcquire() shouldBe false\n    s.counter shouldBe 5\n    s.release(6, false) shouldBe (false, false) // 5 permits left\n    s.counter shouldBe 6\n    s.tryAcquire() shouldBe true // 5 permits left\n    s.counter shouldBe 7\n    s.tryAcquire() shouldBe true // 4 permits left\n    s.counter shouldBe 8\n    s.tryAcquire() shouldBe true // 3 permits left\n    s.counter shouldBe 9\n    s.tryAcquire() shouldBe true // 2 permits left\n    s.counter shouldBe 10\n    s.tryAcquire() shouldBe true // 1 permits left\n    s.counter shouldBe 11\n    s.tryAcquire() shouldBe true // 0 permits left\n    s.counter shouldBe 12\n\n    s.tryAcquire() shouldBe false\n    s.counter shouldBe 12\n    s.release(10, false) shouldBe (true, false) // 5 permits left (10 permits reduced to 5)\n    s.counter shouldBe 13\n    s.tryAcquire() shouldBe true\n    s.counter shouldBe 14\n    s.availablePermits shouldBe 4\n    s.release(1, true) shouldBe (true, false)\n    s.counter shouldBe 13\n    s.availablePermits shouldBe 0\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 12\n    s.availablePermits shouldBe 1\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 11\n    s.availablePermits shouldBe 2\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 10\n    s.availablePermits shouldBe 3\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 9\n    s.availablePermits shouldBe 4\n\n    s.release(1, true) shouldBe (true, false)\n    s.counter shouldBe 8\n    s.availablePermits shouldBe 0\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 7\n    s.availablePermits shouldBe 1\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 6\n    s.availablePermits shouldBe 2\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 5\n    s.availablePermits shouldBe 3\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 4\n    s.availablePermits shouldBe 4\n\n    s.release(1, true) shouldBe (true, false)\n    s.counter shouldBe 3\n    s.availablePermits shouldBe 0\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 2\n    s.availablePermits shouldBe 1\n\n    s.release(1, true) shouldBe (false, false)\n    s.counter shouldBe 1\n    s.availablePermits shouldBe 2\n\n    s.release(1, true) shouldBe (false, true)\n    s.counter shouldBe 0\n    s.availablePermits shouldBe 3\n  }\n\n  it should \"not give away more permits even under concurrent load\" in {\n    // 100 iterations of this test\n    (0 until 100).foreach { _ =>\n      val s = new ResizableSemaphore(32, 35)\n      // try to acquire more permits than allowed in parallel\n      val acquires = concurrently(64, acquireTimeout)(s.tryAcquire())\n\n      val result = Seq.fill(32)(true) ++ Seq.fill(32)(false)\n      acquires should contain theSameElementsAs result\n    }\n  }\n\n  it should \"release permits even under concurrent load\" in {\n    val s = new ResizableSemaphore(32, 35)\n    // try to acquire more permits than allowed in parallel\n    concurrently(64, acquireTimeout)(s.tryAcquire())\n    concurrently(32, acquireTimeout)(s.release(1, true))\n\n    s.counter shouldBe 0\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/RunCliCmdTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.io.File\n\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport common.RunCliCmd\nimport common.TestUtils._\n\nimport scala.collection.mutable.Buffer\n\n@RunWith(classOf[JUnitRunner])\nclass RunCliCmdTests extends AnyFlatSpec with RunCliCmd with BeforeAndAfterEach {\n\n  case class TestRunResult(code: Int) extends RunResult(code, \"\", \"\")\n  val defaultRR = TestRunResult(0)\n\n  override def baseCommand = Buffer.empty\n\n  override def runCmd(expectedExitCode: Int,\n                      dir: File,\n                      env: Map[String, String],\n                      fileStdin: Option[File],\n                      params: Seq[String]): RunResult = {\n    cmdCount += 1\n    rr.getOrElse(defaultRR)\n  }\n\n  override def beforeEach() = {\n    rr = None\n    cmdCount = 0\n  }\n\n  var rr: Option[TestRunResult] = None // optional run result override per test\n  var cmdCount = 0\n\n  it should \"retry commands that experience network errors\" in {\n    Seq(ANY_ERROR_EXIT, DONTCARE_EXIT, NETWORK_ERROR_EXIT).foreach { code =>\n      cmdCount = 0\n\n      rr = Some(TestRunResult(NETWORK_ERROR_EXIT))\n      noException shouldBe thrownBy {\n        cli(Seq.empty, expectedExitCode = code)\n      }\n\n      cmdCount shouldBe 3 + 1\n    }\n  }\n\n  it should \"not retry commands if retry is disabled\" in {\n    rr = Some(TestRunResult(NETWORK_ERROR_EXIT))\n    noException shouldBe thrownBy {\n      cli(Seq.empty, expectedExitCode = ANY_ERROR_EXIT, retriesOnError = 0)\n    }\n\n    cmdCount shouldBe 1\n  }\n\n  it should \"retry commands if any failure is happen\" in {\n    Seq(MISUSE_EXIT, ERROR_EXIT).foreach { code =>\n      cmdCount = 0\n\n      rr = Some(TestRunResult(code))\n      noException shouldBe thrownBy {\n        cli(Seq.empty, expectedExitCode = DONTCARE_EXIT, retriesOnError = 3)\n      }\n\n      cmdCount shouldBe 4\n    }\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/SchedulerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.time.Instant\n\nimport scala.collection.mutable.Buffer\nimport scala.concurrent.Await\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.language.postfixOps\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.actor.PoisonPill\nimport common.StreamLogging\nimport common.WskActorSystem\n\n@RunWith(classOf[JUnitRunner])\nclass SchedulerTests extends AnyFlatSpec with Matchers with WskActorSystem with StreamLogging {\n\n  val timeBetweenCalls = 50 milliseconds\n  val callsToProduce = 5\n  val schedulerSlack = 100 milliseconds\n\n  /**\n   * Calculates the duration between two consecutive elements\n   *\n   * @param times the points in time to calculate the difference between\n   * @return duration between each element of the given sequence\n   */\n  def calculateDifferences(times: Seq[Instant]) = {\n    times sliding (2) map {\n      case Seq(a, b) => Duration.fromNanos(java.time.Duration.between(a, b).toNanos)\n    } toList\n  }\n\n  /**\n   * Waits for the calls to be scheduled and executed. Adds one call-interval as additional slack\n   */\n  def waitForCalls(calls: Int = callsToProduce, interval: FiniteDuration = timeBetweenCalls) =\n    Thread.sleep((calls + 1) * interval toMillis)\n\n  behavior of \"A WaitAtLeast Scheduler\"\n\n  ignore should \"be killable by sending it a poison pill\" in {\n    var callCount = 0\n    val scheduled = Scheduler.scheduleWaitAtLeast(timeBetweenCalls) { () =>\n      callCount += 1\n      Future successful true\n    }\n\n    waitForCalls()\n    // This is equal to a scheduled ! PoisonPill\n    val shutdownTimeout = 10.seconds\n    Await.result(org.apache.pekko.pattern.gracefulStop(scheduled, shutdownTimeout, PoisonPill), shutdownTimeout)\n\n    val countAfterKill = callCount\n    callCount should be >= callsToProduce\n\n    waitForCalls()\n\n    callCount shouldBe countAfterKill\n  }\n\n  it should \"throw an exception when passed a negative duration\" in {\n    an[IllegalArgumentException] should be thrownBy Scheduler.scheduleWaitAtLeast(-100 milliseconds) { () =>\n      Future.successful(true)\n    }\n  }\n\n  it should \"wait at least the given interval between scheduled calls\" in {\n    val calls = Buffer[Instant]()\n\n    val scheduled = Scheduler.scheduleWaitAtLeast(timeBetweenCalls) { () =>\n      calls += Instant.now\n      Future successful true\n    }\n\n    waitForCalls()\n    scheduled ! PoisonPill\n\n    val differences = calculateDifferences(calls.toSeq)\n    withClue(s\"expecting all $differences to be >= $timeBetweenCalls\") {\n      differences.forall(_ >= timeBetweenCalls)\n    }\n  }\n\n  it should \"stop the scheduler if an uncaught exception is thrown by the passed closure\" in {\n    var callCount = 0\n    val scheduled = Scheduler.scheduleWaitAtLeast(timeBetweenCalls) { () =>\n      callCount += 1\n      throw new Exception\n    }\n\n    waitForCalls()\n\n    callCount shouldBe 1\n  }\n\n  it should \"log scheduler halt message with tid\" in {\n    implicit val transid = TransactionId.testing\n    val msg = \"test threw an exception\"\n\n    stream.reset()\n    val scheduled = Scheduler.scheduleWaitAtLeast(timeBetweenCalls) { () =>\n      throw new Exception(msg)\n    }\n\n    waitForCalls()\n    stream.toString.split(\" \").drop(1).mkString(\" \") shouldBe {\n      s\"[ERROR] [${transid.root}] [] [Scheduler] halted because $msg\\n\"\n    }\n  }\n\n  it should \"not stop the scheduler if the future from the closure is failed\" in {\n    var callCount = 0\n\n    val scheduled = Scheduler.scheduleWaitAtLeast(timeBetweenCalls) { () =>\n      callCount += 1\n      Future failed new Exception\n    }\n\n    waitForCalls()\n    scheduled ! PoisonPill\n\n    callCount shouldBe callsToProduce\n  }\n\n  \"A WaitAtMost Scheduler\" should \"wait at most the given interval between scheduled calls\" in {\n    val calls = Buffer[Instant]()\n    val timeBetweenCalls = 200 milliseconds\n    val computationTime = 100 milliseconds\n\n    val scheduled = Scheduler.scheduleWaitAtMost(timeBetweenCalls) { () =>\n      calls += Instant.now\n      org.apache.pekko.pattern.after(computationTime, actorSystem.scheduler)(Future.successful(true))\n    }\n\n    waitForCalls(interval = timeBetweenCalls)\n    scheduled ! PoisonPill\n\n    val differences = calculateDifferences(calls.toSeq)\n    withClue(s\"expecting all $differences to be <= $timeBetweenCalls\") {\n      differences should not be 'empty\n      differences.forall(_ <= timeBetweenCalls + schedulerSlack)\n    }\n  }\n\n  it should \"delay initial schedule by given duration\" in {\n    val timeBetweenCalls = 200 milliseconds\n    val initialDelay = 1.second\n    var callCount = 0\n\n    val scheduled = Scheduler.scheduleWaitAtMost(timeBetweenCalls, initialDelay) { () =>\n      callCount += 1\n      Future successful true\n    }\n\n    try {\n      Thread.sleep(initialDelay.toMillis)\n      callCount should be <= 1\n\n      Thread.sleep(2 * timeBetweenCalls.toMillis)\n      callCount should be > 1\n    } finally {\n      scheduled ! PoisonPill\n    }\n  }\n\n  it should \"perform work immediately when requested\" in {\n    val timeBetweenCalls = 200 milliseconds\n    val initialDelay = 1.second\n    var callCount = 0\n\n    val scheduled = Scheduler.scheduleWaitAtMost(timeBetweenCalls, initialDelay) { () =>\n      callCount += 1\n      Future successful true\n    }\n\n    try {\n      Thread.sleep(2 * timeBetweenCalls.toMillis)\n      callCount should be(0)\n\n      scheduled ! Scheduler.WorkOnceNow\n      Thread.sleep(timeBetweenCalls.toMillis)\n      callCount should be(1)\n    } finally {\n      scheduled ! PoisonPill\n    }\n  }\n\n  it should \"not wait when the closure takes longer than the interval\" in {\n    val calls = Buffer[Instant]()\n    val timeBetweenCalls = 200 milliseconds\n    val computationTime = 300 milliseconds\n\n    val scheduled = Scheduler.scheduleWaitAtMost(timeBetweenCalls) { () =>\n      calls += Instant.now\n      org.apache.pekko.pattern.after(computationTime, actorSystem.scheduler)(Future.successful(true))\n    }\n\n    waitForCalls(interval = timeBetweenCalls)\n    scheduled ! PoisonPill\n\n    val differences = calculateDifferences(calls.toSeq)\n    withClue(s\"expecting all $differences to be <= $computationTime\") {\n      differences should not be 'empty\n      differences.forall(_ <= computationTime + schedulerSlack)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/TransactionIdTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.time.{Instant}\nimport spray.json._\nimport common.StreamLogging\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass TransactionIdTests extends AnyFlatSpec with Matchers with StreamLogging {\n\n  behavior of \"TransactionId deserialization\"\n\n  it should \"deserialize with parent tid\" in {\n\n    val json = \"\"\"[\"ctid1\",1600000000000,[\"ptid1\",1500000000000]]\"\"\".stripMargin.parseJson\n\n    val pnow = Instant.ofEpochMilli(1500000000000L)\n    val cnow = Instant.ofEpochMilli(1600000000000L)\n\n    val ptid = TransactionId(TransactionMetadata(\"ptid1\", pnow))\n    val ctid = TransactionId(TransactionMetadata(\"ctid1\", cnow, parent = Some(ptid.meta)))\n\n    val transactionId = TransactionId.serdes.read(json)\n    transactionId shouldBe ctid\n  }\n\n  it should \"deserialize with parent tid and extraLogging parameter\" in {\n\n    val json = \"\"\"[\"ctid1\",1600000000000,true,[\"ptid1\",1500000000000,true]]\"\"\".stripMargin.parseJson\n\n    val pnow = Instant.ofEpochMilli(1500000000000L)\n    val cnow = Instant.ofEpochMilli(1600000000000L)\n\n    val ptid = TransactionId(TransactionMetadata(\"ptid1\", pnow, extraLogging = true))\n    val ctid = TransactionId(TransactionMetadata(\"ctid1\", cnow, extraLogging = true, parent = Some(ptid.meta)))\n\n    val transactionId = TransactionId.serdes.read(json)\n    transactionId shouldBe ctid\n  }\n\n  it should \"deserialize without parent tid\" in {\n\n    val json = \"\"\"[\"ctid1\",1600000000000]\"\"\".stripMargin.parseJson\n\n    val cnow = Instant.ofEpochMilli(1600000000000L)\n    val ctid = TransactionId(TransactionMetadata(\"ctid1\", cnow))\n\n    val transactionId = TransactionId.serdes.read(json)\n    transactionId shouldBe ctid\n  }\n\n  behavior of \"TransactionId serialization\"\n\n  it should \"serialize with parent tid\" in {\n\n    val pnow = Instant.ofEpochMilli(1500000000000L)\n    val cnow = Instant.ofEpochMilli(1600000000000L)\n\n    val ptid = TransactionId(TransactionMetadata(\"ptid1\", pnow))\n    val ctid = TransactionId(TransactionMetadata(\"ctid1\", cnow, parent = Some(ptid.meta)))\n\n    val js = TransactionId.serdes.write(ctid)\n\n    val js2 = \"\"\"[\"ctid1\",1600000000000,[\"ptid1\",1500000000000]]\"\"\".stripMargin.parseJson\n    js shouldBe js2\n  }\n\n  it should \"serialize with parent tid and extraLogging parameter\" in {\n\n    val pnow = Instant.ofEpochMilli(1500000000000L)\n    val cnow = Instant.ofEpochMilli(1600000000000L)\n\n    val ptid = TransactionId(TransactionMetadata(\"ptid1\", pnow, extraLogging = true))\n    val ctid = TransactionId(TransactionMetadata(\"ctid1\", cnow, extraLogging = true, parent = Some(ptid.meta)))\n\n    val js = TransactionId.serdes.write(ctid)\n\n    val js2 = \"\"\"[\"ctid1\",1600000000000,true,[\"ptid1\",1500000000000,true]]\"\"\".stripMargin.parseJson\n    js shouldBe js2\n  }\n\n  it should \"serialize without parent tid\" in {\n\n    val cnow = Instant.ofEpochMilli(1600000000000L)\n    val ctid = TransactionId(TransactionMetadata(\"ctid1\", cnow))\n\n    val js = TransactionId.serdes.write(ctid)\n    val js2 = \"\"\"[\"ctid1\",1600000000000]\"\"\".stripMargin.parseJson\n    js shouldBe js2\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/UserEventTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common\n\nimport java.nio.charset.StandardCharsets\n\nimport org.apache.pekko.actor.ActorSystem\nimport common._\nimport common.rest.WskRestOperations\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.connector.kafka.KafkaConsumerConnector\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.{Activation, EventMessage, Metric}\n\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass UserEventTests extends AnyFlatSpec with Matchers with WskTestHelpers with StreamLogging with BeforeAndAfterAll {\n\n  implicit val wskprops = WskProps()\n  implicit val system = ActorSystem(\"UserEventTestSystem\")\n\n  val wsk = new WskRestOperations\n\n  val groupid = \"kafkatest\"\n  val topic = \"events\"\n  val maxPollInterval = 60.seconds\n\n  lazy val consumer = new KafkaConsumerConnector(kafkaHosts, groupid, topic)\n  val testActionsDir = WhiskProperties.getFileRelativeToWhiskHome(\"tests/dat/actions\")\n  behavior of \"UserEvents\"\n\n  override def afterAll(): Unit = {\n    consumer.close()\n  }\n\n  def kafkaHosts: String = new WhiskConfig(WhiskConfig.kafkaHosts).kafkaHosts\n\n  def userEventsEnabled: Boolean = UserEvents.enabled\n\n  if (userEventsEnabled) {\n    it should \"invoke an action and produce user events\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n      val name = \"testUserEvents\"\n\n      assetHelper.withCleaner(wsk.action, name, confirmDelete = true) { (action, _) =>\n        action.create(name, file)\n      }\n\n      val run = wsk.action.invoke(name, blocking = true)\n\n      withActivation(wsk.activation, run) { result =>\n        withClue(\"invoking an action was unsuccessful\") {\n          result.response.status shouldBe \"success\"\n        }\n      }\n      // checking for any metrics to arrive\n      val received =\n        consumer.peek(maxPollInterval).map {\n          case (_, _, _, msg) => EventMessage.parse(new String(msg, StandardCharsets.UTF_8))\n        }\n      received.map(event => {\n        event.get.body match {\n          case a: Activation =>\n            Seq(a.statusCode) should contain oneOf (0, 1, 2, 3)\n            event.get.source should fullyMatch regex \"(invoker|controller)\\\\w+\".r\n          case m: Metric =>\n            Seq(m.metricName) should contain oneOf (\"ConcurrentInvocations\", \"ConcurrentRateLimit\", \"TimedRateLimit\")\n            event.get.source should fullyMatch regex \"controller\\\\w+\".r\n        }\n      })\n      // produce at least 2 events - an Activation and a 'ConcurrentInvocations' Metric\n      // >= 2 is due to events that might have potentially occurred in between\n      received.size should be >= 2\n      consumer.commit()\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/etcd/EtcdConfigTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common.etcd\n\nimport common.WskActorSystem\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdConfig}\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.ExecutionContextExecutor\n@RunWith(classOf[JUnitRunner])\nclass EtcdConfigTests extends AnyFlatSpec with Matchers with WskActorSystem {\n  behavior of \"EtcdConfig\"\n\n  implicit val ece: ExecutionContextExecutor = actorSystem.dispatcher\n\n  it should \"create client when no auth is supplied through config\" in {\n    val config = EtcdConfig(\"localhost:2379\", None, None)\n\n    val client = EtcdClient(config)\n    client.close()\n  }\n\n  it should \"create client when auth is supplied through config\" in {\n    val config = EtcdConfig(\"localhost:2379\", Some(\"username\"), Some(\"password\"))\n\n    val client = EtcdClient(config)\n    client.close()\n  }\n\n  it should \"fail to create client when one of username or password is supplied in config\" in {\n    val config = EtcdConfig(\"localhost:2379\", None, Some(\"password\"))\n\n    assertThrows[IllegalArgumentException](EtcdClient(config))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/etcd/EtcdKvTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common.etcd\n\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity.InvokerInstanceId\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.etcd.EtcdKV.InvokerKeys\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport pureconfig.loadConfigOrThrow\n\n@RunWith(classOf[JUnitRunner])\nclass EtcdKvTests extends AnyFlatSpec with ScalaFutures with Matchers {\n\n  behavior of \"InvokerKeys\"\n\n  val clusterName = loadConfigOrThrow[String](ConfigKeys.whiskClusterName)\n  val uniqueName = \"myUniqueName\"\n  val displayedName = \"myDisplayedName\"\n\n  it should \"serialize a InvokerInstanceId to a health-key if there is only id\" in {\n    val instanceId = InvokerInstanceId(0, userMemory = 0.MB)\n    InvokerKeys.health(instanceId) shouldBe s\"$clusterName/invokers/0\"\n  }\n\n  it should \"serialize a InvokerInstanceId to a health-key if there are id and unique name\" in {\n    val instanceId = InvokerInstanceId(0, Some(uniqueName), userMemory = 0.MB)\n    InvokerKeys.health(instanceId) shouldBe s\"$clusterName/invokers/0/$uniqueName\"\n  }\n\n  it should \"serialize a InvokerInstanceId to a health-key if there are id, unique name and displayed name\" in {\n    val instanceId = InvokerInstanceId(0, Some(uniqueName), Some(displayedName), userMemory = 0.MB)\n    InvokerKeys.health(instanceId) shouldBe s\"$clusterName/invokers/0/$uniqueName/$displayedName\"\n  }\n\n  it should \"deserialize InvokerInstanceId from ETCD key if there is only id\" in {\n    val testKey = \"$clusterName/invokers/0\"\n    val instanceId = InvokerKeys.getInstanceId(testKey)\n\n    instanceId shouldBe InvokerInstanceId(0, userMemory = 0.MB)\n  }\n\n  it should \"deserialize InvokerInstanceId from ETCD key with id and a unique name\" in {\n    val testKey = s\"$clusterName/invokers/0/$uniqueName\"\n    val instanceId = InvokerKeys.getInstanceId(testKey)\n\n    instanceId shouldBe InvokerInstanceId(0, Some(uniqueName), userMemory = 0.MB)\n  }\n\n  it should \"deserialize InvokerInstanceId from ETCD key with id, a unique name, and a displayed name\" in {\n    val testKey = s\"$clusterName/invokers/0/$uniqueName/$displayedName\"\n    val instanceId = InvokerKeys.getInstanceId(testKey)\n\n    instanceId shouldBe InvokerInstanceId(0, Some(uniqueName), Some(displayedName), userMemory = 0.MB)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/etcd/EtcdLeaderShipUnitTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common.etcd\n\nimport java.util.concurrent.Executor\nimport java.{lang, util}\n\nimport com.ibm.etcd.api.Event.EventType\nimport com.ibm.etcd.api._\nimport com.ibm.etcd.client.kv.KvClient.Watch\nimport com.ibm.etcd.client.kv.WatchUpdate\nimport com.ibm.etcd.client.{EtcdClient => Client}\nimport common.{StreamLogging, WskActorSystem}\nimport io.grpc.{StatusRuntimeException, Status => GrpcStatus}\nimport org.apache.openwhisk.core.etcd.EtcdType._\nimport org.apache.openwhisk.core.etcd.{EtcdFollower, EtcdLeader, EtcdLeadershipApi}\nimport org.apache.openwhisk.core.service.Lease\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.PatienceConfiguration.Timeout\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContextExecutor, Future}\n\n@RunWith(classOf[JUnitRunner])\nclass EtcdLeaderShipUnitTests\n    extends AnyFlatSpec\n    with ScalaFutures\n    with Matchers\n    with WskActorSystem\n    with StreamLogging {\n\n  implicit val timeout = Timeout(2.seconds)\n  private val leaderKey = \"openwhiskleader\"\n  private val endpoints = \"endpoints\"\n  private val leaseId = 60\n\n  class mockWatchUpdate extends WatchUpdate {\n    private var eventLists: util.List[Event] = new util.ArrayList[Event]()\n    override def getHeader: ResponseHeader = ???\n\n    def addEvents(event: Event): WatchUpdate = {\n      eventLists.add(event)\n      this\n    }\n\n    override def getEvents: util.List[Event] = eventLists\n  }\n\n  class MockEtcdLeadershipApi extends EtcdLeadershipApi {\n\n    override implicit val ece: ExecutionContextExecutor = actorSystem.dispatcher\n\n    override val client: Client = {\n      val hostAndPorts = \"172.17.0.1:2379\"\n      Client.forEndpoints(hostAndPorts).withPlainText().build()\n    }\n\n    var onNext: WatchUpdate => Unit = null\n\n    override def grant(ttl: Long): Future[LeaseGrantResponse] =\n      Future.successful(LeaseGrantResponse.newBuilder().setID(leaseId).setTTL(ttl).build())\n\n    override def keepAliveOnce(leaseId: Long): Future[LeaseKeepAliveResponse] =\n      Future.successful(LeaseKeepAliveResponse.newBuilder().setID(leaseId).build())\n\n    override def putTxn[T](key: String, value: T, cmpVersion: Long, leaseId: Long): Future[TxnResponse] =\n      Future.successful(TxnResponse.newBuilder().setSucceeded(true).build())\n\n    override def watch(key: String, isPrefix: Boolean)(next: WatchUpdate => Unit,\n                                                       error: Throwable => Unit,\n                                                       completed: () => Unit): Watch = {\n      onNext = next\n      new Watch {\n        override def close(): Unit = {}\n\n        override def addListener(listener: Runnable, executor: Executor): Unit = {}\n\n        override def cancel(mayInterruptIfRunning: Boolean): Boolean = true\n\n        override def isCancelled: Boolean = true\n\n        override def isDone: Boolean = true\n\n        override def get(): lang.Boolean = true\n\n        override def get(timeout: Long, unit: TimeUnit): lang.Boolean = true\n      }\n    }\n\n    def publishEvents(eventType: EventType, key: String, value: String): Unit = {\n      val eType = eventType match {\n        case EventType.PUT          => EventType.PUT\n        case EventType.DELETE       => EventType.DELETE\n        case EventType.UNRECOGNIZED => EventType.UNRECOGNIZED\n      }\n\n      val event = Event\n        .newBuilder()\n        .setType(eType)\n        .setKv(\n          KeyValue\n            .newBuilder()\n            .setKey(key)\n            .setValue(value)\n            .build())\n        .build()\n      onNext(new mockWatchUpdate().addEvents(event))\n    }\n  }\n\n  behavior of \"Etcd Leadership Client\"\n\n  \"Etcd LeaderShip client\" should \"elect leader successfully\" in {\n    val mockLeaderShipClient = new MockEtcdLeadershipApi\n\n    val either = mockLeaderShipClient.electLeader(leaderKey, endpoints).futureValue(timeout)\n    either.right.get shouldBe EtcdLeader(leaderKey, endpoints, leaseId)\n  }\n\n  \"Etcd LeaderShip client\" should \"be failed to elect leader\" in {\n    val mockLeaderShipClient = new MockEtcdLeadershipApi() {\n      override def putTxn[T](key: String, value: T, cmpVersion: Long, leaseId: Long): Future[TxnResponse] =\n        Future.successful(TxnResponse.newBuilder().setSucceeded(false).build())\n    }\n\n    val either = mockLeaderShipClient.electLeader(leaderKey, endpoints).futureValue(timeout)\n    either.left.get shouldBe EtcdFollower(leaderKey, endpoints)\n\n  }\n\n  \"Etcd LeaderShip client\" should \"elect leader successfully with provided lease\" in {\n    val mockLeaderShipClient = new MockEtcdLeadershipApi\n\n    val either = mockLeaderShipClient.electLeader(leaderKey, endpoints, Lease(leaseId, 60)).futureValue(timeout)\n    either.right.get shouldBe EtcdLeader(leaderKey, endpoints, leaseId)\n  }\n\n  \"Etcd LeaderShip client\" should \"be failed to elect leader with provided lease\" in {\n    val mockLeaderShipClient = new MockEtcdLeadershipApi() {\n      override def putTxn[T](key: String, value: T, cmpVersion: Long, leaseId: Long): Future[TxnResponse] =\n        Future.successful(TxnResponse.newBuilder().setSucceeded(false).build())\n    }\n\n    val either = mockLeaderShipClient.electLeader(leaderKey, endpoints, Lease(leaseId, 60)).futureValue(timeout)\n    either.left.get shouldBe EtcdFollower(leaderKey, endpoints)\n  }\n\n  \"Etcd LeaderShip client\" should \"throw StatusRuntimeException when provided lease doesn't exist\" in {\n    val mockLeaderShipClient = new MockEtcdLeadershipApi() {\n      override def putTxn[T](key: String, value: T, cmpVersion: Long, leaseId: Long): Future[TxnResponse] =\n        Future.failed(new StatusRuntimeException(GrpcStatus.NOT_FOUND))\n    }\n\n    mockLeaderShipClient\n      .electLeader(leaderKey, endpoints, Lease(leaseId, 60))\n      .failed\n      .futureValue shouldBe a[StatusRuntimeException]\n  }\n\n  \"Etcd LeaderShip client\" should \"keep alive leader key\" in {\n    val mockLeaderShipClient = new MockEtcdLeadershipApi\n\n    mockLeaderShipClient.keepAliveLeader(leaseId).futureValue(timeout) shouldBe leaseId\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/common/etcd/EtcdWorkerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.common.etcd\n\nimport org.apache.pekko.actor.{ActorRef, ActorSystem}\nimport org.apache.pekko.testkit.{ImplicitSender, TestActor, TestActorRef, TestKit, TestProbe}\nimport org.apache.pekko.util.Timeout\nimport com.ibm.etcd.api.{DeleteRangeResponse, PutResponse, TxnResponse}\nimport common.StreamLogging\nimport io.grpc.{Status, StatusRuntimeException}\nimport org.apache.openwhisk.core.entity.SchedulerInstanceId\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdLeader, EtcdWorker}\nimport org.apache.openwhisk.core.service.{\n  AlreadyExist,\n  Done,\n  ElectLeader,\n  ElectionResult,\n  FinishWork,\n  GetLease,\n  InitialDataStorageResults,\n  Lease,\n  RegisterData,\n  RegisterInitialData,\n  WatcherClosed\n}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass EtcdWorkerTests\n    extends TestKit(ActorSystem(\"EtcdWorker\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with StreamLogging {\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n  implicit val ec: ExecutionContext = system.dispatcher\n  val leaseService = TestProbe()\n  val leaseId = 10\n  val leaseTtl = 10\n  leaseService.setAutoPilot((sender: ActorRef, msg: Any) =>\n    msg match {\n      case GetLease =>\n        sender ! Lease(leaseId, leaseTtl)\n        TestActor.KeepRunning\n\n      case _ =>\n        TestActor.KeepRunning\n  })\n\n  //val dataManagementService = TestProbe()\n  val schedulerId = SchedulerInstanceId(\"scheduler0\")\n  val instanceId = schedulerId\n\n  behavior of \"EtcdWorker\"\n\n  it should \"elect leader and send completion ack to parent\" in {\n    val mockEtcd = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n    val leader = Right(EtcdLeader(key, value, leaseId))\n    val etcdWorker = TestActorRef(EtcdWorker.props(mockEtcd, leaseService.ref), self)\n\n    (mockEtcd\n      .electLeader(_: String, _: String, _: Lease))\n      .expects(key, value, *)\n      .returns(Future.successful(leader))\n\n    etcdWorker ! ElectLeader(key, value, recipient = self)\n\n    expectMsg(ElectionResult(leader))\n    expectMsg(FinishWork(key))\n  }\n\n  it should \"register initial data when doesn't exit and send completion ack to parent\" in {\n    val mockEtcd = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n    val etcdWorker = TestActorRef(EtcdWorker.props(mockEtcd, leaseService.ref), self)\n\n    (mockEtcd\n      .putTxn(_: String, _: String, _: Long, _: Long))\n      .expects(key, value, *, *)\n      .returns(Future.successful(TxnResponse.newBuilder().setSucceeded(true).build()))\n\n    etcdWorker ! RegisterInitialData(key, value, recipient = Some(self))\n\n    expectMsg(FinishWork(key))\n    expectMsg(InitialDataStorageResults(key, Right(Done())))\n  }\n\n  it should \"attempt to register initial data when exists and send completion ack to parent\" in {\n    val mockEtcd = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n    val etcdWorker = TestActorRef(EtcdWorker.props(mockEtcd, leaseService.ref), self)\n\n    (mockEtcd\n      .putTxn(_: String, _: String, _: Long, _: Long))\n      .expects(key, value, *, *)\n      .returns(Future.successful(TxnResponse.newBuilder().setSucceeded(false).build()))\n\n    etcdWorker ! RegisterInitialData(key, value, recipient = Some(self))\n\n    expectMsg(FinishWork(key))\n    expectMsg(InitialDataStorageResults(key, Left(AlreadyExist())))\n  }\n\n  it should \"register data and send completion ack to parent\" in {\n    val mockEtcd = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n    val etcdWorker = TestActorRef(EtcdWorker.props(mockEtcd, leaseService.ref), self)\n\n    (mockEtcd\n      .put(_: String, _: String, _: Long))\n      .expects(key, value, leaseId)\n      .returns(Future.successful(PutResponse.newBuilder().build()))\n\n    etcdWorker ! RegisterData(key, value)\n\n    expectMsg(FinishWork(key))\n  }\n\n  it should \"delete data when watcher closed\" in {\n    val mockEtcd = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val etcdWorker = TestActorRef(EtcdWorker.props(mockEtcd, leaseService.ref), self)\n\n    (mockEtcd\n      .del(_: String))\n      .expects(key)\n      .returns(Future.successful(DeleteRangeResponse.newBuilder().build()))\n\n    etcdWorker ! WatcherClosed(key, false)\n\n    expectMsg(FinishWork(key))\n  }\n\n  it should \"retry request after failure if lease does not exist\" in {\n    val mockEtcd = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val etcdWorker = TestActorRef(EtcdWorker.props(mockEtcd, leaseService.ref), self)\n    var firstAttempt = true\n    (mockEtcd\n      .del(_: String))\n      .expects(key)\n      .onCall((_: String) => {\n        if (firstAttempt) {\n          firstAttempt = false\n          Future.failed(new StatusRuntimeException(Status.RESOURCE_EXHAUSTED))\n        } else {\n          Future.successful(DeleteRangeResponse.newBuilder().build())\n        }\n      })\n      .twice()\n\n    etcdWorker ! WatcherClosed(key, false)\n\n    expectMsg(FinishWork(key))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/connector/kafka/KafkaMetricsTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.connector.kafka\n\nimport org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.kafka.common.MetricName\nimport org.apache.kafka.common.metrics.{KafkaMetric, Measurable, MetricConfig}\nimport org.apache.kafka.common.utils.Time\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.Future\n\n@RunWith(classOf[JUnitRunner])\nclass KafkaMetricsTests extends AnyFlatSpec with Matchers with ScalatestRouteTest {\n  behavior of \"KafkaMetrics\"\n\n  it should \"render metrics as json\" in {\n    val metricName = new MetricName(\n      \"bytes-consumed-total\",\n      \"consumer-fetch-manager-metrics\",\n      \"The total number of bytes consumed\",\n      Map(\"client-id\" -> \"event-consumer\").asJava)\n    val valueProvider: Measurable = (config: MetricConfig, now: Long) => 42\n    val metrics = new KafkaMetric(this, metricName, valueProvider, new MetricConfig(), Time.SYSTEM)\n\n    val route = KafkaMetricRoute(() => Future.successful(Map(metricName -> metrics)))\n    Get(\"/metrics/kafka\") ~> route ~> check {\n      //Due to retries using a random port does not immediately result in failure\n      handled shouldBe true\n      responseAs[JsArray].elements.size shouldBe 1\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/WhiskConfigTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core\n\nimport java.io.BufferedWriter\nimport java.io.File\nimport java.io.FileWriter\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.StreamLogging\n\n@RunWith(classOf[JUnitRunner])\nclass WhiskConfigTests extends AnyFlatSpec with Matchers with StreamLogging {\n\n  behavior of \"WhiskConfig\"\n\n  it should \"get required property\" in {\n    val config = new WhiskConfig(WhiskConfig.edgeHost)\n    assert(config.isValid)\n    assert(config.edgeHost.nonEmpty)\n  }\n\n  it should \"be valid when a prop file is provided defining required props\" in {\n    val file = File.createTempFile(\"cxt\", \".txt\")\n    file.deleteOnExit()\n\n    val bw = new BufferedWriter(new FileWriter(file))\n    bw.write(\"a=A\\n\")\n    bw.close()\n\n    val config = new WhiskConfig(Map(\"a\" -> null), Set.empty, file)\n    assert(config.isValid && config(\"a\") == \"A\")\n  }\n\n  it should \"not be valid when a prop file is provided but does not define required props\" in {\n    val file = File.createTempFile(\"cxt\", \".txt\")\n    file.deleteOnExit()\n\n    val bw = new BufferedWriter(new FileWriter(file))\n    bw.write(\"a=A\\n\")\n    bw.close()\n\n    val config = new WhiskConfig(Map(\"a\" -> null, \"b\" -> null), Set.empty, file)\n    assert(!config.isValid && config(\"b\") == null)\n  }\n\n  it should \"be valid when a prop file is provided defining required props and optional properties\" in {\n    val file = File.createTempFile(\"cxt\", \".txt\")\n    file.deleteOnExit()\n\n    val bw = new BufferedWriter(new FileWriter(file))\n    bw.write(\"a=A\\n\")\n    bw.write(\"b=B\\n\")\n    bw.write(\"c=C\\n\")\n    bw.close()\n\n    val config = new WhiskConfig(Map(\"a\" -> null, \"b\" -> \"???\"), Set(\"c\", \"d\"), file, env = Map.empty)\n    assert(config.isValid && config(\"a\") == \"A\" && config(\"b\") == \"B\")\n    assert(config(\"c\") == \"C\")\n    assert(config(\"d\") == \"\")\n    assert(config(\"a\", \"c\") == \"C\")\n    assert(config(\"a\", \"d\") == \"A\")\n    assert(config(\"d\", \"a\") == \"A\")\n    assert(config(\"c\", \"a\") == \"A\")\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/admin/WskAdminTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.admin\n\nimport common.WskAdmin.wskadmin\nimport common._\nimport common.rest.WskRestOperations\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.entity.{BasicAuthenticationAuthKey, Subject}\nimport common.TestHelpers\n\nimport scala.concurrent.duration.DurationInt\nimport scala.util.Try\n\n@RunWith(classOf[JUnitRunner])\nclass WskAdminTests extends TestHelpers with WskActorSystem with Matchers with BeforeAndAfterAll {\n\n  override def beforeAll() = {\n    val testSpaces = Seq(\"testspace\", \"testspace1\", \"testspace2\")\n    testSpaces.foreach(testspace => {\n      Try {\n        val identities = wskadmin.cli(Seq(\"user\", \"list\", \"-a\", testspace))\n        identities.stdout\n          .split(\"\\n\")\n          .foreach(ident => {\n            val sub = ident.split(\"\\\\s+\").last\n            wskadmin.cli(Seq(\"user\", \"delete\", sub, \"-ns\", testspace))\n          })\n      }\n    })\n  }\n\n  behavior of \"Wsk Admin CLI\"\n\n  it should \"confirm wskadmin exists\" in {\n    WskAdmin.exists\n  }\n\n  it should \"CRD a subject\" in {\n    val auth = BasicAuthenticationAuthKey()\n    val subject = Subject().asString\n    try {\n      println(s\"CRD subject: $subject\")\n      val create = wskadmin.cli(Seq(\"user\", \"create\", subject))\n      val get = wskadmin.cli(Seq(\"user\", \"get\", subject))\n      create.stdout should be(get.stdout)\n\n      val authkey = get.stdout.trim\n      authkey should include(\":\")\n      authkey.split(\":\")(0).length should be(36)\n      authkey.split(\":\")(1).length should be >= 64\n\n      wskadmin.cli(Seq(\"user\", \"whois\", authkey)).stdout.trim should be(\n        Seq(s\"subject: $subject\", s\"namespace: $subject\").mkString(\"\\n\"))\n\n      org.apache.openwhisk.utils.retry({\n        // reverse lookup by namespace\n        wskadmin.cli(Seq(\"user\", \"list\", \"-k\", subject)).stdout.trim should be(authkey)\n      }, 10, Some(1.second))\n\n      wskadmin.cli(Seq(\"user\", \"delete\", subject)).stdout should include(\"Subject deleted\")\n\n      // recreate with explicit\n      val newspace = s\"${subject}.myspace\"\n      wskadmin.cli(Seq(\"user\", \"create\", subject, \"-ns\", newspace, \"-u\", auth.compact))\n\n      org.apache.openwhisk.utils.retry({\n        // reverse lookup by namespace\n        wskadmin.cli(Seq(\"user\", \"list\", \"-k\", newspace)).stdout.trim should be(auth.compact)\n      }, 10, Some(1.second))\n\n      wskadmin.cli(Seq(\"user\", \"get\", subject, \"-ns\", newspace)).stdout.trim should be(auth.compact)\n\n      // delete namespace\n      wskadmin.cli(Seq(\"user\", \"delete\", subject, \"-ns\", newspace)).stdout should include(\"Namespace deleted\")\n    } finally {\n      wskadmin.cli(Seq(\"user\", \"delete\", subject)).stdout should include(\"Subject deleted\")\n    }\n  }\n\n  it should \"list all namespaces for a subject\" in {\n    val auth = BasicAuthenticationAuthKey()\n    val subject = Subject().asString\n    try {\n      println(s\"CRD subject: $subject\")\n      val first = wskadmin.cli(Seq(\"user\", \"create\", subject, \"-ns\", s\"$subject.space1\"))\n      val second = wskadmin.cli(Seq(\"user\", \"create\", subject, \"-ns\", s\"$subject.space2\"))\n      wskadmin.cli(Seq(\"user\", \"get\", subject, \"--all\")).stdout.trim should be {\n        s\"\"\"\n           |$subject.space1\\t${first.stdout.trim}\n           |$subject.space2\\t${second.stdout.trim}\n           |\"\"\".stripMargin.trim\n      }\n    } finally {\n      wskadmin.cli(Seq(\"user\", \"delete\", subject)).stdout should include(\"Subject deleted\")\n    }\n  }\n\n  it should \"verify guest account installed correctly\" in {\n    implicit val wskprops = WskProps()\n    val wsk = new WskRestOperations\n    val ns = wsk.namespace.whois()\n    wskadmin.cli(Seq(\"user\", \"get\", ns)).stdout.trim should be(wskprops.authKey)\n  }\n\n  it should \"block and unblock a user respectively\" in {\n    val auth = BasicAuthenticationAuthKey()\n    val subject1 = Subject().asString\n    val subject2 = Subject().asString\n    val commonNamespace = \"testspace\"\n    try {\n      wskadmin.cli(Seq(\"user\", \"create\", subject1, \"-ns\", commonNamespace, \"-u\", auth.compact))\n      wskadmin.cli(Seq(\"user\", \"create\", subject2, \"-ns\", commonNamespace))\n\n      org.apache.openwhisk.utils.retry({\n        // reverse lookup by namespace\n        val out = wskadmin.cli(Seq(\"user\", \"list\", \"-p\", \"2\", \"-k\", commonNamespace)).stdout.trim\n        out should include(auth.compact)\n        out.linesIterator should have size 2\n      }, 10, Some(1.second))\n\n      // block the user\n      wskadmin.cli(Seq(\"user\", \"block\", subject1))\n\n      // wait until the user can no longer be found\n      org.apache.openwhisk.utils.retry({\n        wskadmin.cli(Seq(\"user\", \"list\", \"-p\", \"2\", \"-k\", commonNamespace)).stdout.trim.linesIterator should have size 1\n      }, 10, Some(1.second))\n\n      // unblock the user\n      wskadmin.cli(Seq(\"user\", \"unblock\", subject1))\n\n      // wait until the user can be found again\n      org.apache.openwhisk.utils.retry({\n        val out = wskadmin.cli(Seq(\"user\", \"list\", \"-p\", \"2\", \"-k\", commonNamespace)).stdout.trim\n        out should include(auth.compact)\n        out.linesIterator should have size 2\n      }, 10, Some(1.second))\n    } finally {\n      wskadmin.cli(Seq(\"user\", \"delete\", subject1)).stdout should include(\"Subject deleted\")\n      wskadmin.cli(Seq(\"user\", \"delete\", subject2)).stdout should include(\"Subject deleted\")\n    }\n  }\n\n  it should \"block and unblock should accept more than a single subject\" in {\n    val subject1 = Subject().asString\n    val subject2 = Subject().asString\n    try {\n      wskadmin.cli(Seq(\"user\", \"create\", subject1))\n      wskadmin.cli(Seq(\"user\", \"create\", subject2))\n\n      // empty subjects are expected to be ignored\n      wskadmin.cli(Seq(\"user\", \"block\", subject1, subject2, \"\", \" \")).stdout shouldBe {\n        s\"\"\"|\"$subject1\" blocked successfully\n            |\"$subject2\" blocked successfully\n            |\"\"\".stripMargin\n      }\n\n      wskadmin.cli(Seq(\"user\", \"unblock\", subject1, subject2, \"\", \" \")).stdout shouldBe {\n        s\"\"\"|\"$subject1\" unblocked successfully\n            |\"$subject2\" unblocked successfully\n            |\"\"\".stripMargin\n      }\n    } finally {\n      wskadmin.cli(Seq(\"user\", \"delete\", subject1)).stdout should include(\"Subject deleted\")\n      wskadmin.cli(Seq(\"user\", \"delete\", subject2)).stdout should include(\"Subject deleted\")\n    }\n  }\n\n  it should \"not allow edits on a blocked subject\" in {\n    val subject = Subject().asString\n    try {\n      // initially create the subject\n      wskadmin.cli(Seq(\"user\", \"create\", subject))\n      // editing works\n      wskadmin.cli(Seq(\"user\", \"create\", subject, \"-ns\", \"testspace1\"))\n      // block it\n      wskadmin.cli(Seq(\"user\", \"block\", subject))\n      // Try to add a namespace, doesn't work\n      wskadmin.cli(Seq(\"user\", \"create\", subject, \"-ns\", \"testspace2\"), expectedExitCode = TestUtils.ERROR_EXIT)\n      // Unblock the user\n      wskadmin.cli(Seq(\"user\", \"unblock\", subject))\n      // Adding a namespace works\n      wskadmin.cli(Seq(\"user\", \"create\", subject, \"-ns\", \"testspace2\"))\n    } finally {\n      wskadmin.cli(Seq(\"user\", \"delete\", subject)).stdout should include(\"Subject deleted\")\n    }\n  }\n\n  it should \"adjust throttles for namespace\" in {\n    val subject = Subject().asString\n    try {\n      // set some limits\n      wskadmin.cli(\n        Seq(\n          \"limits\",\n          \"set\",\n          subject,\n          \"--invocationsPerMinute\",\n          \"1\",\n          \"--firesPerMinute\",\n          \"2\",\n          \"--concurrentInvocations\",\n          \"3\"))\n      // check correctly set\n      val lines = wskadmin.cli(Seq(\"limits\", \"get\", subject)).stdout.linesIterator.toSeq\n      lines should have size 3\n      lines(0) shouldBe \"invocationsPerMinute = 1\"\n      lines(1) shouldBe \"firesPerMinute = 2\"\n      lines(2) shouldBe \"concurrentInvocations = 3\"\n    } finally {\n      wskadmin.cli(Seq(\"limits\", \"delete\", subject)).stdout should include(\"Limits deleted\")\n    }\n  }\n\n  it should \"disable saving of activations in ActivationsStore\" in {\n    val subject = Subject().asString\n    try {\n      // set limit\n      wskadmin.cli(Seq(\"limits\", \"set\", subject, \"--storeActivations\", \"false\"))\n      // check correctly set\n      val lines = wskadmin.cli(Seq(\"limits\", \"get\", subject)).stdout.linesIterator.toSeq\n      lines should have size 1\n      lines(0) shouldBe \"storeActivations = False\"\n    } finally {\n      wskadmin.cli(Seq(\"limits\", \"delete\", subject)).stdout should include(\"Limits deleted\")\n    }\n  }\n\n  it should \"adjust whitelist for namespace\" in {\n    val subject = Subject().asString\n    try {\n      // set some limits\n      wskadmin.cli(Seq(\"limits\", \"set\", subject, \"--allowedKinds\", \"nodejs:20\", \"blackbox\"))\n      // check correctly set\n      val lines = wskadmin.cli(Seq(\"limits\", \"get\", subject)).stdout.linesIterator.toSeq\n      lines should have size 1\n      lines(0) should (be(\"allowedKinds = [u'nodejs:20', u'blackbox']\") or be(\n        \"allowedKinds = ['nodejs:20', 'blackbox']\"))\n    } finally {\n      wskadmin.cli(Seq(\"limits\", \"delete\", subject)).stdout should include(\"Limits deleted\")\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/apigw/actions/test/ApiGwRestRoutemgmtActionTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.apigw.actions.test\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.rest.WskRestOperations\n\n/**\n * Tests for basic CLI usage. Some of these tests require a deployed backend.\n */\n@RunWith(classOf[JUnitRunner])\nclass ApiGwRestRoutemgmtActionTests extends ApiGwRoutemgmtActionTests {\n  override lazy val wsk = new WskRestOperations\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.apigw.actions.test\n\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.JsHelpers\nimport common.StreamLogging\nimport common.TestHelpers\nimport common.TestUtils.DONTCARE_EXIT\nimport common.TestUtils.RunResult\nimport common.TestUtils.SUCCESS_EXIT\nimport common.WskOperations\nimport common.WskActorSystem\nimport common.WskAdmin\nimport common.WskProps\nimport common.rest.ApiAction\nimport common.WskTestHelpers\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n/**\n * Tests for basic CLI usage. Some of these tests require a deployed backend.\n */\n@RunWith(classOf[JUnitRunner])\nabstract class ApiGwRoutemgmtActionTests\n    extends TestHelpers\n    with BeforeAndAfterAll\n    with WskActorSystem\n    with WskTestHelpers\n    with JsHelpers\n    with StreamLogging {\n\n  val systemId = \"whisk.system\"\n  implicit val wskprops = WskProps(authKey = WskAdmin.listKeys(systemId)(0)._1, namespace = systemId)\n  val wsk: WskOperations\n\n  def getApis(bpOrName: Option[String],\n              relpath: Option[String] = None,\n              operation: Option[String] = None,\n              docid: Option[String] = None,\n              accesstoken: Option[String] = Some(\"AnAccessToken\"),\n              spaceguid: Option[String] = Some(\"ASpaceGuid\")): Vector[JsValue] = {\n    val parms = Map[String, JsValue]() ++\n      Map(\"__ow_user\" -> wskprops.namespace.toJson) ++ {\n      bpOrName map { b =>\n        Map(\"basepath\" -> b.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      relpath map { r =>\n        Map(\"relpath\" -> r.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      operation map { o =>\n        Map(\"operation\" -> o.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      docid map { d =>\n        Map(\"docid\" -> d.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      accesstoken map { t =>\n        Map(\"accesstoken\" -> t.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      spaceguid map { s =>\n        Map(\"spaceguid\" -> s.toJson)\n      } getOrElse Map[String, JsValue]()\n    }\n\n    val rr = wsk.action.invoke(\n      name = \"apimgmt/getApi\",\n      parameters = parms,\n      blocking = true,\n      result = true,\n      expectedExitCode = SUCCESS_EXIT)(wskprops)\n    var apiJsArray: JsArray =\n      try {\n        var apisobj = rr.stdout.parseJson.asJsObject.fields(\"apis\")\n        apisobj.convertTo[JsArray]\n      } catch {\n        case e: Exception =>\n          JsArray.empty\n      }\n    apiJsArray.elements\n  }\n\n  def createApi(namespace: Option[String] = Some(\"_\"),\n                basepath: Option[String] = Some(\"/\"),\n                relpath: Option[String],\n                operation: Option[String],\n                apiname: Option[String],\n                action: Option[ApiAction],\n                swagger: Option[String] = None,\n                accesstoken: Option[String] = Some(\"AnAccessToken\"),\n                spaceguid: Option[String] = Some(\"ASpaceGuid\"),\n                expectedExitCode: Int = SUCCESS_EXIT): RunResult = {\n    val parms = Map[String, JsValue]() ++ {\n      namespace map { n =>\n        Map(\"namespace\" -> n.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      basepath map { b =>\n        Map(\"gatewayBasePath\" -> b.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      relpath map { r =>\n        Map(\"gatewayPath\" -> r.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      operation map { o =>\n        Map(\"gatewayMethod\" -> o.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      apiname map { an =>\n        Map(\"apiName\" -> an.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      action map { a =>\n        Map(\"action\" -> a.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      swagger map { s =>\n        Map(\"swagger\" -> s.toJson)\n      } getOrElse Map[String, JsValue]()\n    }\n    val parm = Map[String, JsValue](\"apidoc\" -> JsObject(parms)) ++ {\n      namespace map { n =>\n        Map(\"__ow_user\" -> n.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      accesstoken map { t =>\n        Map(\"accesstoken\" -> t.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      spaceguid map { s =>\n        Map(\"spaceguid\" -> s.toJson)\n      } getOrElse Map[String, JsValue]()\n    }\n\n    wsk.action.invoke(\n      name = \"apimgmt/createApi\",\n      parameters = parm,\n      blocking = true,\n      result = true,\n      expectedExitCode = expectedExitCode)(wskprops)\n  }\n\n  def deleteApi(namespace: Option[String] = Some(\"_\"),\n                basepath: Option[String] = Some(\"/\"),\n                relpath: Option[String] = None,\n                operation: Option[String] = None,\n                apiname: Option[String] = None,\n                accesstoken: Option[String] = Some(\"AnAccessToken\"),\n                spaceguid: Option[String] = Some(\"ASpaceGuid\"),\n                expectedExitCode: Int = SUCCESS_EXIT): RunResult = {\n    val parms = Map[String, JsValue]() ++ {\n      namespace map { n =>\n        Map(\"__ow_user\" -> n.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      basepath map { b =>\n        Map(\"basepath\" -> b.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      relpath map { r =>\n        Map(\"relpath\" -> r.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      operation map { o =>\n        Map(\"operation\" -> o.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      apiname map { an =>\n        Map(\"apiname\" -> an.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      accesstoken map { t =>\n        Map(\"accesstoken\" -> t.toJson)\n      } getOrElse Map[String, JsValue]()\n    } ++ {\n      spaceguid map { s =>\n        Map(\"spaceguid\" -> s.toJson)\n      } getOrElse Map[String, JsValue]()\n    }\n\n    wsk.action.invoke(\n      name = \"apimgmt/deleteApi\",\n      parameters = parms,\n      blocking = true,\n      result = true,\n      expectedExitCode = expectedExitCode)(wskprops)\n  }\n\n  def apiMatch(apiarr: Vector[JsValue],\n               basepath: String = \"/\",\n               relpath: String = \"\",\n               operation: String = \"\",\n               apiname: String = \"\",\n               action: ApiAction = null): Boolean = {\n    var matches: Boolean = false\n    for (api <- apiarr) {\n      val basepathExists = JsObjectHelper(api.asJsObject).fieldPathExists(\"value\", \"apidoc\", \"basePath\")\n      if (basepathExists) {\n        System.out.println(\"basePath exists\")\n        val basepathMatches = (JsObjectHelper(api.asJsObject)\n          .getFieldPath(\"value\", \"apidoc\", \"basePath\")\n          .get\n          .convertTo[String] == basepath)\n        if (basepathMatches) {\n          System.out.println(\"basePath matches: \" + basepath)\n          val apinameExists = JsObjectHelper(api.asJsObject).fieldPathExists(\"value\", \"apidoc\", \"info\", \"title\")\n          if (apinameExists) {\n            System.out.println(\"api name exists\")\n            val apinameMatches = (JsObjectHelper(api.asJsObject)\n              .getFieldPath(\"value\", \"apidoc\", \"info\", \"title\")\n              .get\n              .convertTo[String] == apiname)\n            if (apinameMatches) {\n              System.out.println(\"api name matches: \" + apiname)\n              val endpointMatches =\n                JsObjectHelper(api.asJsObject).fieldPathExists(\"value\", \"apidoc\", \"paths\", relpath, operation)\n              if (endpointMatches) {\n                System.out.println(\"endpoint exists/matches : \" + relpath + \"  \" + operation)\n                val actionConfig = JsObjectHelper(api.asJsObject)\n                  .getFieldPath(\"value\", \"apidoc\", \"paths\", relpath, operation, \"x-openwhisk\")\n                  .get\n                  .asJsObject\n                val actionMatches = actionMatch(actionConfig, action)\n                if (actionMatches) {\n                  System.out.println(\"endpoint action matches\")\n                  matches = true;\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n\n    matches\n  }\n\n  def actionMatch(jsAction: JsObject, action: ApiAction): Boolean = {\n    System.out.println(\n      \"actionMatch: url \" + jsAction.fields(\"url\").convertTo[String] + \"; backendUrl \" + action.backendUrl)\n    System.out.println(\n      \"actionMatch: namespace \" + jsAction.fields(\"namespace\").convertTo[String] + \"; namespace \" + action.namespace)\n    System.out.println(\"actionMatch: action \" + jsAction.fields(\"action\").convertTo[String] + \"; action \" + action.name)\n\n    jsAction.fields(\"url\").convertTo[String] == action.backendUrl &&\n    jsAction.fields(\"namespace\").convertTo[String] == action.namespace &&\n    jsAction.fields(\"action\").convertTo[String] == action.name\n  }\n\n  behavior of \"API Gateway apimgmt action parameter validation\"\n\n  it should \"verify successful creation of a new API\" in {\n    val testName = \"APIGWTEST1\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    val actionNamespace = wskprops.namespace\n    val actionUrl = \"https://some.whisk.host/api/v1/web/\" + actionNamespace + \"/default/\" + actionName + \".json\"\n    val actionAuthKey = testName + \"_authkey\"\n    val testaction =\n      new ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)\n\n    try {\n      val createResult = createApi(\n        namespace = Some(wskprops.namespace),\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        apiname = Some(testapiname),\n        action = Some(testaction))\n      JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists(\"apidoc\") should be(true)\n      val apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))\n      apiVector.size should be > 0\n      apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)\n    } finally {\n      val deleteResult =\n        deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify successful API deletion using basepath\" in {\n    val testName = \"APIGWTEST2\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    val actionNamespace = wskprops.namespace\n    val actionUrl = \"https://some.whisk.host/api/v1/web/\" + actionNamespace + \"/default/\" + actionName + \".json\"\n    val actionAuthKey = testName + \"_authkey\"\n    val testaction =\n      new ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)\n\n    try {\n      val createResult = createApi(\n        namespace = Some(wskprops.namespace),\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        apiname = Some(testapiname),\n        action = Some(testaction))\n      JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists(\"apidoc\") should be(true)\n      var apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))\n      apiVector.size should be > 0\n      apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)\n      val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath))\n      apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))\n      apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(false)\n    } finally {\n      val deleteResult =\n        deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify successful addition of new relative path to existing API\" in {\n    val testName = \"APIGWTEST3\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testnewurlop = \"delete\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    val actionNamespace = wskprops.namespace\n    val actionUrl = \"https://some.whisk.host/api/v1/web/\" + actionNamespace + \"/default/\" + actionName + \".json\"\n    val actionAuthKey = testName + \"_authkey\"\n    val testaction =\n      new ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)\n\n    try {\n      var createResult = createApi(\n        namespace = Some(wskprops.namespace),\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        apiname = Some(testapiname),\n        action = Some(testaction))\n      createResult = createApi(\n        namespace = Some(wskprops.namespace),\n        basepath = Some(testbasepath),\n        relpath = Some(testnewrelpath),\n        operation = Some(testnewurlop),\n        apiname = Some(testapiname),\n        action = Some(testaction))\n      JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists(\"apidoc\") should be(true)\n      var apiVector = getApis(bpOrName = Some(testbasepath))\n      apiVector.size should be > 0\n      apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)\n      apiMatch(apiVector, testbasepath, testnewrelpath, testnewurlop, testapiname, testaction) should be(true)\n    } finally {\n      val deleteResult =\n        deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/ApiGwRestBasicTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport java.io.File\nimport java.io.BufferedWriter\nimport java.io.FileWriter\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.commons.io.FileUtils\nimport common.TestUtils._\nimport common.TestUtils\n\nimport scala.util.matching.Regex\nimport common.WskProps\n\n/**\n * Tests for testing the CLI \"api\" subcommand.  Most of these tests require a deployed backend.\n */\n@RunWith(classOf[JUnitRunner])\nabstract class ApiGwRestBasicTests extends BaseApiGwTests {\n\n  lazy val clinamespace = wsk.namespace.whois()\n  val createCode: Int\n\n  def verifyBadCommands(rr: RunResult, badpath: String): Unit = {\n    rr.stderr should include(s\"'${badpath}' must begin with '/'\")\n  }\n\n  def verifyBadCommandsDelete(rr: RunResult, badpath: String): Unit = {\n    verifyBadCommands(rr, badpath)\n  }\n\n  def verifyBadCommandsList(rr: RunResult, badpath: String): Unit = {\n    verifyBadCommands(rr, badpath)\n  }\n\n  def verifyInvalidCommands(rr: RunResult, badverb: String): Unit = {\n    rr.stderr should include(s\"'${badverb}' is not a valid API verb.  Valid values are:\")\n  }\n\n  def verifyInvalidCommandsDelete(rr: RunResult, badverb: String): Unit = {\n    verifyInvalidCommands(rr, badverb)\n  }\n\n  def verifyInvalidCommandsList(rr: RunResult, badverb: String): Unit = {\n    verifyInvalidCommands(rr, badverb)\n  }\n\n  def verifyNonJsonSwagger(rr: RunResult, filename: String): Unit = {\n    rr.stderr should include(s\"Error parsing swagger file '${filename}':\")\n  }\n\n  def verifyMissingField(rr: RunResult): Unit = {\n    rr.stderr should include(s\"Swagger file is invalid (missing basePath, info, paths, or swagger fields\")\n  }\n\n  def verifyApiCreated(rr: RunResult): Unit = {\n    rr.stdout should include(\"ok: created API\")\n  }\n\n  def verifyApiList(rr: RunResult,\n                    clinamespace: String,\n                    actionName: String,\n                    testurlop: String,\n                    testbasepath: String,\n                    testrelpath: String,\n                    testapiname: String): Unit = {\n    rr.stdout should include(\"ok: APIs\")\n    rr.stdout should include regex (s\"Action:\\\\s+/${clinamespace}/${actionName}\\n\")\n    rr.stdout should include regex (s\"Verb:\\\\s+${testurlop}\\n\")\n    rr.stdout should include regex (s\"Base path:\\\\s+${testbasepath}\\n\")\n    rr.stdout should include regex (s\"Path:\\\\s+${testrelpath}\\n\")\n    rr.stdout should include regex (s\"API Name:\\\\s+${testapiname}\\n\")\n    rr.stdout should include regex (s\"URL:\\\\s+\")\n    rr.stdout should include(testbasepath + testrelpath)\n  }\n\n  def verifyApiBaseRelPath(rr: RunResult, testbasepath: String, testrelpath: String): Unit = {\n    rr.stdout should include(testbasepath + testrelpath)\n  }\n\n  def verifyApiGet(rr: RunResult): Unit = {\n    rr.stdout should include regex (s\"\"\"\"operationId\":\\\\s*\"getPathWithSub_pathsInIt\"\"\"\")\n  }\n\n  def verifyApiFullList(rr: RunResult,\n                        clinamespace: String,\n                        actionName: String,\n                        testurlop: String,\n                        testbasepath: String,\n                        testrelpath: String,\n                        testapiname: String): Unit = {\n\n    rr.stdout should include(\"ok: APIs\")\n    if (clinamespace == \"\") {\n      rr.stdout should include regex (s\"/[@\\\\w._\\\\-]+/${actionName}\\\\s+${testurlop}\\\\s+${testapiname}\\\\s+\")\n    } else {\n      rr.stdout should include regex (s\"/${clinamespace}/${actionName}\\\\s+${testurlop}\\\\s+${testapiname}\\\\s+\")\n    }\n    rr.stdout should include(testbasepath + testrelpath)\n\n  }\n\n  def verifyApiFullListDouble(rr: RunResult,\n                              clinamespace: String,\n                              actionName: String,\n                              testurlop: String,\n                              testbasepath: String,\n                              testrelpath: String,\n                              testapiname: String,\n                              newEndpoint: String): Unit = {\n    verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n    rr.stdout should include(testbasepath + newEndpoint)\n  }\n\n  def verifyApiDeleted(rr: RunResult): Unit = {\n    rr.stdout should include(\"ok: deleted API\")\n  }\n\n  def verifyApiDeletedRelpath(rr: RunResult, testrelpath: String, testbasepath: String, op: String = \"\"): Unit = {\n    if (op != \"\")\n      rr.stdout should include(\"ok: deleted \" + testrelpath + \" \" + op.toUpperCase() + \" from \" + testbasepath)\n    else\n      rr.stdout should include(\"ok: deleted \" + testrelpath + \" from \" + testbasepath)\n  }\n\n  def verifyApiNameGet(rr: RunResult, testbasepath: String, actionName: String, responseType: String = \"json\"): Unit = {\n    rr.stdout should include(testbasepath)\n    rr.stdout should include(s\"${actionName}\")\n    rr.stdout should include regex (\"\"\"\"cors\":\\s*\\{\\s*\\n\\s*\"enabled\":\\s*true\"\"\")\n    rr.stdout should include regex (s\"\"\"\"target-url\":\\\\s+.*${actionName}.${responseType}\"\"\")\n  }\n\n  def verifyInvalidSwagger(rr: RunResult): Unit = {\n    rr.stderr should include(s\"Swagger file is invalid\")\n  }\n\n  def verifyApiOp(rr: RunResult, testurlop: String, testapiname: String): Unit = {\n    rr.stdout should include regex (s\"\\\\s+${testurlop}\\\\s+${testapiname}\\\\s+\")\n  }\n\n  def verifyApiOpVerb(rr: RunResult, testurlop: String): Unit = {\n    rr.stdout should include regex (s\"Verb:\\\\s+${testurlop}\")\n  }\n\n  def verifyInvalidKey(rr: RunResult): Unit = {\n    rr.stderr should include(\"The supplied authentication is invalid\")\n  }\n\n  def replaceStringInFile(fileName: String, replacements: Map[Regex, String]): String = {\n    val encoding = \"UTF-8\"\n\n    val contents = FileUtils.readFileToString(new File(fileName), encoding)\n    var newContents = contents\n    replacements foreach ((regex) => newContents = regex._1.replaceAllIn(newContents, regex._2))\n    val tmpFileName = fileName + \"-\" + System.currentTimeMillis() + \".tmp\"\n    val tmpFile = new File(tmpFileName)\n    if (tmpFile.exists()) {\n      FileUtils.forceDelete(tmpFile)\n    }\n    FileUtils.writeStringToFile(new File(tmpFileName), newContents, encoding)\n    tmpFileName\n  }\n\n  behavior of \"Wsk api creation with path parameters no swagger\"\n\n  it should \"fail to create an Api if the base path contains path parameters\" in {\n    val badBasePath = \"/bad/{path}/value\"\n    var rr = apiCreate(\n      basepath = Some(badBasePath),\n      relpath = Some(\"/good/path\"),\n      operation = Some(\"GET\"),\n      action = Some(\"action\"),\n      expectedExitCode = ANY_ERROR_EXIT)\n    rr.stderr should include(\n      s\"The base path (${badBasePath}) cannot have parameters. Only the relative path supports path parameters.\")\n  }\n\n  behavior of \"Wsk api creation with path parameters using swagger\"\n\n  it should \"fail to create API when swagger file contains invalid action response type\" in {\n    val file = TestUtils.getTestApiGwFilename(\"apigw_path_param_support_test_invalidActionType.json\")\n    val rr = apiCreate(swagger = Some(file), expectedExitCode = ANY_ERROR_EXIT)\n    var errMsg =\n      \"API creation failure: The action must use a response type of '.http' in order to receive the path parameters.\"\n    rr.stderr should include(errMsg)\n  }\n\n  it should \"fail to create API when swagger file contains invalid parameter names\" in {\n    var file = TestUtils.getTestApiGwFilename(\"apigw_path_param_support_test_invalidParamName1.json\")\n    var rr = apiCreate(swagger = Some(file), expectedExitCode = ANY_ERROR_EXIT)\n    var errMsg =\n      \"API creation failure: The parameter 'name' defined in path '%s' does not match any of the parameters defined for the path in the swagger file.\"\n    rr.stderr should include(errMsg.format(\"/api2/greeting2/{name}\"))\n\n    file = TestUtils.getTestApiGwFilename(\"apigw_path_param_support_test_invalidParamName2.json\")\n    rr = apiCreate(swagger = Some(file), expectedExitCode = ANY_ERROR_EXIT)\n    errMsg =\n      \"API creation failure: The parameter 'id' defined in path '%s' does not match any of the parameters defined for the path in the swagger file.\"\n    rr.stderr should include(errMsg.format(\"/api2/greeting2/{name}/{id}\"))\n  }\n\n  it should \"fail to create API when swagger file contains an invalid target-url\" in {\n    val file = TestUtils.getTestApiGwFilename(\"apigw_path_param_support_test_invalidTargetUrl.json\")\n    val rr = apiCreate(swagger = Some(file), expectedExitCode = ANY_ERROR_EXIT)\n    var errMsg = \"API creation failure: The target-url for operationId 'getApi2Greeting2Name' must \"\n    errMsg += \"end in '$(request.path)' in order for actions to receive the path parameters.\"\n    rr.stderr should include(errMsg)\n  }\n\n  behavior of \"Wsk api\"\n\n  it should \"reject an api commands with an invalid path parameter\" in {\n    val badpath = \"badpath\"\n\n    var rr = apiCreate(\n      basepath = Some(\"/basepath\"),\n      relpath = Some(badpath),\n      operation = Some(\"GET\"),\n      action = Some(\"action\"),\n      expectedExitCode = ANY_ERROR_EXIT)\n    verifyBadCommands(rr, badpath)\n\n    rr = apiDelete(\n      basepathOrApiName = \"/basepath\",\n      relpath = Some(badpath),\n      operation = Some(\"GET\"),\n      expectedExitCode = ANY_ERROR_EXIT)\n    verifyBadCommandsDelete(rr, badpath)\n\n    rr = apiList(\n      basepathOrApiName = Some(\"/basepath\"),\n      relpath = Some(badpath),\n      operation = Some(\"GET\"),\n      expectedExitCode = ANY_ERROR_EXIT)\n    verifyBadCommandsList(rr, badpath)\n  }\n\n  it should \"reject an api commands with an invalid verb parameter\" in {\n    val badverb = \"badverb\"\n\n    var rr = apiCreate(\n      basepath = Some(\"/basepath\"),\n      relpath = Some(\"/path\"),\n      operation = Some(badverb),\n      action = Some(\"action\"),\n      expectedExitCode = ANY_ERROR_EXIT)\n    verifyInvalidCommands(rr, badverb)\n\n    rr = apiDelete(\n      basepathOrApiName = \"/basepath\",\n      relpath = Some(\"/path\"),\n      operation = Some(badverb),\n      expectedExitCode = ANY_ERROR_EXIT)\n    verifyInvalidCommandsDelete(rr, badverb)\n\n    rr = apiList(\n      basepathOrApiName = Some(\"/basepath\"),\n      relpath = Some(\"/path\"),\n      operation = Some(badverb),\n      expectedExitCode = ANY_ERROR_EXIT)\n    verifyInvalidCommandsList(rr, badverb)\n  }\n\n  it should \"reject an api create command that specifies a nonexistent configuration file\" in {\n    val configfile = \"/nonexistent/file\"\n\n    val rr = apiCreate(swagger = Some(configfile), expectedExitCode = ANY_ERROR_EXIT)\n    rr.stderr should include(s\"Error reading swagger file '${configfile}'\")\n  }\n\n  it should \"reject an api create command specifying a non-JSON configuration file\" in {\n    val file = File.createTempFile(\"api.json\", \".txt\")\n    file.deleteOnExit()\n    val filename = file.getAbsolutePath()\n\n    val bw = new BufferedWriter(new FileWriter(file))\n    bw.write(\"a=A\")\n    bw.close()\n\n    val rr = apiCreate(swagger = Some(filename), expectedExitCode = ANY_ERROR_EXIT)\n    verifyNonJsonSwagger(rr, filename)\n  }\n\n  it should \"reject an api create command specifying a non-swagger JSON configuration file\" in {\n    val file = File.createTempFile(\"api.json\", \".txt\")\n    file.deleteOnExit()\n    val filename = file.getAbsolutePath()\n\n    val bw = new BufferedWriter(new FileWriter(file))\n    bw.write(\"\"\"|{\n                    |   \"swagger\": \"2.0\",\n                    |   \"info\": {\n                    |      \"title\": \"My API\",\n                    |      \"version\": \"1.0.0\"\n                    |   },\n                    |   \"BADbasePath\": \"/bp\",\n                    |   \"paths\": {\n                    |     \"/rp\": {\n                    |       \"get\":{}\n                    |     }\n                    |   }\n                    |}\"\"\".stripMargin)\n    bw.close()\n\n    val rr = apiCreate(swagger = Some(filename), expectedExitCode = ANY_ERROR_EXIT)\n    verifyMissingField(rr)\n  }\n\n  it should \"verify full list output\" in {\n    val testName = \"CLI_APIGWTEST_RO1\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiList(\n        basepathOrApiName = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        full = Some(true))\n      verifyApiList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath)\n    }\n  }\n\n  it should \"verify successful creation and deletion of a new API\" in {\n    val testName = \"CLI_APIGWTEST1\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path/with/sub_paths/in/it\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))\n      verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n      rr = apiGet(basepathOrApiName = Some(testbasepath))\n      verifyApiGet(rr)\n      val deleteresult = apiDelete(basepathOrApiName = testbasepath)\n      verifyApiDeleted(deleteresult)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify get API name \" in {\n    val testName = \"CLI_APIGWTEST3\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiGet(basepathOrApiName = Some(testapiname))\n      verifyApiNameGet(rr, testbasepath, actionName)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify delete API name \" in {\n    val testName = \"CLI_APIGWTEST4\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiDelete(basepathOrApiName = testapiname)\n      verifyApiDeleted(rr)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify delete API basepath \" in {\n    val testName = \"CLI_APIGWTEST5\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiDelete(basepathOrApiName = testbasepath)\n      verifyApiDeleted(rr)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify adding endpoints to existing api\" in {\n    val testName = \"CLI_APIGWTEST6\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path2\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    val newEndpoint = \"/newEndpoint\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(newEndpoint),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiList(basepathOrApiName = Some(testbasepath))\n      verifyApiFullListDouble(\n        rr,\n        clinamespace,\n        actionName,\n        testurlop,\n        testbasepath,\n        newEndpoint,\n        testapiname,\n        newEndpoint)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify successful creation with swagger doc as input\" in {\n    // NOTE: These values must match the swagger file contents\n    val testName = \"CLI_APIGWTEST7\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testrelpath1 = \"/pathSecure1\"\n    val testrelpath2 = \"/pathSecure2\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    val swaggerPath = TestUtils.getTestApiGwFilename(\"testswaggerdoc1\")\n    try {\n      var rr = apiCreate(swagger = Some(swaggerPath))\n      verifyApiCreated(rr)\n      rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))\n      verifyApiFullList(rr, \"\", actionName, testurlop, testbasepath, testrelpath, testapiname)\n      verifyApiFullList(rr, \"\", actionName, testurlop, testbasepath, testrelpath1, testapiname)\n      verifyApiFullList(rr, \"\", actionName, testurlop, testbasepath, testrelpath2, testapiname)\n    } finally {\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify adding endpoints to two existing apis\" in {\n    val testName = \"CLI_APIGWTEST8\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testbasepath2 = \"/\" + testName + \"_bp2\"\n    val testrelpath = \"/path2\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val testapiname2 = testName + \" API Name 2\"\n    val actionName = testName + \"_action\"\n    val newEndpoint = \"/newEndpoint\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiCreate(\n        basepath = Some(testbasepath2),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname2))\n      verifyApiCreated(rr)\n\n      // Update both APIs - each with a new endpoint\n      rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(newEndpoint),\n        operation = Some(testurlop),\n        action = Some(actionName))\n      verifyApiCreated(rr)\n      rr = apiCreate(\n        basepath = Some(testbasepath2),\n        relpath = Some(newEndpoint),\n        operation = Some(testurlop),\n        action = Some(actionName))\n      verifyApiCreated(rr)\n\n      rr = apiList(basepathOrApiName = Some(testbasepath))\n      verifyApiFullListDouble(\n        rr,\n        clinamespace,\n        actionName,\n        testurlop,\n        testbasepath,\n        testrelpath,\n        testapiname,\n        newEndpoint)\n\n      rr = apiList(basepathOrApiName = Some(testbasepath2))\n      verifyApiFullListDouble(\n        rr,\n        clinamespace,\n        actionName,\n        testurlop,\n        testbasepath2,\n        testrelpath,\n        testapiname2,\n        newEndpoint)\n\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath2, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify successful creation of a new API using an action name using all allowed characters\" in {\n    val testName = \"CLI_APIGWTEST9\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"a-c@t ion\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))\n      verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n      val deleteresult = apiDelete(basepathOrApiName = testbasepath)\n      verifyApiDeleted(deleteresult)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify failed creation with invalid swagger doc as input\" in {\n    val testName = \"CLI_APIGWTEST10\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    val swaggerPath = TestUtils.getTestApiGwFilename(s\"testswaggerdocinvalid\")\n    try {\n      val rr = apiCreate(swagger = Some(swaggerPath), expectedExitCode = ANY_ERROR_EXIT)\n      verifyInvalidSwagger(rr)\n    } finally {\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify delete basepath/path \" in {\n    val testName = \"CLI_APIGWTEST11\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      var rr2 = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testnewrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr2)\n      rr = apiDelete(basepathOrApiName = testbasepath, relpath = Some(testrelpath))\n      verifyApiDeletedRelpath(rr, testrelpath, testbasepath)\n      rr2 = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testnewrelpath))\n      verifyApiFullList(rr2, clinamespace, actionName, testurlop, testbasepath, testnewrelpath, testapiname)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify delete single operation from existing API basepath/path/operation(s) \" in {\n    val testName = \"CLI_APIGWTEST12\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path2\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testurlop2 = \"post\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop2),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiList(basepathOrApiName = Some(testbasepath))\n      verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n      verifyApiFullList(rr, clinamespace, actionName, testurlop2, testbasepath, testrelpath, testapiname)\n      rr = apiDelete(basepathOrApiName = testbasepath, relpath = Some(testrelpath), operation = Some(testurlop2))\n      verifyApiDeletedRelpath(rr, testrelpath, testbasepath, testurlop2)\n\n      rr = apiList(basepathOrApiName = Some(testbasepath))\n      verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify successful creation with complex swagger doc as input\" in {\n    val testName = \"CLI_APIGWTEST13\"\n    val testbasepath = \"/test1/v1\"\n    val testrelpath = \"/whisk_system/utils/echo\"\n    val testrelpath2 = \"/whisk_system/utils/split\"\n    val testurlop = \"get\"\n    val testurlop2 = \"post\"\n    val testapiname = testName + \" API Name\"\n    val actionName = \"test1a\"\n    val swaggerPath = TestUtils.getTestApiGwFilename(s\"testswaggerdoc2\")\n    try {\n      var rr = apiCreate(swagger = Some(swaggerPath))\n      verifyApiCreated(rr)\n      rr = apiList(basepathOrApiName = Some(testbasepath))\n      verifyApiFullList(rr, \"\", actionName, testurlop, testbasepath, testrelpath, testapiname)\n      verifyApiFullList(rr, \"\", actionName, testurlop2, testbasepath, testrelpath2, testapiname)\n    } finally {\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify successful creation and deletion with multiple base paths\" in {\n    val testName = \"CLI_APIGWTEST14\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testbasepath2 = \"/\" + testName + \"_bp2\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val testapiname2 = testName + \" API Name 2\"\n    val actionName = testName + \"_action\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n      rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))\n      verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n      rr = apiCreate(\n        basepath = Some(testbasepath2),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname2))\n      verifyApiCreated(rr)\n      rr = apiList(basepathOrApiName = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop))\n      verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath2, testrelpath, testapiname2)\n      rr = apiDelete(basepathOrApiName = testbasepath2)\n      verifyApiDeleted(rr)\n      rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))\n      verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n      rr = apiDelete(basepathOrApiName = testbasepath)\n      verifyApiDeleted(rr)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath2, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify API with http response type \" in {\n    val testName = \"CLI_APIGWTEST17\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n    val responseType = \"http\"\n    try {\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname),\n        responsetype = Some(responseType))\n      verifyApiCreated(rr)\n\n      rr = apiGet(basepathOrApiName = Some(testapiname))\n      verifyApiNameGet(rr, testbasepath, actionName, responseType)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"reject deletion of a non-existent api\" in {\n    val nonexistentApi = \"/not-there\"\n\n    val rr = apiDelete(basepathOrApiName = nonexistentApi, expectedExitCode = ANY_ERROR_EXIT)\n    rr.stderr should include(s\"API '${nonexistentApi}' does not exist\")\n  }\n\n  it should \"successfully list an API whose endpoints are not mapped to actions\" in {\n    val testName = \"CLI_APIGWTEST23\"\n    val testapiname = \"A descriptive name\"\n    val testbasepath = \"/NoActions\"\n    val testrelpath = \"/\"\n    val testops: Seq[String] = Seq(\"put\", \"delete\", \"get\", \"head\", \"options\", \"patch\", \"post\")\n    val swaggerPath = TestUtils.getTestApiGwFilename(s\"endpoints.without.action.swagger.json\")\n\n    try {\n      var rr = apiCreate(swagger = Some(swaggerPath))\n      this.verifyApiCreated(rr)\n\n      rr = apiList(basepathOrApiName = Some(testbasepath))\n      testops foreach { testurlop =>\n        verifyApiOp(rr, testurlop, testapiname)\n      }\n      verifyApiBaseRelPath(rr, testbasepath, testrelpath)\n\n      rr = apiList(basepathOrApiName = Some(testbasepath), full = Some(true))\n      testops foreach { testurlop =>\n        verifyApiOpVerb(rr, testurlop)\n      }\n      verifyApiBaseRelPath(rr, testbasepath, testrelpath)\n\n    } finally {\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"reject creation of an API with invalid auth key\" in {\n    val testName = \"CLI_APIGWTEST24\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val actionName = testName + \"_action\"\n\n    try {\n      // Create the action for the API.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some(\"true\"))\n\n      // Set an invalid auth key\n      val badWskProps = WskProps(authKey = \"bad-auth-key\")\n\n      val rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname),\n        expectedExitCode = ANY_ERROR_EXIT)(badWskProps)\n      verifyInvalidKey(rr)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n    }\n  }\n\n  it should \"verify get API name that uses custom package\" in {\n    val testName = \"CLI_APIGWTEST25\"\n    val testbasepath = \"/\" + testName + \"_bp\"\n    val testrelpath = \"/path\"\n    val testnewrelpath = \"/path_new\"\n    val testurlop = \"get\"\n    val testapiname = testName + \" API Name\"\n    val packageName = withTimestamp(\"pkg\")\n    val actionName = packageName + \"/\" + testName + \"_action\"\n    try {\n      wsk.pkg.create(packageName).stdout should include regex (s\"\"\"\"name\":\\\\s*\"$packageName\"\"\"\")\n\n      // Create the action for the API.  It must be a \"web-action\" action.\n      val file = TestUtils.getTestActionFilename(s\"echo.js\")\n      wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = 200, web = Some(\"true\"))\n\n      var rr = apiCreate(\n        basepath = Some(testbasepath),\n        relpath = Some(testrelpath),\n        operation = Some(testurlop),\n        action = Some(actionName),\n        apiname = Some(testapiname))\n      verifyApiCreated(rr)\n\n      rr = apiGet(basepathOrApiName = Some(testapiname))\n      verifyApiList(rr, clinamespace, testName + \"_action\", testurlop, testbasepath, testrelpath, testapiname)\n    } finally {\n      wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)\n      apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)\n      wsk.pkg.delete(packageName).stdout should include regex (s\"\"\"\"name\":\\\\s*\"$packageName\"\"\"\")\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/ApiGwRestTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.JsObject\nimport spray.json._\nimport common.rest.WskRestOperations\nimport common.rest.RestResult\nimport common.TestUtils.{RunResult, _}\nimport common.{TestUtils, WskActorSystem}\nimport system.rest.RestUtil\nimport java.io.File\n\n/**\n * Tests for testing the CLI \"api\" subcommand.  Most of these tests require a deployed backend.\n */\n@RunWith(classOf[JUnitRunner])\nclass ApiGwRestTests extends ApiGwRestBasicTests with RestUtil with WskActorSystem {\n  override lazy val wsk = new WskRestOperations\n  override lazy val createCode = OK.intValue\n\n  override def verifyBadCommands(rr: RunResult, badpath: String): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val error = RestResult.getField(apiResultRest.respBody, \"error\")\n    error should include(\"Error: Resource path must begin with '/'.\")\n  }\n\n  override def verifyBadCommandsDelete(rr: RunResult, badpath: String): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val error = RestResult.getField(apiResultRest.respBody, \"error\")\n    error should include(s\"API deletion failure: API '/basepath' does not exist\")\n  }\n\n  override def verifyBadCommandsList(rr: RunResult, badpath: String): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val apis = apiResultRest.getFieldListJsObject(\"apis\")\n    apis.size shouldBe 0\n  }\n\n  override def verifyInvalidCommands(rr: RunResult, badverb: String): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val error = apiResultRest.getField(\"error\")\n    error should include(s\"Error: Resource verb '${badverb}' not supported\")\n  }\n\n  override def verifyInvalidCommandsDelete(rr: RunResult, badverb: String): Unit = {\n    verifyBadCommandsDelete(rr, badverb)\n  }\n\n  override def verifyInvalidCommandsList(rr: RunResult, badverb: String): Unit = {\n    verifyBadCommandsList(rr, badverb)\n  }\n\n  override def verifyNonJsonSwagger(rr: RunResult, filename: String): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val error = apiResultRest.getField(\"error\")\n    error should include(s\"swagger field cannot be parsed. Ensure it is valid JSON\")\n  }\n\n  override def verifyMissingField(rr: RunResult): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val error = apiResultRest.getField(\"error\")\n    error should include(s\"swagger is missing the basePath field.\")\n  }\n\n  override def verifyApiCreated(rr: RunResult): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    apiResultRest.statusCode shouldBe OK\n  }\n\n  def verifyList(rr: RunResult,\n                 namespace: String,\n                 actionName: String,\n                 testurlop: String,\n                 testbasepath: String,\n                 testrelpath: String,\n                 testapiname: String,\n                 newEndpoint: String = \"\"): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject(\"apis\")(0), \"value\")\n    val apidoc = RestResult.getFieldJsObject(apiValue, \"apidoc\")\n    val basepath = RestResult.getField(apidoc, \"basePath\")\n    basepath shouldBe testbasepath\n\n    val paths = RestResult.getFieldJsObject(apidoc, \"paths\")\n    paths.fields.contains(testrelpath) shouldBe true\n\n    val info = RestResult.getFieldJsObject(apidoc, \"info\")\n    val title = RestResult.getField(info, \"title\")\n    title shouldBe testapiname\n\n    verifyPaths(paths, testrelpath, testurlop, actionName, namespace)\n\n    if (newEndpoint != \"\") {\n      verifyPaths(paths, newEndpoint, testurlop, actionName, namespace)\n    }\n  }\n\n  def verifyPaths(paths: JsObject,\n                  testrelpath: String,\n                  testurlop: String,\n                  actionName: String,\n                  namespace: String = \"\") = {\n    val relpath = RestResult.getFieldJsObject(paths, testrelpath)\n    val urlop = RestResult.getFieldJsObject(relpath, testurlop)\n    val openwhisk = RestResult.getFieldJsObject(urlop, \"x-openwhisk\")\n    val actionN = RestResult.getField(openwhisk, \"action\")\n    actionN shouldBe actionName\n\n    if (namespace != \"\") {\n      val namespaceS = RestResult.getField(openwhisk, \"namespace\")\n      namespaceS shouldBe namespace\n    }\n  }\n\n  override def verifyApiList(rr: RunResult,\n                             clinamespace: String,\n                             actionName: String,\n                             testurlop: String,\n                             testbasepath: String,\n                             testrelpath: String,\n                             testapiname: String): Unit = {\n    verifyList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n  }\n\n  override def verifyApiGet(rr: RunResult): Unit = {\n    rr.stdout should include regex (s\"\"\"\"operationId\":\\\\s*\"getPathWithSub_pathsInIt\"\"\"\")\n  }\n\n  override def verifyApiFullList(rr: RunResult,\n                                 clinamespace: String,\n                                 actionName: String,\n                                 testurlop: String,\n                                 testbasepath: String,\n                                 testrelpath: String,\n                                 testapiname: String): Unit = {\n    verifyList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname)\n  }\n\n  override def verifyApiFullListDouble(rr: RunResult,\n                                       clinamespace: String,\n                                       actionName: String,\n                                       testurlop: String,\n                                       testbasepath: String,\n                                       testrelpath: String,\n                                       testapiname: String,\n                                       newEndpoint: String): Unit = {\n    verifyList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname, newEndpoint)\n  }\n\n  override def verifyApiDeleted(rr: RunResult): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    apiResultRest.statusCode shouldBe OK\n  }\n\n  override def verifyApiDeletedRelpath(rr: RunResult,\n                                       testrelpath: String,\n                                       testbasepath: String,\n                                       op: String = \"\"): Unit = {\n    verifyApiDeleted(rr)\n  }\n\n  override def verifyApiNameGet(rr: RunResult,\n                                testbasepath: String,\n                                actionName: String,\n                                responseType: String = \"json\"): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n\n    val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject(\"apis\")(0), \"value\")\n    val apidoc = RestResult.getFieldJsObject(apiValue, \"apidoc\")\n\n    val config = RestResult.getFieldJsObject(apidoc, \"x-ibm-configuration\")\n\n    val cors = RestResult.getFieldJsObject(config, \"cors\")\n    val enabled = RestResult.getFieldJsValue(cors, \"enabled\").toString()\n    enabled shouldBe \"true\"\n\n    val basepath = RestResult.getField(apidoc, \"basePath\")\n    basepath shouldBe testbasepath\n\n    val paths = RestResult.getFieldJsObject(apidoc, \"paths\")\n    val relpath = RestResult.getFieldJsObject(paths, \"/path\")\n    val urlop = RestResult.getFieldJsObject(relpath, \"get\")\n    val openwhisk = RestResult.getFieldJsObject(urlop, \"x-openwhisk\")\n    val actionN = RestResult.getField(openwhisk, \"action\")\n    actionN shouldBe actionName\n    rr.stdout should include regex (s\"\"\"\"target-url\":\\\\s*\".*${actionName}.${responseType}.*\"\"\"\")\n  }\n\n  override def verifyInvalidSwagger(rr: RunResult): Unit = {\n    verifyMissingField(rr)\n  }\n\n  override def verifyApiOp(rr: RunResult, testurlop: String, testapiname: String): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject(\"apis\")(0), \"value\")\n    val apidoc = RestResult.getFieldJsObject(apiValue, \"apidoc\")\n    val info = RestResult.getFieldJsObject(apidoc, \"info\")\n    val title = RestResult.getField(info, \"title\")\n    title shouldBe testapiname\n    val paths = RestResult.getFieldJsObject(apidoc, \"paths\")\n    val relpath = RestResult.getFieldJsObject(paths, \"/\")\n    val urlop = RestResult.getFieldJsObject(relpath, testurlop)\n    relpath.fields.contains(testurlop) shouldBe true\n  }\n\n  override def verifyApiBaseRelPath(rr: RunResult, testbasepath: String, testrelpath: String): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject(\"apis\")(0), \"value\")\n    val apidoc = RestResult.getFieldJsObject(apiValue, \"apidoc\")\n    val basepath = RestResult.getField(apidoc, \"basePath\")\n    basepath shouldBe testbasepath\n\n    val paths = RestResult.getFieldJsObject(apidoc, \"paths\")\n    paths.fields.contains(testrelpath) shouldBe true\n  }\n\n  override def verifyApiOpVerb(rr: RunResult, testurlop: String): Unit = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject(\"apis\")(0), \"value\")\n    val apidoc = RestResult.getFieldJsObject(apiValue, \"apidoc\")\n    val paths = RestResult.getFieldJsObject(apidoc, \"paths\")\n    val relpath = RestResult.getFieldJsObject(paths, \"/\")\n    val urlop = RestResult.getFieldJsObject(relpath, testurlop)\n    relpath.fields.contains(testurlop) shouldBe true\n  }\n\n  override def verifyInvalidKey(rr: RunResult): Unit = {\n    rr.stderr should include(\"A valid auth key is required\")\n  }\n\n  def getSwaggerApiUrl(rr: RunResult): String = {\n    val apiResultRest = rr.asInstanceOf[RestResult]\n    apiResultRest.getField(\"gwApiUrl\") + \"/path\"\n  }\n\n  def getParametersFromJson(rr: RunResult, pathName: String): Vector[JsObject] = {\n    val apiResult = rr.asInstanceOf[RestResult]\n    val apidoc = apiResult.getFieldJsObject(\"apidoc\")\n    val paths = RestResult.getFieldJsObject(apidoc, \"paths\")\n    val path = RestResult.getFieldJsObject(paths, pathName)\n    val get = RestResult.getFieldJsObject(path, \"get\")\n    RestResult.getFieldListJsObject(get, \"parameters\")\n  }\n\n  behavior of \"Wsk rest api creation with path parameters with swagger\"\n\n  it should \"create the API when swagger file contains path parameters\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      println(wskprops.apihost)\n      val actionName = \"cli_apigwtest_path_param_swagger_action\"\n      var exception: Throwable = null\n      val apiName = \"/guest/v1\"\n      val reqPath = \"\\\\$\\\\(request.path\\\\)\"\n      val testRelPath = \"/api2/greeting2/{name}\"\n      val testRelPathGet = \"/api2/greeting2/name\"\n      val hostRegex = \"%HOST%\".r\n      val namespaceRegex = \"%NAMESPACE%\".r\n      var file = TestUtils.getTestActionFilename(s\"echo-web-http.js\")\n      assetHelper.withCleaner(wsk.action, actionName, confirmDelete = true) { (action, _) =>\n        action.create(actionName, Some(file), web = Some(\"true\"))\n      }\n      try {\n        val apiGwURL = \"https://\" + wskprops.apihost\n        file = TestUtils.getTestApiGwFilename(\"apigw_path_param_support_test_withPathParameters1.json\")\n        var replacements = Map(hostRegex -> apiGwURL, namespaceRegex -> \"guest\")\n        file = replaceStringInFile(file, replacements)\n        var rr = apiCreate(swagger = Some(file), expectedExitCode = SUCCESS_EXIT)\n        val apiResult = rr.asInstanceOf[RestResult]\n        val url = apiResult.getField(\"gwApiUrl\")\n        val params = getParametersFromJson(rr, testRelPath)\n        println(\"url: \" + url)\n        params.size should be(1)\n        RestResult.getField(params(0), \"name\") should be(\"name\")\n        RestResult.getField(params(0), \"in\") should be(\"path\")\n        RestResult.getFieldJsValue(params(0), \"required\").toString() should be(\"true\")\n        RestResult.getField(params(0), \"type\") should be(\"string\")\n      } catch {\n        case unknown: Throwable => exception = unknown;\n      } finally {\n        apiDelete(basepathOrApiName = apiName)\n        val f = new File(file)\n        if (f.exists()) { f.delete() }\n      }\n      assert(exception == null)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/BaseApiGwTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport java.io.File\nimport java.time.Instant\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.concurrent.duration._\nimport scala.math.max\n\nimport org.junit.runner.RunWith\n\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.TestHelpers\nimport common.TestUtils._\nimport common.WhiskProperties\nimport common.WskOperations\nimport common.WskProps\nimport common.WskTestHelpers\n\n/**\n * Tests for testing the CLI \"api\" subcommand.  Most of these tests require a deployed backend.\n */\n@RunWith(classOf[JUnitRunner])\nabstract class BaseApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach with BeforeAndAfterAll {\n\n  implicit val wskprops = WskProps()\n  val wsk: WskOperations\n\n  // This test suite makes enough CLI invocations in 60 seconds to trigger the OpenWhisk\n  // throttling restriction.  To avoid CLI failures due to being throttled, track the\n  // CLI invocation calls and when at the throttle limit, pause the next CLI invocation\n  // with exactly enough time to relax the throttling.\n  val maxActionsPerMin = WhiskProperties.getMaxActionInvokesPerMinute()\n  val invocationTimes = new ArrayBuffer[Instant]()\n\n  // Custom CLI properties file\n  val cliWskPropsFile = File.createTempFile(\"wskprops\", \".tmp\")\n\n  /**\n   * Expected to be called before each action invocation to\n   * settle the throttle when there isn't enough capacity to handle the test.\n   */\n  def checkThrottle(maxInvocationsBeforeThrottle: Int = maxActionsPerMin, throttlePercent: Int = 50) = {\n    val t = Instant.now\n    val tminus60 = t.minusSeconds(60)\n    val invocationsLast60Seconds = invocationTimes.filter(_.isAfter(tminus60)).sorted\n    val invocationCount = invocationsLast60Seconds.length\n    println(s\"Action invokes within last minute: ${invocationCount}\")\n\n    if (invocationCount >= maxInvocationsBeforeThrottle && throttlePercent >= 1) {\n      val numInvocationsToClear = max(invocationCount / (100 / throttlePercent), 1)\n      val invocationToClear = invocationsLast60Seconds(numInvocationsToClear - 1)\n      println(\n        s\"throttling ${throttlePercent}% of action invocations within last minute = ($numInvocationsToClear) invocations\")\n      val throttleTime = 60.seconds.toMillis - (t.toEpochMilli - invocationToClear.toEpochMilli)\n\n      println(s\"Waiting ${throttleTime} milliseconds to settle the throttle\")\n      Thread.sleep(throttleTime)\n    }\n\n    invocationTimes += Instant.now\n  }\n\n  override def beforeEach() = {\n    //checkThrottle()\n  }\n\n  /*\n   * Create a CLI properties file for use by the tests\n   */\n  override def beforeAll() = {\n    //Wait a while to settle the throttle so that the status of throttle algorithm implemented in checkThrottle\n    //is consistent with the reality\n    //Without this waiting time API Tests do fail very consistently on faster build machines\n    //In the longer run the APIGW tests should be moved to a separate namespace\n    Thread.sleep(60.seconds.toMillis)\n    cliWskPropsFile.deleteOnExit()\n    val wskprops = WskProps(token = \"SOME TOKEN\")\n    wskprops.writeFile(cliWskPropsFile)\n    println(s\"wsk temporary props file created here: ${cliWskPropsFile.getCanonicalPath()}\")\n  }\n\n  /*\n   * Forcibly clear the throttle so that downstream tests are not affected by\n   * this test suite\n   */\n  override def afterAll() = {\n    // Check and settle the throttle so that this test won't cause issues with any follow on tests\n    checkThrottle(maxInvocationsBeforeThrottle = 1, throttlePercent = 100)\n  }\n\n  def apiCreate(basepath: Option[String] = None,\n                relpath: Option[String] = None,\n                operation: Option[String] = None,\n                action: Option[String] = None,\n                apiname: Option[String] = None,\n                swagger: Option[String] = None,\n                responsetype: Option[String] = None,\n                expectedExitCode: Int = SUCCESS_EXIT,\n                cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath()))(\n    implicit wskpropsOverride: WskProps): RunResult = {\n\n    checkThrottle()\n    wsk.api.create(basepath, relpath, operation, action, apiname, swagger, responsetype, expectedExitCode, cliCfgFile)(\n      wskpropsOverride)\n  }\n\n  def apiList(basepathOrApiName: Option[String] = None,\n              relpath: Option[String] = None,\n              operation: Option[String] = None,\n              limit: Option[Int] = None,\n              since: Option[Instant] = None,\n              full: Option[Boolean] = None,\n              nameSort: Option[Boolean] = None,\n              expectedExitCode: Int = SUCCESS_EXIT,\n              cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath())): RunResult = {\n\n    checkThrottle()\n    wsk.api.list(basepathOrApiName, relpath, operation, limit, since, full, nameSort, expectedExitCode, cliCfgFile)\n  }\n\n  def apiGet(basepathOrApiName: Option[String] = None,\n             full: Option[Boolean] = None,\n             expectedExitCode: Int = SUCCESS_EXIT,\n             cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath()),\n             format: Option[String] = None): RunResult = {\n\n    checkThrottle()\n    wsk.api.get(basepathOrApiName, full, expectedExitCode, cliCfgFile, format)\n  }\n\n  def apiDelete(basepathOrApiName: String,\n                relpath: Option[String] = None,\n                operation: Option[String] = None,\n                expectedExitCode: Int = SUCCESS_EXIT,\n                cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath())): RunResult = {\n\n    checkThrottle()\n    wsk.api.delete(basepathOrApiName, relpath, operation, expectedExitCode, cliCfgFile)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/TestJsonArgs.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport spray.json._\n\nobject TestJsonArgs {\n\n  def getInvalidJSONInput =\n    Seq(\n      \"{\\\"invalid1\\\": }\",\n      \"{\\\"invalid2\\\": bogus}\",\n      \"{\\\"invalid1\\\": \\\"aKey\\\"\",\n      \"invalid \\\"string\\\"\",\n      \"{\\\"invalid1\\\": [1, 2, \\\"invalid\\\"\\\"arr\\\"]}\")\n\n  def getJSONFileOutput() =\n    JsArray(\n      JsObject(\"key\" -> JsString(\"a key\"), \"value\" -> JsString(\"a value\")),\n      JsObject(\"key\" -> JsString(\"a bool\"), \"value\" -> JsTrue),\n      JsObject(\"key\" -> JsString(\"objKey\"), \"value\" -> JsObject(\"b\" -> JsString(\"c\"))),\n      JsObject(\n        \"key\" -> JsString(\"objKey2\"),\n        \"value\" -> JsObject(\"another object\" -> JsObject(\"some string\" -> JsString(\"1111\")))),\n      JsObject(\n        \"key\" -> JsString(\"objKey3\"),\n        \"value\" -> JsObject(\"json object\" -> JsObject(\"some int\" -> JsNumber(1111)))),\n      JsObject(\"key\" -> JsString(\"a number arr\"), \"value\" -> JsArray(JsNumber(1), JsNumber(2), JsNumber(3))),\n      JsObject(\"key\" -> JsString(\"a string arr\"), \"value\" -> JsArray(JsString(\"1\"), JsString(\"2\"), JsString(\"3\"))),\n      JsObject(\"key\" -> JsString(\"a bool arr\"), \"value\" -> JsArray(JsTrue, JsFalse, JsTrue)),\n      JsObject(\"key\" -> JsString(\"strThatLooksLikeJSON\"), \"value\" -> JsString(\"{\\\"someKey\\\": \\\"someValue\\\"}\")))\n\n  def getEscapedJSONTestArgInput() =\n    Map(\n      \"key1\" -> JsObject(\"nonascii\" -> JsString(\"日本語\")),\n      \"key2\" -> JsObject(\"valid\" -> JsString(\"J\\\\SO\\\"N\")),\n      \"\\\"key\\\"with\\\\escapes\" -> JsObject(\"valid\" -> JsString(\"JSON\")),\n      \"another\\\"escape\\\"\" -> JsObject(\"valid\" -> JsString(\"\\\\nJ\\\\rO\\\\tS\\\\bN\\\\f\")))\n\n  def getEscapedJSONTestArgOutput() =\n    JsArray(\n      JsObject(\"key\" -> JsString(\"key1\"), \"value\" -> JsObject(\"nonascii\" -> JsString(\"日本語\"))),\n      JsObject(\"key\" -> JsString(\"key2\"), \"value\" -> JsObject(\"valid\" -> JsString(\"J\\\\SO\\\"N\"))),\n      JsObject(\"key\" -> JsString(\"\\\"key\\\"with\\\\escapes\"), \"value\" -> JsObject(\"valid\" -> JsString(\"JSON\"))),\n      JsObject(\"key\" -> JsString(\"another\\\"escape\\\"\"), \"value\" -> JsObject(\"valid\" -> JsString(\"\\\\nJ\\\\rO\\\\tS\\\\bN\\\\f\"))))\n\n  def getValidJSONTestArgOutput() =\n    JsArray(\n      JsObject(\"key\" -> JsString(\"number\"), \"value\" -> JsNumber(8)),\n      JsObject(\"key\" -> JsString(\"bignumber\"), \"value\" -> JsNumber(12345678912.123456789012)),\n      JsObject(\n        \"key\" -> JsString(\"objArr\"),\n        \"value\" -> JsArray(\n          JsObject(\"name\" -> JsString(\"someName\"), \"required\" -> JsTrue),\n          JsObject(\"name\" -> JsString(\"events\"), \"count\" -> JsNumber(10)))),\n      JsObject(\"key\" -> JsString(\"strArr\"), \"value\" -> JsArray(JsString(\"44\"), JsString(\"55\"))),\n      JsObject(\"key\" -> JsString(\"string\"), \"value\" -> JsString(\"This is a string\")),\n      JsObject(\"key\" -> JsString(\"numArr\"), \"value\" -> JsArray(JsNumber(44), JsNumber(55))),\n      JsObject(\n        \"key\" -> JsString(\"object\"),\n        \"value\" -> JsObject(\n          \"objString\" -> JsString(\"aString\"),\n          \"objStrNum\" -> JsString(\"123\"),\n          \"objNum\" -> JsNumber(300),\n          \"objBool\" -> JsFalse,\n          \"objNumArr\" -> JsArray(JsNumber(1), JsNumber(2)),\n          \"objStrArr\" -> JsArray(JsString(\"1\"), JsString(\"2\")))),\n      JsObject(\"key\" -> JsString(\"strNum\"), \"value\" -> JsString(\"9\")))\n\n  def getValidJSONTestArgInput() =\n    Map(\n      \"string\" -> JsString(\"This is a string\"),\n      \"strNum\" -> JsString(\"9\"),\n      \"number\" -> JsNumber(8),\n      \"bignumber\" -> JsNumber(12345678912.123456789012),\n      \"numArr\" -> JsArray(JsNumber(44), JsNumber(55)),\n      \"strArr\" -> JsArray(JsString(\"44\"), JsString(\"55\")),\n      \"objArr\" -> JsArray(\n        JsObject(\"name\" -> JsString(\"someName\"), \"required\" -> JsTrue),\n        JsObject(\"name\" -> JsString(\"events\"), \"count\" -> JsNumber(10))),\n      \"object\" -> JsObject(\n        \"objString\" -> JsString(\"aString\"),\n        \"objStrNum\" -> JsString(\"123\"),\n        \"objNum\" -> JsNumber(300),\n        \"objBool\" -> JsFalse,\n        \"objNumArr\" -> JsArray(JsNumber(1), JsNumber(2)),\n        \"objStrArr\" -> JsArray(JsString(\"1\"), JsString(\"2\"))))\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskActionSequenceTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.TestHelpers\nimport common.TestUtils\nimport common.WskOperations\nimport common.WskProps\nimport common.WskTestHelpers\nimport TestUtils.RunResult\nimport spray.json._\nimport org.apache.openwhisk.core.entity.EntityPath\n\n/**\n * Tests creation and retrieval of a sequence action\n */\n@RunWith(classOf[JUnitRunner])\nabstract class WskActionSequenceTests extends TestHelpers with WskTestHelpers {\n\n  implicit val wskprops = WskProps()\n  val wsk: WskOperations\n  val defaultNamespace = EntityPath.DEFAULT.asString\n  lazy val namespace = wsk.namespace.whois()\n\n  behavior of \"Wsk Action Sequence\"\n\n  it should \"create, and get an action sequence\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"actionSeq\"\n    val packageName = \"samples\"\n    val helloName = \"hello\"\n    val catName = \"cat\"\n    val fullHelloActionName = s\"/$defaultNamespace/$packageName/$helloName\"\n    val fullCatActionName = s\"/$defaultNamespace/$packageName/$catName\"\n\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, shared = Some(true))(wp)\n    }\n\n    assetHelper.withCleaner(wsk.action, fullHelloActionName) {\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n      (action, _) =>\n        action.create(fullHelloActionName, file)(wp)\n    }\n\n    assetHelper.withCleaner(wsk.action, fullCatActionName) {\n      val file = Some(TestUtils.getTestActionFilename(\"cat.js\"))\n      (action, _) =>\n        action.create(fullCatActionName, file)(wp)\n    }\n\n    val artifacts = s\"$fullHelloActionName,$fullCatActionName\"\n    val kindValue = JsString(\"sequence\")\n    val compValue = JsArray(\n      JsString(resolveDefaultNamespace(fullHelloActionName)),\n      JsString(resolveDefaultNamespace(fullCatActionName)))\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(artifacts), kind = Some(\"sequence\"))\n    }\n\n    val action = wsk.action.get(name)\n    verifyActionSequence(action, name, compValue, kindValue)\n  }\n\n  def verifyActionSequence(action: RunResult, name: String, compValue: JsArray, kindValue: JsString): Unit = {\n    val stdout = action.stdout\n    assert(stdout.startsWith(s\"ok: got action $name\\n\"))\n    wsk.parseJsonString(stdout).fields(\"exec\").asJsObject.fields(\"components\") shouldBe compValue\n    wsk.parseJsonString(stdout).fields(\"exec\").asJsObject.fields(\"kind\") shouldBe kindValue\n  }\n\n  private def resolveDefaultNamespace(actionName: String) = actionName.replace(\"/_/\", s\"/$namespace/\")\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskEntitlementTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.TestHelpers\nimport common.TestUtils\nimport common.TestUtils.RunResult\nimport common.WskOperations\nimport common.WskProps\nimport common.WskTestHelpers\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.entity.Subject\nimport org.apache.openwhisk.core.entity.WhiskPackage\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nabstract class WskEntitlementTests extends TestHelpers with WskTestHelpers with BeforeAndAfterAll {\n\n  val wsk: WskOperations\n  lazy val defaultWskProps = WskProps()\n  lazy val guestWskProps = getAdditionalTestSubject(Subject().asString)\n  val forbiddenCode: Int\n  val timeoutCode: Int\n  val notFoundCode: Int\n\n  override def afterAll() = {\n    disposeAdditionalTestSubject(guestWskProps.namespace)\n  }\n\n  def retry[A](block: => A) = org.apache.openwhisk.utils.retry(block, 10, Some(500.milliseconds))\n\n  val samplePackage = \"samplePackage\"\n  val sampleAction = \"sampleAction\"\n  val fullSampleActionName = s\"$samplePackage/$sampleAction\"\n  val guestNamespace = guestWskProps.namespace\n\n  behavior of \"Wsk Package Entitlement\"\n\n  it should \"not allow unauthorized subject to operate on private action\" in withAssetCleaner(guestWskProps) {\n    (wp, assetHelper) =>\n      val privateAction = \"privateAction\"\n\n      assetHelper.withCleaner(wsk.action, privateAction) { (action, name) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")))(wp)\n      }\n\n      val fullyQualifiedActionName = s\"/$guestNamespace/$privateAction\"\n      wsk.action\n        .get(fullyQualifiedActionName, expectedExitCode = forbiddenCode)(defaultWskProps)\n        .stderr should include(\"not authorized\")\n\n      withAssetCleaner(defaultWskProps) { (wp, assetHelper) =>\n        assetHelper.withCleaner(wsk.action, fullyQualifiedActionName, confirmDelete = false) { (action, name) =>\n          val rr = action.create(name, None, update = true, expectedExitCode = forbiddenCode)(wp)\n          rr.stderr should include(\"not authorized\")\n          rr\n        }\n\n        assetHelper.withCleaner(wsk.action, \"unauthorized sequence\", confirmDelete = false) { (action, name) =>\n          val rr = action.create(\n            name,\n            Some(fullyQualifiedActionName),\n            kind = Some(\"sequence\"),\n            update = true,\n            expectedExitCode = forbiddenCode)(wp)\n          rr.stderr should include(\"not authorized\")\n          rr\n        }\n      }\n\n      wsk.action\n        .delete(fullyQualifiedActionName, expectedExitCode = forbiddenCode)(defaultWskProps)\n        .stderr should include(\"not authorized\")\n\n      wsk.action\n        .invoke(fullyQualifiedActionName, expectedExitCode = forbiddenCode)(defaultWskProps)\n        .stderr should include(\"not authorized\")\n  }\n\n  it should \"reject deleting action in shared package not owned by authkey\" in withAssetCleaner(guestWskProps) {\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n        pkg.create(samplePackage, shared = Some(true))(wp)\n      }\n\n      assetHelper.withCleaner(wsk.action, fullSampleActionName) {\n        val file = Some(TestUtils.getTestActionFilename(\"empty.js\"))\n        (action, _) =>\n          action.create(fullSampleActionName, file)(wp)\n      }\n\n      val fullyQualifiedActionName = s\"/$guestNamespace/$fullSampleActionName\"\n      wsk.action.get(fullyQualifiedActionName)(defaultWskProps)\n      wsk.action.delete(fullyQualifiedActionName, expectedExitCode = forbiddenCode)(defaultWskProps)\n  }\n\n  it should \"reject create action in shared package not owned by authkey\" in withAssetCleaner(guestWskProps) {\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, name) =>\n        pkg.create(name, shared = Some(true))(wp)\n      }\n\n      val fullyQualifiedActionName = s\"/$guestNamespace/notallowed\"\n      val file = Some(TestUtils.getTestActionFilename(\"empty.js\"))\n\n      withAssetCleaner(defaultWskProps) { (wp, assetHelper) =>\n        assetHelper.withCleaner(wsk.action, fullyQualifiedActionName, confirmDelete = false) { (action, name) =>\n          action.create(name, file, expectedExitCode = forbiddenCode)(wp)\n        }\n      }\n  }\n\n  it should \"reject update action in shared package not owned by authkey\" in withAssetCleaner(guestWskProps) {\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n        pkg.create(samplePackage, shared = Some(true))(wp)\n      }\n\n      assetHelper.withCleaner(wsk.action, fullSampleActionName) {\n        val file = Some(TestUtils.getTestActionFilename(\"empty.js\"))\n        (action, _) =>\n          action.create(fullSampleActionName, file)(wp)\n      }\n\n      val fullyQualifiedActionName = s\"/$guestNamespace/$fullSampleActionName\"\n      wsk.action.create(fullyQualifiedActionName, None, update = true, expectedExitCode = forbiddenCode)(\n        defaultWskProps)\n  }\n\n  behavior of \"Wsk Package Listing\"\n\n  it should \"list shared packages\" in withAssetCleaner(guestWskProps) { (wp, assetHelper) =>\n    assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n      pkg.create(samplePackage, shared = Some(true))(wp)\n    }\n\n    retry {\n      val packageList = wsk.pkg.list(Some(s\"/$guestNamespace\"))(defaultWskProps)\n      verifyPackageSharedList(packageList, guestNamespace, samplePackage)\n    }\n  }\n\n  it should \"list shared packages when package is turned into public\" in withAssetCleaner(guestWskProps) {\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n        pkg.create(samplePackage)(wp)\n      }\n\n      retry {\n        val packageList = wsk.pkg.list(Some(s\"/$guestNamespace\"))(defaultWskProps)\n        verifyPackageNotSharedList(packageList, guestNamespace, samplePackage)\n      }\n\n      wsk.pkg.create(samplePackage, update = true, shared = Some(true))(wp)\n\n      retry {\n        val packageList = wsk.pkg.list(Some(s\"/$guestNamespace\"))(defaultWskProps)\n        verifyPackageSharedList(packageList, guestNamespace, samplePackage)\n      }\n  }\n\n  //TODO: convert to API-level test under whisk.core.controller once issues/3959 is resolved\n  it should \"reject getting package from invalid namespace\" in withAssetCleaner(guestWskProps) { (wp, assetHelper) =>\n    val invalidNamespace = \"whisk.systsdf\"\n    wsk.pkg.get(s\"/${invalidNamespace}/utils\", expectedExitCode = forbiddenCode)(wp).stderr should include(\n      \"not authorized\")\n  }\n\n  //TODO: convert to API-level test under whisk.core.controller once issues/3959 is resolved\n  it should \"reject getting invalid package from valid namespace\" in withAssetCleaner(guestWskProps) {\n    (wp, assetHelper) =>\n      val invalidPackage = \"utilssss\"\n      wsk.pkg.get(s\"/whisk.system/${invalidPackage}\", expectedExitCode = forbiddenCode)(wp).stderr should include(\n        \"not authorized\")\n  }\n\n  def verifyPackageSharedList(packageList: RunResult, namespace: String, packageName: String): Unit = {\n    val fullyQualifiedPackageName = s\"/$namespace/$packageName\"\n    withClue(s\"Packagelist is: ${packageList.stdout}; Packagename is: $fullyQualifiedPackageName\")(\n      packageList.stdout should include regex (fullyQualifiedPackageName + \"\"\"\\s+shared\"\"\"))\n  }\n\n  it should \"not list private packages\" in withAssetCleaner(guestWskProps) { (wp, assetHelper) =>\n    assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n      pkg.create(samplePackage)(wp)\n    }\n\n    retry {\n      val packageList = wsk.pkg.list(Some(s\"/$guestNamespace\"))(defaultWskProps)\n      verifyPackageNotSharedList(packageList, guestNamespace, samplePackage)\n    }\n  }\n\n  def verifyPackageNotSharedList(packageList: RunResult, namespace: String, packageName: String): Unit = {\n    val fullyQualifiedPackageName = s\"/$namespace/$packageName\"\n    withClue(s\"Packagelist is: ${packageList.stdout}; Packagename is: $fullyQualifiedPackageName\")(\n      packageList.stdout should not include (fullyQualifiedPackageName))\n  }\n\n  it should \"list shared package actions\" in withAssetCleaner(guestWskProps) { (wp, assetHelper) =>\n    assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n      pkg.create(samplePackage, shared = Some(true))(wp)\n    }\n\n    assetHelper.withCleaner(wsk.action, fullSampleActionName) {\n      val file = Some(TestUtils.getTestActionFilename(\"empty.js\"))\n      (action, _) =>\n        action.create(fullSampleActionName, file, kind = Some(\"nodejs:default\"))(wp)\n    }\n\n    val fullyQualifiedPackageName = s\"/$guestNamespace/$samplePackage\"\n    retry {\n      val packageList = wsk.action.list(Some(fullyQualifiedPackageName))(defaultWskProps)\n      verifyPackageList(packageList, guestNamespace, samplePackage, sampleAction)\n    }\n  }\n\n  def verifyPackageList(packageList: RunResult, namespace: String, packageName: String, actionName: String): Unit = {\n    val result = packageList.stdout\n    result should include(s\"/$namespace/$packageName/$actionName\")\n  }\n\n  behavior of \"Wsk Package Binding\"\n\n  it should \"create a package binding\" in withAssetCleaner(guestWskProps) { (wp, assetHelper) =>\n    assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n      pkg.create(samplePackage, shared = Some(true))(wp)\n    }\n\n    val name = \"bindPackage\"\n    val annotations = Map(\"a\" -> \"A\".toJson, WhiskPackage.bindingFieldName -> \"xxx\".toJson)\n    val provider = s\"/$guestNamespace/$samplePackage\"\n    withAssetCleaner(defaultWskProps) { (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n        pkg.bind(provider, name, annotations = annotations)(wp)\n      }\n\n      val stdout = wsk.pkg.get(name)(defaultWskProps).stdout\n      val annotationString = wsk.parseJsonString(stdout).fields(\"annotations\").toString\n      annotationString should include(\"\"\"\"key\":\"a\"\"\"\")\n      annotationString should include(\"\"\"\"value\":\"A\"\"\"\")\n      annotationString should include(s\"\"\"\"key\":\"${WhiskPackage.bindingFieldName}\"\"\"\")\n      annotationString should not include (\"\"\"\"key\":\"xxx\"\"\"\")\n      annotationString should include(s\"\"\"\"name\":\"${samplePackage}\"\"\"\")\n    }\n  }\n\n  it should \"not create a package binding for private package\" in withAssetCleaner(guestWskProps) { (wp, assetHelper) =>\n    assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n      pkg.create(samplePackage, shared = Some(false))(wp)\n    }\n\n    val name = \"bindPackage\"\n    val provider = s\"/$guestNamespace/$samplePackage\"\n    withAssetCleaner(defaultWskProps) { (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.pkg, name, confirmDelete = false) { (pkg, _) =>\n        pkg.bind(provider, name, expectedExitCode = forbiddenCode)(wp)\n      }\n    }\n  }\n\n  behavior of \"Wsk Package Action\"\n\n  it should \"get and invoke an action from package\" in withAssetCleaner(guestWskProps) { (wp, assetHelper) =>\n    assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n      pkg.create(samplePackage, parameters = Map(\"a\" -> \"A\".toJson), shared = Some(true))(wp)\n    }\n\n    assetHelper.withCleaner(wsk.action, fullSampleActionName) {\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n      (action, _) =>\n        action.create(fullSampleActionName, file)(wp)\n    }\n\n    val fullyQualifiedActionName = s\"/$guestNamespace/$fullSampleActionName\"\n    val action = wsk.action.get(fullyQualifiedActionName)(defaultWskProps)\n    verifyAction(action)\n\n    val run = wsk.action.invoke(fullyQualifiedActionName)(defaultWskProps)\n\n    withActivation(wsk.activation, run)({\n      _.response.success shouldBe true\n    })(defaultWskProps)\n  }\n\n  def verifyAction(action: RunResult) = {\n    val stdout = action.stdout\n    stdout should include(\"name\")\n    stdout should include(\"parameters\")\n    stdout should include(\"limits\")\n    stdout should include(\"\"\"\"key\": \"a\"\"\"\")\n    stdout should include(\"\"\"\"value\": \"A\"\"\"\")\n  }\n\n  it should \"invoke an action sequence from package\" in withAssetCleaner(guestWskProps) { (wp, assetHelper) =>\n    assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n      pkg.create(samplePackage, parameters = Map(\"a\" -> \"A\".toJson), shared = Some(true))(wp)\n    }\n\n    assetHelper.withCleaner(wsk.action, fullSampleActionName) {\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n      (action, _) =>\n        action.create(fullSampleActionName, file)(wp)\n    }\n\n    withAssetCleaner(defaultWskProps) { (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, \"sequence\") { (action, name) =>\n        val fullyQualifiedActionName = s\"/$guestNamespace/$fullSampleActionName\"\n        action.create(name, Some(fullyQualifiedActionName), kind = Some(\"sequence\"), update = true)(wp)\n      }\n\n      val run = wsk.action.invoke(\"sequence\")(defaultWskProps)\n      withActivation(wsk.activation, run)({\n        _.response.success shouldBe true\n      })(defaultWskProps)\n    }\n  }\n\n  it should \"not allow invoke an action sequence with more than one component from package after entitlement change\" in withAssetCleaner(\n    guestWskProps) { (guestwp, assetHelper) =>\n    val privateSamplePackage = samplePackage + \"prv\"\n    Seq(samplePackage, privateSamplePackage).foreach { n =>\n      assetHelper.withCleaner(wsk.pkg, n) { (pkg, _) =>\n        pkg.create(n, parameters = Map(\"a\" -> \"A\".toJson), shared = Some(true))(guestwp)\n      }\n    }\n\n    Seq(fullSampleActionName, s\"$privateSamplePackage/$sampleAction\").foreach { a =>\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n      assetHelper.withCleaner(wsk.action, a) { (action, _) =>\n        action.create(a, file)(guestwp)\n      }\n    }\n\n    withAssetCleaner(defaultWskProps) { (dwp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, \"sequence\") { (action, name) =>\n        val fullyQualifiedActionName = s\"/$guestNamespace/$fullSampleActionName\"\n        val fullyQualifiedActionName2 = s\"/$guestNamespace/$privateSamplePackage/$sampleAction\"\n        action.create(name, Some(s\"$fullyQualifiedActionName,$fullyQualifiedActionName2\"), kind = Some(\"sequence\"))(dwp)\n      }\n\n      // change package visibility\n      wsk.pkg.create(privateSamplePackage, update = true, shared = Some(false))(guestwp)\n      wsk.action.invoke(\"sequence\", expectedExitCode = forbiddenCode)(defaultWskProps)\n    }\n  }\n\n  it should \"invoke a packaged action not owned by the subject to get the subject's namespace\" in withAssetCleaner(\n    guestWskProps) { (_, assetHelper) =>\n    val packageName = \"namespacePackage\"\n    val actionName = \"namespaceAction\"\n    val packagedActionName = s\"$packageName/$actionName\"\n\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, shared = Some(true))(guestWskProps)\n    }\n\n    assetHelper.withCleaner(wsk.action, packagedActionName) {\n      val file = Some(TestUtils.getTestActionFilename(\"helloContext.js\"))\n      (action, _) =>\n        action.create(packagedActionName, file)(guestWskProps)\n    }\n\n    val fullyQualifiedActionName = s\"/$guestNamespace/$packagedActionName\"\n    val run = wsk.action.invoke(fullyQualifiedActionName)(defaultWskProps)\n\n    withActivation(wsk.activation, run)({ activation =>\n      val namespace = wsk.namespace.whois()(defaultWskProps)\n      activation.response.success shouldBe true\n      activation.response.result.get.toString should include regex (s\"\"\"\"namespace\":\\\\s*\"$namespace\"\"\"\")\n    })(defaultWskProps)\n  }\n\n  behavior of \"Wsk Trigger Feed\"\n\n  it should \"not create a trigger with timeout error when feed fails to initialize\" in withAssetCleaner(guestWskProps) {\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) =>\n        pkg.create(samplePackage, shared = Some(true))(wp)\n      }\n\n      val sampleFeed = s\"$samplePackage/sampleFeed\"\n      assetHelper.withCleaner(wsk.action, sampleFeed) {\n        val file = Some(TestUtils.getTestActionFilename(\"empty.js\"))\n        (action, _) =>\n          action.create(sampleFeed, file, kind = Some(\"nodejs:default\"))(wp)\n      }\n\n      val fullyQualifiedFeedName = s\"/$guestNamespace/$sampleFeed\"\n      withAssetCleaner(defaultWskProps) { (wp, assetHelper) =>\n        assetHelper.withCleaner(wsk.trigger, \"badfeed\", confirmDelete = false) { (trigger, name) =>\n          trigger.create(name, feed = Some(fullyQualifiedFeedName), expectedExitCode = timeoutCode)(wp)\n        }\n        // with several active controllers race condition with cache invalidation might occur, thus retry\n        retry(wsk.trigger.get(\"badfeed\", expectedExitCode = notFoundCode)(wp))\n      }\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskRestActionSequenceTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json._\nimport common.rest.WskRestOperations\nimport common.rest.RestResult\nimport common.TestUtils.RunResult\nimport common.WskActorSystem\n\n@RunWith(classOf[JUnitRunner])\nclass WskRestActionSequenceTests extends WskActionSequenceTests with WskActorSystem {\n  override lazy val wsk = new WskRestOperations\n\n  override def verifyActionSequence(action: RunResult, name: String, compValue: JsArray, kindValue: JsString): Unit = {\n    val actionResultRest = action.asInstanceOf[RestResult]\n    actionResultRest.respBody.fields(\"exec\").asJsObject.fields(\"components\") shouldBe compValue\n    actionResultRest.respBody.fields(\"exec\").asJsObject.fields(\"kind\") shouldBe kindValue\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskRestBasicUsageTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.BadRequest\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Conflict\n\nimport java.time.Instant\nimport java.time.Clock\nimport scala.language.postfixOps\nimport scala.concurrent.duration.DurationInt\nimport scala.util.Random\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common.TestHelpers\nimport common.TestUtils\nimport common.TestUtils._\nimport common.WhiskProperties\nimport common.WskProps\nimport common.WskTestHelpers\nimport common.WskActorSystem\nimport common.rest.{RestResult, RunRestCmd, WskRestOperations}\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport TestJsonArgs._\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.POST\nimport org.apache.pekko.http.scaladsl.model.{\n  ContentTypes,\n  HttpEntity,\n  HttpHeader,\n  HttpMethod,\n  HttpRequest,\n  HttpResponse,\n  Uri\n}\nimport org.apache.pekko.http.scaladsl.model.Uri.{Path, Query}\nimport org.apache.pekko.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials, RawHeader}\nimport org.apache.pekko.util.ByteString\nimport org.apache.openwhisk.http.Messages\n\n/**\n * Tests for basic CLI usage. Some of these tests require a deployed backend.\n */\n@RunWith(classOf[JUnitRunner])\nclass WskRestBasicUsageTests extends TestHelpers with WskTestHelpers with WskActorSystem with RunRestCmd {\n\n  implicit val wskprops = WskProps()\n  implicit lazy override val executionContext = actorSystem.dispatcher\n  val wsk = new WskRestOperations()\n  val defaultAction: Some[String] = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n  val usrAgentHeaderRegEx: String = \"\"\"\\bUser-Agent\\b\": \\[\\s+\"OpenWhisk\\-CLI/1.\\d+.*\"\"\"\n\n  val requireAPIKeyAnnotation = WhiskProperties.getBooleanProperty(\"whisk.feature.requireApiKeyAnnotation\", true);\n\n  behavior of \"Wsk API basic usage\"\n\n  it should \"allow a 3 part Fully Qualified Name (FQN) without a leading '/'\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val guestNamespace = wsk.namespace.whois()\n      val packageName = \"packageName3ptFQN\"\n      val actionName = \"actionName3ptFQN\"\n      val triggerName = \"triggerName3ptFQN\"\n      val ruleName = \"ruleName3ptFQN\"\n      val fullQualifiedName = s\"${guestNamespace}/${packageName}/${actionName}\"\n      // Used for action and rule creation below\n      assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n        pkg.create(packageName)\n      }\n      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>\n        trigger.create(triggerName)\n      }\n      // Test action and rule creation where action name is 3 part FQN w/out leading slash\n      assetHelper.withCleaner(wsk.action, fullQualifiedName) { (action, _) =>\n        action.create(fullQualifiedName, defaultAction)\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, _) =>\n        rule.create(ruleName, trigger = triggerName, action = fullQualifiedName)\n      }\n\n      val run = wsk.action.invoke(fullQualifiedName)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n      }\n      val action = wsk.action.get(fullQualifiedName)\n      action.getField(\"name\") shouldBe actionName\n      action.getField(\"namespace\") shouldBe s\"${guestNamespace}/${packageName}\"\n  }\n\n  behavior of \"Wsk actions\"\n\n  it should \"reject creating entities with invalid names\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val names = Seq(\n      (\"\", NotFound.intValue),\n      (\" \", BadRequest.intValue),\n      (\"hi+there\", BadRequest.intValue),\n      (\"$hola\", BadRequest.intValue),\n      (\"dora?\", BadRequest.intValue),\n      (\"|dora|dora?\", BadRequest.intValue))\n\n    names foreach {\n      case (name, ec) =>\n        assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) =>\n          action.create(name, defaultAction, expectedExitCode = ec)\n        }\n    }\n  }\n\n  it should \"create, and get an action to verify parameter and annotation parsing\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"actionAnnotations\"\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)\n      }\n\n      val action = wsk.action.get(name)\n      action.getField(\"name\") shouldBe name\n\n      val receivedParams = wsk.parseJsonString(action.stdout).fields(\"parameters\").convertTo[JsArray].elements\n      val receivedAnnots = wsk.parseJsonString(action.stdout).fields(\"annotations\").convertTo[JsArray].elements\n      val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements\n\n      for (expectedItem <- escapedJSONArr) {\n        receivedParams should contain(expectedItem)\n        receivedAnnots should contain(expectedItem)\n      }\n  }\n\n  it should \"delete the given annotations using delAnnotations\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"hello\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      val annotations = Map(\"key1\" -> \"value1\".toJson, \"key2\" -> \"value2\".toJson)\n      action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")), annotations = annotations)\n      val annotationString = wsk.parseJsonString(wsk.action.get(name).stdout).fields(\"annotations\").toString\n\n      annotationString should include(\"\"\"\"key\":\"key1\"\"\"\")\n      annotationString should include(\"\"\"\"value\":\"value1\"\"\"\")\n      annotationString should include(\"\"\"\"key\":\"key2\"\"\"\")\n      annotationString should include(\"\"\"\"value\":\"value2\"\"\"\")\n\n      //Delete key1 only\n      val delAnnotations = Array(\"key1\")\n\n      action.create(\n        name,\n        Some(TestUtils.getTestActionFilename(\"hello.js\")),\n        delAnnotations = delAnnotations,\n        update = true)\n      val newAnnotationString = wsk.parseJsonString(wsk.action.get(name).stdout).fields(\"annotations\").toString\n\n      newAnnotationString should not include (\"\"\"\"key\":\"key1\"\"\"\")\n      newAnnotationString should not include (\"\"\"\"value\":\"value1\"\"\"\")\n      newAnnotationString should include(\"\"\"\"key\":\"key2\"\"\"\")\n      newAnnotationString should include(\"\"\"\"value\":\"value2\"\"\"\")\n\n      action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")), update = true)\n    }\n  }\n\n  it should \"create, and get an action to verify file parameter and annotation parsing\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"actionAnnotAndParamParsing\"\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n      val argInput = Some(TestUtils.getTestActionFilename(\"validInput1.json\"))\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, annotationFile = argInput, parameterFile = argInput)\n      }\n\n      val action = wsk.action.get(name)\n      action.getField(\"name\") shouldBe name\n\n      val receivedParams = wsk.parseJsonString(action.stdout).fields(\"parameters\").convertTo[JsArray].elements\n      val receivedAnnots = wsk.parseJsonString(action.stdout).fields(\"annotations\").convertTo[JsArray].elements\n      val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements\n\n      for (expectedItem <- escapedJSONArr) {\n        receivedParams should contain(expectedItem)\n        receivedAnnots should contain(expectedItem)\n      }\n  }\n\n  it should \"create an action with the proper parameter and annotation escapes\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"actionEscapes\"\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, parameters = getEscapedJSONTestArgInput, annotations = getEscapedJSONTestArgInput)\n      }\n\n      val action = wsk.action.get(name)\n      action.getField(\"name\") shouldBe name\n\n      val receivedParams = wsk.parseJsonString(action.stdout).fields(\"parameters\").convertTo[JsArray].elements\n      val receivedAnnots = wsk.parseJsonString(action.stdout).fields(\"annotations\").convertTo[JsArray].elements\n      val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements\n\n      for (expectedItem <- escapedJSONArr) {\n        receivedParams should contain(expectedItem)\n        receivedAnnots should contain(expectedItem)\n      }\n  }\n\n  it should \"invoke an action that exits during initialization and get appropriate error\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"abort init\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"initexit.js\")))\n      }\n\n      withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>\n        val response = activation.response\n        response.result.get.asJsObject.fields(\"error\") shouldBe Messages.abnormalInitialization.toJson\n        response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n      }\n  }\n\n  it should \"invoke an action that hangs during initialization and get appropriate error\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"hang init\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"initforever.js\")), timeout = Some(3 seconds))\n      }\n\n      withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>\n        val response = activation.response\n        response.result.get.asJsObject.fields(\"error\") shouldBe Messages.timedoutActivation(3 seconds, true).toJson\n        response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n      }\n  }\n\n  it should \"invoke an action that exits during run and get appropriate error\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"abort run\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"runexit.js\")))\n      }\n\n      withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>\n        val response = activation.response\n        response.result.get.asJsObject.fields(\"error\") shouldBe Messages.abnormalRun.toJson\n        response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n      }\n  }\n\n  it should \"ensure keys are not omitted from activation record\" in withAssetCleaner(wskprops) {\n    val name = \"activationRecordTest\"\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"argCheck.js\")))\n      }\n\n      val run = wsk.action.invoke(name)\n      withActivation(wsk.activation, run) { activation =>\n        activation.start should be > Instant.EPOCH\n        activation.end should be > Instant.EPOCH\n        activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.Success)\n        activation.response.success shouldBe true\n        activation.response.result shouldBe Some(JsObject.empty)\n        activation.logs shouldBe Some(List.empty)\n        activation.annotations shouldBe defined\n      }\n  }\n\n  it should \"write the action-path and the limits to the annotations\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"annotations\"\n      val memoryLimit = 512 MB\n      val logLimit = 1 MB\n      val timeLimit = 60 seconds\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(\n          name,\n          Some(TestUtils.getTestActionFilename(\"helloAsync.js\")),\n          memory = Some(memoryLimit),\n          timeout = Some(timeLimit),\n          logsize = Some(logLimit))\n      }\n\n      val run = wsk.action.invoke(name, Map(\"payload\" -> \"this is a test\".toJson))\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n        val annotations = activation.annotations.get\n\n        val limitsObj = JsObject(\n          \"key\" -> JsString(\"limits\"),\n          \"value\" -> ActionLimits(TimeLimit(timeLimit), MemoryLimit(memoryLimit), LogLimit(logLimit)).toJson)\n\n        val path = annotations.find {\n          _.fields(\"key\").convertTo[String] == \"path\"\n        }.get\n\n        path.fields(\"value\").convertTo[String] should fullyMatch regex (s\"\"\".*/$name\"\"\")\n        annotations should contain(limitsObj)\n      }\n  }\n\n  it should \"create, and invoke an action that utilizes an invalid docker container with appropriate error\" in withAssetCleaner(\n    wskprops) {\n    val name = \"invalidDockerContainer\"\n    val containerName = s\"bogus${Random.alphanumeric.take(16).mkString.toLowerCase}\"\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, name) {\n        // docker name is a randomly generate string\n        (action, _) =>\n          action.create(name, None, docker = Some(containerName))\n      }\n\n      val run = wsk.action.invoke(name)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n        activation.response.result.get.asJsObject\n          .fields(\"error\") shouldBe s\"Failed to pull container image '$containerName'.\".toJson\n        activation.annotations shouldBe defined\n        val limits = activation.annotations.get.filter(_.fields(\"key\").convertTo[String] == \"limits\")\n        withClue(limits) {\n          limits.length should be > 0\n          limits(0).fields(\"value\") should not be JsNull\n        }\n      }\n  }\n\n  it should \"invoke an action using npm openwhisk\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"hello npm openwhisk\"\n    assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) =>\n      action.create(\n        name,\n        Some(TestUtils.getTestActionFilename(\"helloOpenwhiskPackage.js\")),\n        annotations = Map(Annotations.ProvideApiKeyAnnotationName -> JsTrue))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"ignore_certs\" -> true.toJson, \"name\" -> name.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"delete\" -> true.toJson))\n      activation.logs.get.mkString(\" \") should include(\"action list has this many actions\")\n    }\n\n    wsk.action.delete(name, expectedExitCode = NotFound.intValue)\n  }\n\n  it should \"invoke an action receiving context properties excluding api key\" in withAssetCleaner(wskprops) {\n    assume(requireAPIKeyAnnotation)\n    (wp, assetHelper) =>\n      val namespace = wsk.namespace.whois()\n      val name = \"context\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"helloContext.js\")))\n      }\n\n      val start = Instant.now(Clock.systemUTC()).toEpochMilli\n      val run = wsk.action.invoke(name)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n        val fields = activation.response.result.get.convertTo[Map[String, String]]\n        fields(\"api_host\") shouldBe WhiskProperties.getApiHostForAction\n        fields.get(\"api_key\") shouldBe empty\n        fields(\"namespace\") shouldBe namespace\n        fields(\"action_name\") shouldBe s\"/$namespace/$name\"\n        fields(\"action_version\") should fullyMatch regex (\"\"\"\\d+.\\d+.\\d+\"\"\")\n        fields(\"activation_id\") shouldBe activation.activationId\n        fields(\"deadline\").toLong should be >= start\n      }\n  }\n\n  it should \"invoke an action receiving context properties including api key\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val namespace = wsk.namespace.whois()\n      val name = \"context\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(\n          name,\n          Some(TestUtils.getTestActionFilename(\"helloContext.js\")),\n          annotations = Map(Annotations.ProvideApiKeyAnnotationName -> JsTrue))\n      }\n\n      val start = Instant.now(Clock.systemUTC()).toEpochMilli\n      val run = wsk.action.invoke(name)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n        val fields = activation.response.result.get.convertTo[Map[String, String]]\n        fields(\"api_host\") shouldBe WhiskProperties.getApiHostForAction\n        fields(\"api_key\") shouldBe wskprops.authKey\n        fields(\"namespace\") shouldBe namespace\n        fields(\"action_name\") shouldBe s\"/$namespace/$name\"\n        fields(\"action_version\") should fullyMatch regex (\"\"\"\\d+.\\d+.\\d+\"\"\")\n        fields(\"activation_id\") shouldBe activation.activationId\n        fields(\"deadline\").toLong should be >= start\n      }\n  }\n\n  it should \"invoke an action successfully with options --blocking and --result\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"invokeResult\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"echo.js\")))\n      }\n      val args = Map(\"hello\" -> \"Robert\".toJson)\n      val run = wsk.action.invoke(name, args, blocking = true, result = true)\n      //--result takes precedence over --blocking\n      run.stdout.parseJson shouldBe args.toJson\n  }\n\n  it should \"invoke an action that returns a result by the deadline\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"deadline\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"helloDeadline.js\")), timeout = Some(3 seconds))\n      }\n\n      val run = wsk.action.invoke(name)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n        activation.response.result shouldBe Some(JsObject(\"timedout\" -> true.toJson))\n      }\n  }\n\n  it should \"invoke an action twice, where the first times out but the second does not and should succeed\" in withAssetCleaner(\n    wskprops) {\n    // this test issues two activations: the first is forced to time out and not return a result by its deadline (ie it does not resolve\n    // its promise). The invoker should reclaim its container so that a second activation of the same action (which must happen within a\n    // short period of time (seconds, not minutes) is allocated a fresh container and hence runs as expected (vs. hitting in the container\n    // cache and reusing a bad container).\n    (wp, assetHelper) =>\n      val name = \"timeout\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"helloDeadline.js\")), timeout = Some(3 seconds))\n      }\n\n      val start = Instant.now(Clock.systemUTC()).toEpochMilli\n      val hungRun = wsk.action.invoke(name, Map(\"forceHang\" -> true.toJson))\n      withActivation(wsk.activation, hungRun) { activation =>\n        // the first action must fail with a timeout error\n        activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n        activation.response.result shouldBe Some(\n          JsObject(\"error\" -> Messages.timedoutActivation(3 seconds, false).toJson))\n      }\n\n      // run the action again, this time without forcing it to timeout\n      // it should succeed because it ran in a fresh container\n      val goodRun = wsk.action.invoke(name, Map(\"forceHang\" -> false.toJson))\n      withActivation(wsk.activation, goodRun) { activation =>\n        // the first action must fail with a timeout error\n        activation.response.status shouldBe \"success\"\n        activation.response.result shouldBe Some(JsObject(\"timedout\" -> true.toJson))\n      }\n  }\n\n  it should \"ensure --web flags set the proper annotations\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    Seq(\"true\", \"faLse\", \"tRue\", \"nO\", \"yEs\", \"no\", \"raw\", \"NO\", \"Raw\").foreach { flag =>\n      val webEnabled = flag.toLowerCase == \"true\" || flag.toLowerCase == \"yes\"\n      val rawEnabled = flag.toLowerCase == \"raw\"\n\n      val runtime = \"nodejs:default\"\n      val name = \"webaction-\" + flag\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(\n          name,\n          Some(TestUtils.getTestActionFilename(\"echo.js\")),\n          web = Some(flag.toLowerCase),\n          kind = Some(runtime))\n      }\n\n      val action = wsk.action.get(name)\n\n      // first check if we got 'nodejs:*' in the exec value\n      action\n        .getFieldJsValue(\"annotations\")\n        .convertTo[Seq[JsObject]]\n        .find(_.fields(\"key\").convertTo[String] == \"exec\")\n        .map(_.fields(\"value\"))\n        .map(exec => { exec.convertTo[String] should startWith(\"nodejs:\") })\n        .getOrElse(fail())\n\n      // then we check the remaining annotations\n      val baseAnnotations = Parameters(\"web-export\", JsBoolean(webEnabled || rawEnabled)) ++\n        Parameters(\"raw-http\", JsBoolean(rawEnabled)) ++\n        Parameters(\"final\", JsBoolean(webEnabled || rawEnabled))\n      val testAnnotations = if (requireAPIKeyAnnotation) {\n        baseAnnotations ++ Parameters(Annotations.ProvideApiKeyAnnotationName, JsFalse)\n      } else baseAnnotations\n\n      // we ignore the exec field here, since we already compared it above\n      action\n        .getFieldJsValue(\"annotations\")\n        .convertTo[Set[JsObject]]\n        .filter(annotation => annotation.fields(\"key\").convertTo[String] != \"exec\") shouldBe testAnnotations.toJsArray\n        .convertTo[Set[JsObject]]\n    }\n  }\n\n  it should \"ensure action update creates an action with --web flag\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val runtime = \"nodejs:default\"\n      val name = \"webaction\"\n      val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, web = Some(\"true\"), update = true, kind = Some(runtime))\n      }\n\n      val baseAnnotations =\n        Parameters(\"web-export\", JsTrue) ++\n          Parameters(\"raw-http\", JsFalse) ++\n          Parameters(\"final\", JsTrue)\n\n      val testAnnotations = if (requireAPIKeyAnnotation) {\n        baseAnnotations ++\n          Parameters(Annotations.ProvideApiKeyAnnotationName, JsFalse)\n      } else {\n        baseAnnotations\n      }\n\n      val action = wsk.action.get(name)\n\n      // first check if we got 'nodejs:*' in the exec value\n      action\n        .getFieldJsValue(\"annotations\")\n        .convertTo[Seq[JsObject]]\n        .find(_.fields(\"key\").convertTo[String] == \"exec\")\n        .map(_.fields(\"value\"))\n        .map(exec => { exec.convertTo[String] should startWith(\"nodejs:\") })\n        .getOrElse(fail())\n\n      // then we check the remaining annotations\n      // we ignore the exec field here, since we already compared it above\n      action\n        .getFieldJsValue(\"annotations\")\n        .convertTo[Set[JsObject]]\n        .filter(annotation => annotation.fields(\"key\").convertTo[String] != \"exec\") shouldBe testAnnotations.toJsArray\n        .convertTo[Set[JsObject]]\n\n  }\n\n  it should \"invoke action while not encoding &, <, > characters\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"nonescape\"\n    val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n    val nonescape = \"&<>\"\n    val input = Map(\"payload\" -> nonescape.toJson)\n    val output = JsObject(\"payload\" -> JsString(s\"hello, $nonescape!\"))\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, file)\n    }\n\n    withActivation(wsk.activation, wsk.action.invoke(name, parameters = input)) { activation =>\n      activation.response.success shouldBe true\n      activation.response.result shouldBe Some(output)\n      activation.logs.toList.flatten.filter(_.contains(nonescape)).length shouldBe 1\n    }\n  }\n\n  behavior of \"Wsk packages\"\n\n  it should \"create, and delete a package\" in {\n    val name = \"createDeletePackage\"\n    wsk.pkg.create(name).statusCode shouldBe OK\n    wsk.pkg.delete(name).statusCode shouldBe OK\n  }\n\n  it should \"create, and get a package to verify parameter and annotation parsing\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"packageAnnotAndParamParsing\"\n\n      assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n        pkg.create(name, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)\n      }\n\n      val pack = wsk.pkg.get(name)\n\n      val receivedParams = wsk.parseJsonString(pack.stdout).fields(\"parameters\").convertTo[JsArray].elements\n      val receivedAnnots = wsk.parseJsonString(pack.stdout).fields(\"annotations\").convertTo[JsArray].elements\n      val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements\n\n      for (expectedItem <- escapedJSONArr) {\n        receivedParams should contain(expectedItem)\n        receivedAnnots should contain(expectedItem)\n      }\n  }\n\n  it should \"create, and get a package to verify file parameter and annotation parsing\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"packageAnnotAndParamFileParsing\"\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n      val argInput = Some(TestUtils.getTestActionFilename(\"validInput1.json\"))\n\n      assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n        pkg.create(name, annotationFile = argInput, parameterFile = argInput)\n      }\n\n      val stdout = wsk.pkg.get(name).stdout\n      val receivedParams = wsk.parseJsonString(stdout).fields(\"parameters\").convertTo[JsArray].elements\n      val receivedAnnots = wsk.parseJsonString(stdout).fields(\"annotations\").convertTo[JsArray].elements\n      val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements\n\n      for (expectedItem <- escapedJSONArr) {\n        receivedParams should contain(expectedItem)\n        receivedAnnots should contain(expectedItem)\n      }\n  }\n\n  it should \"create a package with the proper parameter and annotation escapes\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"packageEscapses\"\n\n      assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n        pkg.create(name, parameters = getEscapedJSONTestArgInput, annotations = getEscapedJSONTestArgInput)\n      }\n\n      val pack = wsk.pkg.get(name)\n      val receivedParams = wsk.parseJsonString(pack.stdout).fields(\"parameters\").convertTo[JsArray].elements\n      val receivedAnnots = wsk.parseJsonString(pack.stdout).fields(\"annotations\").convertTo[JsArray].elements\n      val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements\n\n      for (expectedItem <- escapedJSONArr) {\n        receivedParams should contain(expectedItem)\n        receivedAnnots should contain(expectedItem)\n      }\n  }\n\n  it should \"report conformance error accessing action as package\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"aAsP\"\n    val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, file)\n    }\n\n    wsk.pkg.get(name, expectedExitCode = Conflict.intValue).stderr should include(Messages.conformanceMessage)\n\n    wsk.pkg.bind(name, \"bogus\", expectedExitCode = Conflict.intValue).stderr should include(\n      Messages.requestedBindingIsNotValid)\n\n    wsk.pkg.bind(\"bogus\", \"alsobogus\", expectedExitCode = BadRequest.intValue).stderr should include(\n      Messages.bindingDoesNotExist)\n\n  }\n\n  behavior of \"Wsk triggers\"\n\n  it should \"create, and get a trigger to verify parameter and annotation parsing\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"triggerAnnotAndParamParsing\"\n\n      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n        trigger.create(name, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)\n      }\n\n      val stdout = wsk.trigger.get(name).stdout\n\n      val receivedParams = wsk.parseJsonString(stdout).fields(\"parameters\").convertTo[JsArray].elements\n      val receivedAnnots = wsk.parseJsonString(stdout).fields(\"annotations\").convertTo[JsArray].elements\n      val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements\n\n      for (expectedItem <- escapedJSONArr) {\n        receivedParams should contain(expectedItem)\n        receivedAnnots should contain(expectedItem)\n      }\n  }\n\n  it should \"create, and get a trigger to verify file parameter and annotation parsing\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"triggerAnnotAndParamFileParsing\"\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n      val argInput = Some(TestUtils.getTestActionFilename(\"validInput1.json\"))\n\n      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n        trigger.create(name, annotationFile = argInput, parameterFile = argInput)\n      }\n\n      val stdout = wsk.trigger.get(name).stdout\n\n      val receivedParams = wsk.parseJsonString(stdout).fields(\"parameters\").convertTo[JsArray].elements\n      val receivedAnnots = wsk.parseJsonString(stdout).fields(\"annotations\").convertTo[JsArray].elements\n      val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements\n\n      for (expectedItem <- escapedJSONArr) {\n        receivedParams should contain(expectedItem)\n        receivedAnnots should contain(expectedItem)\n      }\n  }\n\n  it should \"create a trigger with the proper parameter and annotation escapes\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"triggerEscapes\"\n\n      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n        trigger.create(name, parameters = getEscapedJSONTestArgInput, annotations = getEscapedJSONTestArgInput)\n      }\n\n      val stdout = wsk.trigger.get(name).stdout\n\n      val receivedParams = wsk.parseJsonString(stdout).fields(\"parameters\").convertTo[JsArray].elements\n      val receivedAnnots = wsk.parseJsonString(stdout).fields(\"annotations\").convertTo[JsArray].elements\n      val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements\n\n      for (expectedItem <- escapedJSONArr) {\n        receivedParams should contain(expectedItem)\n        receivedAnnots should contain(expectedItem)\n      }\n  }\n\n  it should \"not create a trigger when feed fails to initialize\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    assetHelper.withCleaner(wsk.trigger, \"badfeed\", confirmDelete = false) { (trigger, name) =>\n      trigger.create(name, feed = Some(s\"bogus\"), expectedExitCode = ANY_ERROR_EXIT).exitCode should equal(NOT_FOUND)\n      trigger.get(name, expectedExitCode = NotFound.intValue)\n\n      trigger.create(name, feed = Some(s\"bogus/feed\"), expectedExitCode = ANY_ERROR_EXIT).exitCode should equal(\n        NOT_FOUND)\n      trigger.get(name, expectedExitCode = NotFound.intValue)\n    }\n  }\n\n  it should \"invoke a feed action with the correct lifecyle event when creating, retrieving and deleting a feed trigger\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val actionName = \"echo\"\n    val triggerName = \"feedTest\"\n\n    assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n      action.create(actionName, Some(TestUtils.getTestActionFilename(\"echo.js\")))\n    }\n\n    try {\n      wsk.trigger.create(triggerName, feed = Some(actionName)).statusCode shouldBe OK\n\n      wsk.trigger.get(triggerName).statusCode shouldBe OK\n    } finally {\n      wsk.trigger.delete(triggerName).statusCode shouldBe OK\n    }\n  }\n\n  it should \"forward headers as parameters to the associated action\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val guestNamespace = wsk.namespace.whois()\n      val name = \"triggerWithHeaders\"\n      val actionName = \"params\"\n      val ruleName = \"ruleWithHeaders\"\n\n      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n        trigger.create(name)\n      }\n\n      assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n        action.create(actionName, Some(TestUtils.getTestActionFilename(\"params.js\")))\n      }\n\n      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, _) =>\n        rule.create(ruleName, trigger = name, action = actionName)\n        rule.enable(ruleName)\n      }\n\n      val path = Path(s\"$basePath/namespaces/$guestNamespace/triggers/$name\")\n\n      val resp = requestEntityWithHeader(POST, path, List(RawHeader(\"Foo\", \"Bar\")))(wp)\n      val result = new RestResult(resp.status.intValue, getTransactionId(resp), getRespData(resp))\n\n      withActivation(wsk.activation, result) { triggerActivation =>\n        val ruleActivation = triggerActivation.logs.get.map(_.parseJson.convertTo[common.RuleActivationResult]).head\n        withActivation(wsk.activation, ruleActivation.activationId) { actionActivation =>\n          actionActivation.response.result match {\n            case Some(result) =>\n              result.asJsObject.fields.get(\"args\") map { headers =>\n                headers.asJsObject.fields.get(\"__ow_headers\") map { params =>\n                  params.asJsObject.fields.get(\"foo\") map { foo =>\n                    foo shouldBe JsString(\"Bar\")\n                  }\n                }\n              }\n\n            case others =>\n              fail(s\"no result found: $others\")\n\n          }\n          actionActivation.cause shouldBe None\n        }\n      }\n  }\n\n  def requestEntityWithHeader(method: HttpMethod,\n                              path: Path,\n                              headers: List[HttpHeader],\n                              params: Map[String, String] = Map.empty,\n                              body: Option[String] = None)(implicit wp: WskProps): HttpResponse = {\n    val credentials = wp.authKey.split(\":\")\n    val creds = new BasicHttpCredentials(credentials(0), credentials(1))\n\n    // startsWith(http) includes https\n    val hostWithScheme = if (wp.apihost.startsWith(\"http\")) {\n      Uri(wp.apihost)\n    } else {\n      Uri().withScheme(\"https\").withHost(wp.apihost)\n    }\n\n    val request = HttpRequest(\n      method,\n      hostWithScheme.withPath(path).withQuery(Query(params)),\n      Authorization(creds) :: headers,\n      entity =\n        body.map(b => HttpEntity.Strict(ContentTypes.`application/json`, ByteString(b))).getOrElse(HttpEntity.Empty))\n    val response = Http().singleRequest(request, connectionContext).flatMap { _.toStrict(toStrictTimeout) }.futureValue\n\n    logger.debug(this, s\"Request: $request\")\n    logger.debug(this, s\"Response: $response\")\n\n    val validationErrors = validateRequestAndResponse(request, response)\n    if (validationErrors.nonEmpty) {\n      fail(\n        s\"HTTP request or response did not match the Swagger spec.\\nRequest: $request\\n\" +\n          s\"Response: $response\\nValidation Error: $validationErrors\")\n    }\n    response\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskRestEntitlementTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.BadGateway\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Forbidden\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common.rest.WskRestOperations\nimport common.rest.RestResult\nimport common.TestUtils.RunResult\nimport common.WskActorSystem\n\n@RunWith(classOf[JUnitRunner])\nclass WskRestEntitlementTests extends WskEntitlementTests with WskActorSystem {\n  override lazy val wsk = new WskRestOperations\n  override lazy val forbiddenCode = Forbidden.intValue\n  override lazy val timeoutCode = BadGateway.intValue\n  override lazy val notFoundCode = NotFound.intValue\n\n  override def verifyAction(action: RunResult): org.scalatest.Assertion = {\n    val stdout = action.stdout\n    stdout should include(\"name\")\n    stdout should include(\"parameters\")\n    stdout should include(\"limits\")\n    stdout should include(\"\"\"\"key\":\"a\"\"\"\")\n    stdout should include(\"\"\"\"value\":\"A\"\"\"\")\n  }\n\n  override def verifyPackageList(packageList: RunResult,\n                                 namespace: String,\n                                 packageName: String,\n                                 actionName: String): Unit = {\n    val packageListResultRest = packageList.asInstanceOf[RestResult]\n    val packages = packageListResultRest.getBodyListJsObject\n    val ns = s\"$namespace/$packageName\"\n    packages.exists(pack =>\n      RestResult.getField(pack, \"namespace\") == ns && RestResult.getField(pack, \"name\") == actionName) shouldBe true\n  }\n\n  override def verifyPackageSharedList(packageList: RunResult, namespace: String, packageName: String): Unit = {\n    val packageListResultRest = packageList.asInstanceOf[RestResult]\n    val packages = packageListResultRest.getBodyListJsObject\n    packages.exists(\n      pack =>\n        RestResult.getField(pack, \"namespace\") == namespace && RestResult\n          .getField(pack, \"name\") == packageName) shouldBe true\n  }\n\n  override def verifyPackageNotSharedList(packageList: RunResult, namespace: String, packageName: String): Unit = {\n    val packageListResultRest = packageList.asInstanceOf[RestResult]\n    val packages = packageListResultRest.getBodyListJsObject\n    packages.exists(\n      pack =>\n        RestResult.getField(pack, \"namespace\") == namespace && RestResult\n          .getField(pack, \"name\") == packageName) shouldBe false\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/cli/test/WskWebActionsTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli.test\n\nimport java.util.Base64\n\nimport scala.util.Failure\nimport scala.util.Try\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport io.restassured.RestAssured\nimport io.restassured.http.Header\nimport common._\nimport common.rest.WskRestOperations\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport system.rest.RestUtil\nimport org.apache.openwhisk.common.PrintStreamLogging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.Subject\n\n/**\n * Tests web actions.\n */\n@RunWith(classOf[JUnitRunner])\nclass WskWebActionsTests extends TestHelpers with WskTestHelpers with RestUtil with WskActorSystem {\n  val MAX_URL_LENGTH = 8192 // 8K matching nginx default\n\n  private implicit val wskprops = WskProps()\n  val wsk: WskOperations = new WskRestOperations\n  lazy val namespace = wsk.namespace.whois()\n\n  protected val testRoutePath: String = \"/api/v1/web\"\n\n  behavior of \"Wsk Web Actions\"\n\n  it should \"ensure __ow_headers contains the proper content-type\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"webContenttype\"\n    val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n    val bodyContent = JsObject(\"key\" -> \"value\".toJson)\n    val host = getServiceURL()\n    val url = s\"$host$testRoutePath/$namespace/default/$name.json\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, file, web = Some(\"true\"))\n    }\n\n    val resWithContentType =\n      RestAssured.given().contentType(\"application/json\").body(bodyContent.compactPrint).config(sslconfig).post(url)\n\n    resWithContentType.statusCode shouldBe 200\n    resWithContentType.header(\"Content-type\") shouldBe \"application/json\"\n    resWithContentType.body.asString.parseJson.asJsObject\n      .fields(\"__ow_headers\")\n      .asJsObject\n      .fields(\"content-type\") shouldBe \"application/json\".toJson\n\n    val resWithoutContentType =\n      RestAssured.given().config(sslconfig).get(url)\n\n    resWithoutContentType.statusCode shouldBe 200\n    resWithoutContentType.header(\"Content-type\") shouldBe \"application/json\"\n    resWithoutContentType.body.asString.parseJson.asJsObject\n      .fields(\"__ow_headers\")\n      .toString should not include (\"content-type\")\n  }\n\n  /**\n   * Tests web actions, plus max url limit.\n   */\n  it should \"create a web action accessible via HTTPS\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"webaction\"\n    val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n    val host = getServiceURL()\n    val requestPath = host + s\"$testRoutePath/$namespace/default/$name.json/a?a=\"\n    val padAmount = MAX_URL_LENGTH - requestPath.length\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, file, web = Some(\"true\"))\n    }\n\n    Seq(\n      (\"A\", 200),\n      (\"A\" * padAmount, 200),\n      // ideally the bad case is just +1 but there's some differences\n      // in how characters are counted i.e., whether these count \"https://:443\"\n      // or not; it seems sufficient to test right around the boundary\n      (\"A\" * (padAmount + 100), 414))\n      .foreach {\n        case (pad, code) =>\n          val url = (requestPath + pad)\n          val response = RestAssured.given().config(sslconfig).get(url)\n          val responseCode = response.statusCode\n\n          withClue(s\"response code: $responseCode, url length: ${url.length}, pad amount: ${pad.length}, url: $url\") {\n            responseCode shouldBe code\n            if (code == 200) {\n              response.body.asString.parseJson.asJsObject.fields(\"a\").convertTo[String] shouldBe pad\n            } else {\n              response.body.asString should include(\"414 Request-URI Too Large\") // from nginx\n            }\n          }\n      }\n  }\n\n  /**\n   * Tests web action requiring authentication.\n   */\n  it should \"create a web action requiring authentication accessible via HTTPS\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"webaction\"\n      val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n      val host = getServiceURL()\n      val url = s\"$host$testRoutePath/$namespace/default/$name.json\"\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, web = Some(\"true\"), annotations = Map(\"require-whisk-auth\" -> true.toJson))\n      }\n\n      val unauthorizedResponse = RestAssured.given().config(sslconfig).get(url)\n      unauthorizedResponse.statusCode shouldBe 401\n\n      val authorizedResponse = RestAssured\n        .given()\n        .config(sslconfig)\n        .auth()\n        .preemptive()\n        .basic(wskprops.authKey.split(\":\")(0), wskprops.authKey.split(\":\")(1))\n        .get(url)\n\n      authorizedResponse.statusCode shouldBe 200\n      authorizedResponse.body.asString.parseJson.asJsObject.fields(\"__ow_user\").convertTo[String] shouldBe namespace\n  }\n\n  /**\n   * Tests web action not requiring authentication.\n   */\n  it should \"create a web action not requiring authentication accessible via HTTPS\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"webaction\"\n      val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n      val host = getServiceURL()\n      val url = s\"$host$testRoutePath/$namespace/default/$name.json\"\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, web = Some(\"true\"), annotations = Map(\"require-whisk-auth\" -> false.toJson))\n      }\n\n      val unauthorizedResponse = RestAssured.given().config(sslconfig).get(url)\n      unauthorizedResponse.statusCode shouldBe 200\n  }\n\n  it should \"ensure that CORS header is preserved for custom options\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"webaction\"\n      val file = Some(TestUtils.getTestActionFilename(\"corsHeaderMod.js\"))\n      val host = getServiceURL()\n      val url = host + s\"$testRoutePath/$namespace/default/$name.http\"\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, web = Some(\"true\"), annotations = Map(\"web-custom-options\" -> true.toJson))\n      }\n\n      val response = RestAssured.given().config(sslconfig).options(url)\n\n      response.statusCode shouldBe 200\n      response.header(\"Access-Control-Allow-Origin\") shouldBe \"Origin set from Web Action\"\n      response.header(\"Access-Control-Allow-Methods\") shouldBe \"Methods set from Web Action\"\n      response.header(\"Access-Control-Allow-Headers\") shouldBe \"Headers set from Web Action\"\n      response.header(\"Location\") shouldBe \"openwhisk.org\"\n      response.header(\"Set-Cookie\") shouldBe \"cookie-cookie-cookie\"\n  }\n\n  it should \"ensure that default CORS header is preserved\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"webaction\"\n    val file = Some(TestUtils.getTestActionFilename(\"corsHeaderMod.js\"))\n    val host = getServiceURL()\n    val url = host + s\"$testRoutePath/$namespace/default/$name\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, file, web = Some(\"true\"))\n    }\n\n    Seq(\n      RestAssured\n        .given()\n        .config(sslconfig)\n        .header(\"Access-Control-Request-Headers\", \"x-custom-header\")\n        .options(s\"$url.http\"),\n      RestAssured\n        .given()\n        .config(sslconfig)\n        .header(\"Access-Control-Request-Headers\", \"x-custom-header\")\n        .get(s\"$url.json\")).foreach { response =>\n      response.statusCode shouldBe 200\n      response.header(\"Access-Control-Allow-Origin\") shouldBe \"*\"\n      response.header(\"Access-Control-Allow-Methods\") shouldBe \"OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH\"\n      response.header(\"Access-Control-Allow-Headers\") shouldBe \"x-custom-header\"\n      response.header(\"Location\") shouldBe null\n      response.header(\"Set-Cookie\") shouldBe null\n    }\n\n    Seq(\n      RestAssured.given().config(sslconfig).options(s\"$url.http\"),\n      RestAssured.given().config(sslconfig).get(s\"$url.json\")).foreach { response =>\n      response.statusCode shouldBe 200\n      response.header(\"Access-Control-Allow-Origin\") shouldBe \"*\"\n      response.header(\"Access-Control-Allow-Methods\") shouldBe \"OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH\"\n      response.header(\"Access-Control-Allow-Headers\") shouldBe \"Authorization, Origin, X-Requested-With, Content-Type, Accept, User-Agent\"\n      response.header(\"Location\") shouldBe null\n      response.header(\"Set-Cookie\") shouldBe null\n    }\n  }\n\n  it should \"invoke web action to ensure the returned body argument is correct\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"webaction\"\n      val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n      val bodyContent = \"This is the body\"\n      val host = getServiceURL()\n      val url = s\"$host$testRoutePath/$namespace/default/webaction.json\"\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, web = Some(\"true\"))\n      }\n\n      val paramRes = RestAssured.given().contentType(\"text/html\").param(\"key\", \"value\").config(sslconfig).post(url)\n      paramRes.statusCode shouldBe 200\n      paramRes.body.asString().parseJson.asJsObject.fields(\"__ow_body\").convertTo[String] shouldBe \"key=value\"\n\n      val bodyRes = RestAssured.given().contentType(\"text/html\").body(bodyContent).config(sslconfig).post(url)\n      bodyRes.statusCode shouldBe 200\n      bodyRes.body.asString().parseJson.asJsObject.fields(\"__ow_body\").convertTo[String] shouldBe bodyContent\n  }\n\n  it should \"reject invocation of web action with invalid accept header\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"webaction\"\n      val file = Some(TestUtils.getTestActionFilename(\"textBody.js\"))\n      val host = getServiceURL()\n      val url = host + s\"$testRoutePath/$namespace/default/$name.http\"\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, web = Some(\"true\"))\n      }\n\n      val response = RestAssured.given().header(\"accept\", \"application/json\").config(sslconfig).get(url)\n      response.statusCode shouldBe 406\n      response.body.asString should include(\"Resource representation is only available with these types:\\\\ntext/html\")\n  }\n\n  it should \"support multiple response header values\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"webaction\"\n    val file = Some(TestUtils.getTestActionFilename(\"multipleHeaders.js\"))\n    val host = getServiceURL()\n    val url = host + s\"$testRoutePath/$namespace/default/$name.http\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, file, web = Some(\"true\"), annotations = Map(\"web-custom-options\" -> true.toJson))\n    }\n\n    val response = RestAssured.given().config(sslconfig).options(url)\n\n    response.statusCode shouldBe 200\n    val cookieHeaders = response.headers.getList(\"Set-Cookie\")\n    cookieHeaders should contain allOf (new Header(\"Set-Cookie\", \"a=b\"),\n    new Header(\"Set-Cookie\", \"c=d\"))\n  }\n\n  it should \"handle http web action returning JSON as string\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"jsonStringWebAction\"\n    val file = Some(TestUtils.getTestActionFilename(\"jsonStringWebAction.js\"))\n    val host = getServiceURL\n    val url = host + s\"$testRoutePath/$namespace/default/$name.http\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, file, web = Some(\"raw\"))\n    }\n\n    val response = RestAssured.given().config(sslconfig).get(url)\n\n    response.statusCode shouldBe 200\n    response.header(\"Content-type\") shouldBe \"application/json\"\n    response.body.asString.parseJson.asJsObject shouldBe JsObject(\"status\" -> \"success\".toJson)\n  }\n\n  it should \"handle http web action with base64 encoded binary response\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"binaryWeb\"\n      val file = Some(TestUtils.getTestActionFilename(\"pngWeb.js\"))\n      val host = getServiceURL\n      val url = host + s\"$testRoutePath/$namespace/default/$name.http\"\n      val png = \"iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAA/klEQVQYGWNgAAEHBxaG//+ZQMyyn581Pfas+cRQnf1LfF\" +\n        \"Ljf+62smUgcUbt0FA2Zh7drf/ffMy9vLn3RurrW9e5hCU11i2azfD4zu1/DHz8TAy/foUxsXBrFzHzC7r8+M9S1vn1qxQT07dDjL\" +\n        \"9fdemrqKxlYGT6z8AIMo6hgeUfA0PUvy9fGFh5GWK3z7vNxSWt++jX99+8SoyiGQwsW38w8PJEM7x5v5SJ8f+/xv8MDAzffv9hev\" +\n        \"fkWjiXBGMpMx+j2awovjcMjFztDO8+7GF49LkbZDCDeXLTWnZO7qDfn1/+5jbw/8pjYWS4wZLztXnuEuYTk2M+MzIw/AcA36Vewa\" +\n        \"D6fzsAAAAASUVORK5CYII=\"\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file, web = Some(\"true\"))\n      }\n\n      val response = RestAssured.given().config(sslconfig).get(url)\n\n      response.statusCode shouldBe 200\n      response.header(\"Content-type\") shouldBe \"image/png\"\n      response.body.asByteArray shouldBe Base64.getDecoder().decode(png)\n  }\n\n  /**\n   * Tests web action for HEAD requests\n   */\n  it should \"create a web action making a HEAD request\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"webactionHead\"\n    val file = Some(TestUtils.getTestActionFilename(\"echo-web-http-head.js\"))\n    val host = getServiceURL()\n    val url = s\"$host$testRoutePath/$namespace/default/$name\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, file, web = Some(\"true\"))\n    }\n\n    val authorizedResponse = RestAssured\n      .given()\n      .config(sslconfig)\n      .auth()\n      .preemptive()\n      .basic(wskprops.authKey.split(\":\")(0), wskprops.authKey.split(\":\")(1))\n      .head(url)\n\n    authorizedResponse.statusCode shouldBe 200\n    authorizedResponse.body.asString() shouldBe \"\"\n    authorizedResponse.getHeader(\"Request-type\") shouldBe \"head\"\n  }\n\n  private val subdomainRegex = Seq.fill(WhiskProperties.getPartsInVanitySubdomain)(\"[a-zA-Z0-9]+\").mkString(\"-\")\n\n  private lazy val (vanitySubdomain, vanityNamespace, makeTestSubject) = {\n    if (namespace.matches(subdomainRegex)) {\n      (namespace, namespace, false)\n    } else {\n      val s = Subject().asString.toLowerCase // this will generate two confirming parts\n      (s, s.replace(\"-\", \"_\"), true)\n    }\n  }\n\n  private lazy val wskPropsForSubdomainTest = if (makeTestSubject) {\n    getAdditionalTestSubject(vanityNamespace) // create new subject for the test\n  } else {\n    WskProps()\n  }\n\n  override def afterAll() = {\n    if (makeTestSubject) {\n      disposeAdditionalTestSubject(vanityNamespace)\n    }\n  }\n\n  \"test subdomain\" should \"have conforming parts\" in {\n    vanitySubdomain should fullyMatch regex subdomainRegex.r\n    vanitySubdomain.length should be <= 63\n  }\n\n  \"vanity subdomain\" should \"access a web action via namespace subdomain\" in withAssetCleaner(wskPropsForSubdomainTest) {\n    (wp, assetHelper) =>\n      val actionName = \"webaction\"\n\n      val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n      assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n        action.create(actionName, file, web = Some(true.toString))(wp)\n      }\n\n      val url = getServiceApiHost(vanitySubdomain, true) + s\"/default/$actionName.json/a?a=A\"\n      println(s\"url: $url\")\n\n      // try the rest assured path first, failing that, try curl with explicit resolve\n      Try {\n        val response = RestAssured.given().config(sslconfig).get(url)\n        val responseCode = response.statusCode\n        responseCode shouldBe 200\n        response.body.asString.parseJson.asJsObject.fields(\"a\").convertTo[String] shouldBe \"A\"\n      } match {\n        case Failure(t) =>\n          println(s\"RestAssured path failed, trying curl: $t\")\n          implicit val tid = TransactionId.testing\n          implicit val logger = new PrintStreamLogging(Console.out)\n          val host = getServiceApiHost(vanitySubdomain, false)\n          // if the edge host is a name, try to resolve it, otherwise, it should be an ip address already\n          val edgehost = WhiskProperties.getEdgeHost\n          val ip = Try(java.net.InetAddress.getByName(edgehost).getHostAddress) getOrElse \"???\"\n          println(s\"edge: $edgehost, ip: $ip\")\n          val cmd = Seq(\"curl\", \"-k\", url, \"--resolve\", s\"$host:$ip\")\n          val (stdout, stderr, exitCode) = SimpleExec.syncRunCmd(cmd)\n          withClue(s\"\\n$stderr\\n\") {\n            stdout.parseJson.asJsObject.fields(\"a\").convertTo[String] shouldBe \"A\"\n            exitCode shouldBe 0\n          }\n\n        case _ =>\n      }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/connector/test/EventMessageTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.connector.test\n\nimport java.time.Instant\nimport java.util.concurrent.TimeUnit\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.connector.Activation\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\n\nimport scala.concurrent.duration._\nimport scala.util.Success\n\n/**\n * Unit tests for the EventMessage objects.\n */\n@RunWith(classOf[JUnitRunner])\nclass EventMessageTests extends AnyFlatSpec with Matchers {\n\n  behavior of \"Activation\"\n\n  val activationId = ActivationId.generate()\n  val fullActivation = WhiskActivation(\n    namespace = EntityPath(\"ns\"),\n    name = EntityName(\"a\"),\n    Subject(),\n    activationId = activationId,\n    start = Instant.now(),\n    end = Instant.now(),\n    response = ActivationResponse.success(Some(JsObject(\"res\" -> JsNumber(1))), Some(42)),\n    annotations = Parameters(\"limits\", ActionLimits(TimeLimit(1.second), MemoryLimit(128.MB), LogLimit(1.MB)).toJson) ++\n      Parameters(WhiskActivation.waitTimeAnnotation, 5.toJson) ++\n      Parameters(WhiskActivation.initTimeAnnotation, 10.toJson) ++\n      Parameters(WhiskActivation.kindAnnotation, \"testkind\") ++\n      Parameters(WhiskActivation.pathAnnotation, \"ns2/a\") ++\n      Parameters(WhiskActivation.causedByAnnotation, \"sequence\"),\n    duration = Some(123))\n\n  it should \"transform an activation into an event body\" in {\n    Activation.from(fullActivation) shouldBe Success(\n      Activation(\n        \"ns2/a\",\n        activationId.asString,\n        0,\n        toDuration(123),\n        toDuration(5),\n        toDuration(10),\n        \"testkind\",\n        false,\n        128,\n        Some(\"sequence\"),\n        Some(42)))\n  }\n\n  it should \"fail transformation if needed annotations are missing\" in {\n    Activation.from(fullActivation.copy(annotations = Parameters())) shouldBe 'failure\n    Activation.from(fullActivation.copy(annotations = fullActivation.annotations - WhiskActivation.kindAnnotation)) shouldBe 'failure\n    Activation.from(fullActivation.copy(annotations = fullActivation.annotations - WhiskActivation.pathAnnotation)) shouldBe 'failure\n  }\n\n  it should \"provide sensible defaults for optional annotations\" in {\n    val a =\n      fullActivation\n        .copy(\n          duration = None,\n          annotations = Parameters(WhiskActivation.kindAnnotation, \"testkind\") ++ Parameters(\n            WhiskActivation.pathAnnotation,\n            \"ns2/a\"))\n\n    Activation.from(a) shouldBe Success(\n      Activation(\n        \"ns2/a\",\n        activationId.asString,\n        0,\n        toDuration(0),\n        toDuration(0),\n        toDuration(0),\n        \"testkind\",\n        false,\n        0,\n        None,\n        Some(42)))\n  }\n\n  it should \"Transform a activation with status code\" in {\n    val resultWithError =\n      \"\"\"\n        |{\n        | \"statusCode\" : 404,\n        | \"body\": \"Requested resource not found\"\n        |}\n        |\"\"\".stripMargin.parseJson\n    val a =\n      fullActivation\n        .copy(response = ActivationResponse.applicationError(resultWithError, Some(42)))\n    Activation.from(a).map(act => act.userDefinedStatusCode) shouldBe Success(Some(404))\n  }\n\n  it should \"Transform a activation with error status code\" in {\n    val resultWithError =\n      \"\"\"\n        |{\n        | \"error\": {\n        |   \"statusCode\" : \"404\",\n        |   \"body\": \"Requested resource not found\"\n        | }\n        |}\n        |\"\"\".stripMargin.parseJson\n    Activation.userDefinedStatusCode(Some(resultWithError)) shouldBe Some(404)\n  }\n\n  it should \"Transform a activation with error status code with invalid error code\" in {\n    val resultWithInvalidError =\n      \"\"\"\n        |{\n        |   \"statusCode\" : \"i404\",\n        |   \"body\": \"Requested resource not found\"\n        |}\n        |\"\"\".stripMargin.parseJson\n    Activation.userDefinedStatusCode(Some(resultWithInvalidError)) shouldBe Some(400)\n  }\n\n  def toDuration(milliseconds: Long) = new FiniteDuration(milliseconds, TimeUnit.MILLISECONDS)\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/connector/test/MessageFeedTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.connector.test\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport scala.collection.mutable.Buffer\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\n\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.actor.FSM\nimport org.apache.pekko.actor.FSM.CurrentState\nimport org.apache.pekko.actor.FSM.SubscribeTransitionCallBack\nimport org.apache.pekko.actor.FSM.Transition\nimport org.apache.pekko.actor.PoisonPill\nimport org.apache.pekko.actor.Props\nimport org.apache.pekko.testkit.TestKit\nimport common.StreamLogging\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.connector.MessageFeed._\nimport org.apache.openwhisk.utils.retry\n\n@RunWith(classOf[JUnitRunner])\nclass MessageFeedTests\n    extends AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with StreamLogging {\n\n  val system = ActorSystem(\"MessageFeedTestSystem\")\n  val actorsToDestroyAfterEach: Buffer[ActorRef] = Buffer.empty\n\n  override def afterEach() = {\n    actorsToDestroyAfterEach.foreach { _ ! PoisonPill }\n    actorsToDestroyAfterEach.clear()\n  }\n\n  override def afterAll() = TestKit.shutdownActorSystem(system)\n\n  case class Connector(autoStart: Boolean = true) extends TestKit(system) {\n    val peekCount = new AtomicInteger()\n\n    val consumer = new TestConnector(\"feedtest\", 4, true) {\n      override def peek(duration: FiniteDuration, retry: Int = 0) = {\n        peekCount.incrementAndGet()\n        super.peek(duration)\n      }\n    }\n\n    val sentCount = new AtomicInteger()\n\n    def fill(n: Int) = {\n      val msgs = (1 to n).map { _ =>\n        new Message {\n          override def serialize = {\n            sentCount.incrementAndGet().toString\n          }\n          override def toString = {\n            s\"message${sentCount.get}\"\n          }\n        }\n      }\n      consumer.send(msgs)\n    }\n\n    val receivedCount = new AtomicInteger()\n\n    def handler(bytes: Array[Byte]): Future[Unit] = {\n      Future.successful(receivedCount.incrementAndGet())\n    }\n\n    val fsm = childActorOf(\n      Props(new MessageFeed(\"test\", logging, consumer, consumer.maxPeek, 200.milliseconds, handler, autoStart)))\n\n    actorsToDestroyAfterEach += (fsm, testActor)\n\n    def monitorTransitionsAndStart() = {\n      fsm ! SubscribeTransitionCallBack(testActor)\n      expectMsg(CurrentState(fsm, Idle))\n      fsm ! Ready\n      expectMsg(Transition(fsm, Idle, FillingPipeline))\n      this\n    }\n  }\n\n  def timeout(actor: ActorRef) = actor ! FSM.StateTimeout\n\n  it should \"wait for ready before accepting messages\" in {\n    val connector = Connector(autoStart = false)\n    connector.fsm ! SubscribeTransitionCallBack(connector.testActor)\n\n    // start idle\n    connector.expectMsg(CurrentState(connector.fsm, Idle))\n\n    // stay until received ready\n    connector.fsm ! FSM.StateTimeout // should be ignored\n    connector.fsm ! Processed // should be ignored\n    Thread.sleep(500.milliseconds.toMillis)\n    connector.peekCount.get shouldBe 0\n\n    // start filling\n    connector.fsm ! Ready\n    connector.expectMsg(Transition(connector.fsm, Idle, FillingPipeline))\n    retry(connector.peekCount.get should be > 0)\n  }\n\n  it should \"auto start and start polling for messages\" in {\n    val connector = Connector(autoStart = true)\n    // automatically start filling\n    retry(connector.peekCount.get should be > 0, 5, Some(200.milliseconds))\n  }\n\n  it should \"stop polling for messages when the pipeline is full\" in {\n    val connector = Connector(autoStart = false).monitorTransitionsAndStart()\n    // push enough to cause pipeline to exceed fill mark\n    connector.fill(connector.consumer.maxPeek * 2 + 1)\n    retry(connector.peekCount.get should be > 0)\n    retry(connector.receivedCount.get shouldBe connector.consumer.maxPeek, 10, Some(200.milliseconds))\n\n    val peeks = connector.peekCount.get\n    connector.expectMsg(Transition(connector.fsm, FillingPipeline, DrainingPipeline))\n\n    connector.peekCount.get shouldBe peeks\n    connector.expectNoMessage(500.milliseconds)\n  }\n\n  it should \"transition from drain to fill mode\" in {\n    val connector = Connector(autoStart = false).monitorTransitionsAndStart()\n    println(connector.fsm.toString())\n    // push enough to cause pipeline to exceed fill mark\n    val sendCount = connector.consumer.maxPeek * 2 + 2\n    connector.fill(sendCount)\n    retry(connector.peekCount.get should be > 0)\n    retry(connector.receivedCount.get shouldBe connector.consumer.maxPeek, 10, Some(200.milliseconds))\n\n    val peeks = connector.peekCount.get\n    connector.expectMsg(Transition(connector.fsm, FillingPipeline, DrainingPipeline))\n\n    // stay in drain mode, no more peeking\n    timeout(connector.fsm) // should be ignored\n    connector.expectNoMessage(500.milliseconds)\n    connector.peekCount.get shouldBe peeks // no new reads\n\n    // expecting overflow of 2 in the queue, which is true if all expected messages were sent\n    retry(connector.sentCount.get shouldBe sendCount, 5, Some(200.milliseconds))\n\n    // drain one, should stay in draining state\n    connector.fsm ! Processed\n    connector.expectNoMessage(500.milliseconds)\n    connector.peekCount.get shouldBe peeks // no new reads\n\n    // back to fill mode\n    connector.fsm ! Processed\n    connector.expectMsg(Transition(connector.fsm, DrainingPipeline, FillingPipeline))\n    retry(connector.peekCount.get should be >= (peeks + 1))\n\n    // should send back to drain mode\n    connector.fill(1)\n    connector.expectMsg(Transition(connector.fsm, FillingPipeline, DrainingPipeline))\n\n    connector.expectNoMessage(500.milliseconds)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/connector/test/MessageTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.connector.test\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Unhealthy}\nimport org.apache.openwhisk.core.connector.InvokerResourceMessage\nimport org.apache.openwhisk.core.entity.SchedulerInstanceId\nimport org.apache.openwhisk.core.scheduler.{SchedulerEndpoints, SchedulerStates}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\n@RunWith(classOf[JUnitRunner])\nclass MessageTests extends TestKit(ActorSystem(\"Message\")) with AnyFlatSpecLike with Matchers {\n  behavior of \"Message\"\n\n  it should \"be able to compare the InvokerResourceMessage\" in {\n    val msg1 = InvokerResourceMessage(Unhealthy.asString, 1024L, 0, 0, Seq.empty, Seq.empty)\n    val msg2 = InvokerResourceMessage(Unhealthy.asString, 1024L, 0, 0, Seq.empty, Seq.empty)\n\n    msg1 == msg2 shouldBe true\n  }\n\n  it should \"be different when the state of InvokerResourceMessage is different\" in {\n    val msg1 = InvokerResourceMessage(Unhealthy.asString, 1024L, 0, 0, Seq.empty, Seq.empty)\n    val msg2 = InvokerResourceMessage(Healthy.asString, 1024L, 0, 0, Seq.empty, Seq.empty)\n\n    msg1 != msg2 shouldBe true\n  }\n\n  it should \"be different when the free memory of InvokerResourceMessage is different\" in {\n    val msg1 = InvokerResourceMessage(Healthy.asString, 1024L, 0, 0, Seq.empty, Seq.empty)\n    val msg2 = InvokerResourceMessage(Healthy.asString, 2048L, 0, 0, Seq.empty, Seq.empty)\n\n    msg1 != msg2 shouldBe true\n  }\n\n  it should \"be different when the busy memory of InvokerResourceMessage is different\" in {\n    val msg1 = InvokerResourceMessage(Healthy.asString, 1024L, 0, 0, Seq.empty, Seq.empty)\n    val msg2 = InvokerResourceMessage(Healthy.asString, 1024L, 1024L, 0, Seq.empty, Seq.empty)\n\n    msg1 != msg2 shouldBe true\n  }\n\n  it should \"be different when the in-progress memory of InvokerResourceMessage is different\" in {\n    val msg1 = InvokerResourceMessage(Healthy.asString, 1024L, 0, 0, Seq.empty, Seq.empty)\n    val msg2 = InvokerResourceMessage(Healthy.asString, 1024L, 0, 1024L, Seq.empty, Seq.empty)\n\n    msg1 != msg2 shouldBe true\n  }\n\n  it should \"be different when the tags of InvokerResourceMessage is different\" in {\n    val msg1 = InvokerResourceMessage(Healthy.asString, 1024L, 0, 0, Seq(\"tag1\"), Seq.empty)\n    val msg2 = InvokerResourceMessage(Healthy.asString, 1024L, 0, 0, Seq(\"tag1\", \"tag2\"), Seq.empty)\n\n    msg1 != msg2 shouldBe true\n  }\n\n  it should \"be different when the dedicated namespaces of InvokerResourceMessage is different\" in {\n    val msg1 = InvokerResourceMessage(Healthy.asString, 1024L, 0, 0, Seq.empty, Seq(\"ns1\"))\n    val msg2 = InvokerResourceMessage(Healthy.asString, 1024L, 0, 0, Seq.empty, Seq(\"ns2\"))\n\n    msg1 != msg2 shouldBe true\n  }\n\n  it should \"be able to compare the SchedulerStates\" in {\n    val msg1 = SchedulerStates(SchedulerInstanceId(\"0\"), queueSize = 0, SchedulerEndpoints(\"10.10.10.10\", 1234, 1234))\n    val msg2 = SchedulerStates(SchedulerInstanceId(\"0\"), queueSize = 0, SchedulerEndpoints(\"10.10.10.10\", 1234, 1234))\n\n    msg1 == msg2 shouldBe true\n  }\n\n  it should \"be different when the queue size of SchedulerStates is different\" in {\n    val msg1 = SchedulerStates(SchedulerInstanceId(\"0\"), queueSize = 20, SchedulerEndpoints(\"10.10.10.10\", 1234, 1234))\n    val msg2 = SchedulerStates(SchedulerInstanceId(\"0\"), queueSize = 10, SchedulerEndpoints(\"10.10.10.10\", 1234, 1234))\n\n    msg1 != msg2 shouldBe true\n  }\n\n  it should \"be not different when other than the queue size of SchedulerStates is different\" in {\n    // only the queue size matter\n    val msg1 = SchedulerStates(SchedulerInstanceId(\"0\"), queueSize = 20, SchedulerEndpoints(\"10.10.10.10\", 1234, 1234))\n    val msg2 = SchedulerStates(SchedulerInstanceId(\"1\"), queueSize = 20, SchedulerEndpoints(\"10.10.10.20\", 5678, 5678))\n\n    msg1 == msg2 shouldBe true\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/connector/test/TestConnector.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.connector.test\n\nimport java.util.ArrayList\nimport java.util.concurrent.LinkedBlockingQueue\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.collection.JavaConverters._\nimport common.StreamLogging\nimport org.apache.openwhisk.common.Counter\nimport org.apache.openwhisk.core.connector.{Message, MessageConsumer, MessageProducer, ResultMetadata}\n\nclass TestConnector(topic: String, override val maxPeek: Int, allowMoreThanMax: Boolean)\n    extends MessageConsumer\n    with StreamLogging {\n\n  override def peek(duration: FiniteDuration, retry: Int = 0) = {\n    val msgs = new ArrayList[Message]\n    queue.synchronized {\n      queue.drainTo(msgs, if (allowMoreThanMax) Int.MaxValue else maxPeek)\n      msgs.asScala map { m =>\n        offset += 1\n        (topic, -1, offset, m.serialize.getBytes)\n      }\n    }\n  }\n\n  override def commit(retry: Int = 0) = {\n    if (throwCommitException) {\n      throw new Exception(\"commit failed\")\n    } else {\n      // nothing to do\n    }\n  }\n\n  def occupancy = queue.size\n\n  def send(msg: Message): Future[ResultMetadata] = {\n    producer.send(topic, msg)\n  }\n\n  def send(msgs: Seq[Message]): Future[ResultMetadata] = {\n    import scala.language.reflectiveCalls\n    producer.sendBulk(topic, msgs)\n  }\n\n  def close() = {\n    closed = true\n    producer.close()\n  }\n\n  def getProducer(): MessageProducer = producer\n\n  private val producer = new MessageProducer {\n    def send(topic: String, msg: Message, retry: Int = 0): Future[ResultMetadata] = {\n      queue.synchronized {\n        if (queue.offer(msg)) {\n          logging.info(this, s\"put: $msg\")\n          Future.successful(ResultMetadata(topic, 0, queue.size()))\n        } else {\n          logging.error(this, s\"put failed: $msg\")\n          Future.failed(new IllegalStateException(\"failed to write msg\"))\n        }\n      }\n    }\n\n    def sendBulk(topic: String, msgs: Seq[Message]): Future[ResultMetadata] = {\n      queue.synchronized {\n        if (queue.addAll(msgs.asJava)) {\n          logging.info(this, s\"put: ${msgs.length} messages\")\n          Future.successful(ResultMetadata(topic, 0, queue.size()))\n        } else {\n          logging.error(this, s\"put failed: ${msgs.length} messages\")\n          Future.failed(new IllegalStateException(\"failed to write msg\"))\n        }\n      }\n    }\n\n    def close() = {}\n    def sentCount() = counter.next()\n    val counter = new Counter()\n  }\n\n  var throwCommitException = false\n  private val queue = new LinkedBlockingQueue[Message]()\n  @volatile private var closed = false\n  private var offset = -1L\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/connector/tests/AcknowledgementMessageTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.connector.tests\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json._\nimport org.apache.openwhisk.common.{TransactionId, WhiskInstants}\nimport org.apache.openwhisk.core.connector.{\n  AcknowledgementMessage,\n  CombinedCompletionAndResultMessage,\n  CompletionMessage,\n  ResultMessage\n}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size.SizeInt\n\nimport scala.concurrent.duration.DurationInt\nimport scala.util.Success\n\n/**\n * Unit tests for the AcknowledgementMessageTests object.\n */\n@RunWith(classOf[JUnitRunner])\nclass AcknowledgementMessageTests extends AnyFlatSpec with Matchers with WhiskInstants {\n\n  behavior of \"acknowledgement message\"\n\n  val defaultUserMemory: ByteSize = 1024.MB\n  val activation = WhiskActivation(\n    namespace = EntityPath(\"ns\"),\n    name = EntityName(\"a\"),\n    Subject(),\n    activationId = ActivationId.generate(),\n    start = nowInMillis(),\n    end = nowInMillis(),\n    response = ActivationResponse.success(Some(JsObject(\"res\" -> JsNumber(1)))),\n    annotations = Parameters(\"limits\", ActionLimits(TimeLimit(1.second), MemoryLimit(128.MB), LogLimit(1.MB)).toJson),\n    duration = Some(123))\n\n  it should \"serialize and deserialize a Result message with Left result\" in {\n    val m = ResultMessage(TransactionId.testing, activation).shrink\n    m.response shouldBe 'left\n    m.isSlotFree shouldBe empty\n    m.serialize shouldBe JsObject(\"transid\" -> m.transid.toJson, \"response\" -> m.response.left.get.toJson).compactPrint\n    m.serialize shouldBe m.toJson.compactPrint\n    AcknowledgementMessage.parse(m.serialize) shouldBe Success(m)\n  }\n\n  it should \"serialize and deserialize a Result message with Right result\" in {\n    val m = ResultMessage(TransactionId.testing, activation)\n    m.response shouldBe 'right\n    m.isSlotFree shouldBe empty\n    m.serialize shouldBe JsObject(\"transid\" -> m.transid.toJson, \"response\" -> m.response.right.get.toJson).compactPrint\n    AcknowledgementMessage.parse(m.serialize) shouldBe Success(m)\n  }\n\n  it should \"serialize and deserialize a Completion message\" in {\n    val m = CompletionMessage(\n      TransactionId.testing,\n      ActivationId.generate(),\n      Some(false),\n      InvokerInstanceId(0, userMemory = defaultUserMemory))\n    m.isSlotFree should not be empty\n    m.serialize shouldBe m.toJson.compactPrint\n    AcknowledgementMessage.parse(m.serialize) shouldBe Success(m)\n  }\n\n  it should \"serialize and deserialize a CombinedCompletionAndResultMessage\" in {\n    withClue(\"system error false and right\") {\n      val c = CombinedCompletionAndResultMessage(\n        TransactionId.testing,\n        activation,\n        InvokerInstanceId(0, userMemory = defaultUserMemory))\n      c.response shouldBe 'right\n      c.isSlotFree should not be empty\n      c.isSystemError shouldBe Some(false)\n      c.serialize shouldBe c.toJson.compactPrint\n      AcknowledgementMessage.parse(c.serialize) shouldBe Success(c)\n    }\n\n    withClue(\"system error true and right\") {\n      val response = ActivationResponse.whiskError(JsString(\"error\"))\n      val someActivation = activation.copy(response = response)\n      val c = CombinedCompletionAndResultMessage(\n        TransactionId.testing,\n        someActivation,\n        InvokerInstanceId(0, userMemory = defaultUserMemory))\n      c.response shouldBe 'right\n      c.isSlotFree should not be empty\n      c.isSystemError shouldBe Some(true)\n      c.serialize shouldBe c.toJson.compactPrint\n      AcknowledgementMessage.parse(c.serialize) shouldBe Success(c)\n    }\n\n    withClue(\"system error false and left\") {\n      val c = CombinedCompletionAndResultMessage(\n        TransactionId.testing,\n        activation,\n        InvokerInstanceId(0, userMemory = defaultUserMemory)).shrink\n      c.response shouldBe 'left\n      c.isSlotFree should not be empty\n      c.isSystemError shouldBe Some(false)\n      c.serialize shouldBe c.toJson.compactPrint\n      AcknowledgementMessage.parse(c.serialize) shouldBe Success(c)\n    }\n\n    withClue(\"system error true and left\") {\n      val response = ActivationResponse.whiskError(JsString(\"error\"))\n      val someActivation = activation.copy(response = response)\n      val c = CombinedCompletionAndResultMessage(\n        TransactionId.testing,\n        someActivation,\n        InvokerInstanceId(0, userMemory = defaultUserMemory)).shrink\n      c.response shouldBe 'left\n      c.isSlotFree should not be empty\n      c.isSystemError shouldBe Some(true)\n      c.serialize shouldBe c.toJson.compactPrint\n      AcknowledgementMessage.parse(c.serialize) shouldBe Success(c)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/docker/test/AkkaContainerClientTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker.test\n\nimport common.StreamLogging\nimport common.WskActorSystem\nimport java.nio.charset.StandardCharsets\nimport java.time.Instant\nimport org.apache.http.HttpRequest\nimport org.apache.http.HttpResponse\nimport org.apache.http.entity.StringEntity\nimport org.apache.http.localserver.LocalServerTestBase\nimport org.apache.http.protocol.HttpContext\nimport org.apache.http.protocol.HttpRequestHandler\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport scala.concurrent.Await\nimport scala.concurrent.TimeoutException\nimport scala.concurrent.duration._\nimport spray.json.JsObject\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.PekkoContainerClient\nimport org.apache.openwhisk.core.containerpool.ContainerHealthError\nimport org.apache.openwhisk.core.entity.ActivationResponse._\nimport org.apache.openwhisk.core.entity.size._\n\n/**\n * Unit tests for PekkoContainerClientTests which communicate with containers.\n */\n@RunWith(classOf[JUnitRunner])\nclass PekkoContainerClientTests\n    extends AnyFlatSpec\n    with Matchers\n    with BeforeAndAfter\n    with BeforeAndAfterAll\n    with StreamLogging\n    with WskActorSystem {\n\n  implicit val transid = TransactionId.testing\n  implicit val ec = actorSystem.dispatcher\n\n  var testHang: FiniteDuration = 0.second\n  var testStatusCode: Int = 200\n  var testResponse: String = null\n  var testConnectionFailCount: Int = 0\n\n  val mockServer = new LocalServerTestBase {\n    var failcount = 0\n    override def setUp() = {\n      super.setUp()\n      this.serverBootstrap\n        .registerHandler(\n          \"/init\",\n          new HttpRequestHandler() {\n            override def handle(request: HttpRequest, response: HttpResponse, context: HttpContext) = {\n              if (testHang.length > 0) {\n                Thread.sleep(testHang.toMillis)\n              }\n              if (testConnectionFailCount > 0 && failcount < testConnectionFailCount) {\n                failcount += 1\n                println(\"failing in test\")\n                throw new RuntimeException(\"failing...\")\n              }\n              response.setStatusCode(testStatusCode);\n              if (testResponse != null) {\n                response.setEntity(new StringEntity(testResponse, StandardCharsets.UTF_8))\n              }\n            }\n          })\n    }\n  }\n\n  mockServer.setUp()\n  val httpHost = mockServer.start()\n  val hostWithPort = s\"${httpHost.getHostName}:${httpHost.getPort}\"\n\n  before {\n    testHang = 0.second\n    testStatusCode = 200\n    testResponse = null\n    testConnectionFailCount = 0\n    stream.reset()\n  }\n\n  override def afterAll = {\n    mockServer.shutDown()\n  }\n\n  behavior of \"PekkoContainerClient\"\n\n  it should \"not wait longer than set timeout\" in {\n    val timeout = 5.seconds\n    val connection = new PekkoContainerClient(httpHost.getHostName, httpHost.getPort, timeout, 100)\n    testHang = timeout * 2\n    val start = Instant.now()\n    val result = Await.result(connection.post(\"/init\", JsObject.empty, 1.B, 1.B, retry = true), 10.seconds)\n\n    val end = Instant.now()\n    val waited = end.toEpochMilli - start.toEpochMilli\n    result shouldBe 'left\n    waited should be > timeout.toMillis\n    waited should be < (timeout * 2).toMillis\n  }\n\n  it should \"handle empty entity response\" in {\n    val timeout = 5.seconds\n    val connection = new PekkoContainerClient(httpHost.getHostName, httpHost.getPort, timeout, 100)\n    testStatusCode = 204\n    val result = Await.result(connection.post(\"/init\", JsObject.empty, 1.B, 1.B, retry = true), 10.seconds)\n    result shouldBe Left(NoResponseReceived())\n  }\n\n  it should \"retry till timeout on StreamTcpException\" in {\n    val timeout = 5.seconds\n    val connection = new PekkoContainerClient(\"0.0.0.0\", 12345, timeout, 100)\n    val start = Instant.now()\n    val result = Await.result(connection.post(\"/init\", JsObject.empty, 1.B, 1.B, retry = true), 10.seconds)\n    val end = Instant.now()\n    val waited = end.toEpochMilli - start.toEpochMilli\n    result match {\n      case Left(Timeout(_: TimeoutException)) => // good\n      case _                                  => fail(s\"$result was not a Timeout(TimeoutException)\")\n    }\n    waited should be > timeout.toMillis\n    waited should be < (timeout * 2).toMillis\n  }\n\n  it should \"throw ContainerHealthError on HttpHostConnectException if reschedule==true\" in {\n    val timeout = 5.seconds\n    val connection = new PekkoContainerClient(\"0.0.0.0\", 12345, timeout, 100)\n    assertThrows[ContainerHealthError] {\n      Await.result(connection.post(\"/run\", JsObject.empty, 1.B, 1.B, retry = false, reschedule = true), 10.seconds)\n    }\n  }\n\n  it should \"retry till success within timeout limit\" in {\n    val timeout = 5.seconds\n    val retryInterval = 500.milliseconds\n    val connection =\n      new PekkoContainerClient(httpHost.getHostName, httpHost.getPort, timeout, 100, retryInterval)\n    val start = Instant.now()\n    testConnectionFailCount = 5\n    testResponse = \"\"\n    val result = Await.result(connection.post(\"/init\", JsObject.empty, 1.B, 1.B, retry = true), 10.seconds)\n    val end = Instant.now()\n    val waited = end.toEpochMilli - start.toEpochMilli\n    result shouldBe Right {\n      ContainerResponse(true, \"\", None)\n    }\n\n    waited should be > (testConnectionFailCount * retryInterval).toMillis\n    waited should be < timeout.toMillis\n  }\n\n  it should \"not truncate responses within limit\" in {\n    val timeout = 1.minute.toMillis\n    val connection = new PekkoContainerClient(httpHost.getHostName, httpHost.getPort, timeout.millis, 100)\n    Seq(true, false).foreach { success =>\n      Seq(null, \"\", \"abc\", \"\"\"{\"a\":\"B\"}\"\"\", \"\"\"[\"a\", \"b\"]\"\"\").foreach { r =>\n        testStatusCode = if (success) 200 else 500\n        testResponse = r\n        val result = Await.result(connection.post(\"/init\", JsObject.empty, 50.B, 50.B, retry = true), 10.seconds)\n        result shouldBe Right {\n          ContainerResponse(okStatus = success, if (r != null) r else \"\", None)\n        }\n      }\n    }\n  }\n\n  it should \"truncate responses that exceed limit\" in {\n    val timeout = 1.minute.toMillis\n    val limit = 2.B\n    val truncationLimit = 1.B\n    val connection =\n      new PekkoContainerClient(httpHost.getHostName, httpHost.getPort, timeout.millis, 100)\n    Seq(true, false).foreach { success =>\n      Seq(\"abc\", \"\"\"{\"a\":\"B\"}\"\"\", \"\"\"[\"a\", \"b\"]\"\"\").foreach { r =>\n        testStatusCode = if (success) 200 else 500\n        testResponse = r\n        val result =\n          Await.result(connection.post(\"/init\", JsObject.empty, limit, truncationLimit, retry = true), 10.seconds)\n        result shouldBe Right {\n          ContainerResponse(okStatus = success, r.take(truncationLimit.toBytes.toInt), Some((r.length.B, limit)))\n        }\n      }\n    }\n  }\n\n  it should \"truncate large responses that exceed limit\" in {\n    val timeout = 1.minute.toMillis\n    //use a limit large enough to not fit into a single ByteString as response entity is parsed into multiple ByteStrings\n    //seems like this varies, but often is ~64k or ~128k\n    val limit = 300.KB\n    val truncationLimit = 299.B\n    val connection =\n      new PekkoContainerClient(httpHost.getHostName, httpHost.getPort, timeout.millis, 100)\n    Seq(true, false).foreach { success =>\n      // Generate a response that's 1MB\n      val response = \"0\" * 1024 * 1024\n      testStatusCode = if (success) 200 else 500\n      testResponse = response\n      val result =\n        Await.result(connection.post(\"/init\", JsObject.empty, limit, truncationLimit, retry = true), 10.seconds)\n      result shouldBe Right {\n        ContainerResponse(\n          okStatus = success,\n          response.take(truncationLimit.toBytes.toInt),\n          Some((response.length.B, limit)))\n      }\n\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/docker/test/ApacheBlockingContainerClientTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker.test\n\nimport java.nio.charset.StandardCharsets\nimport java.time.Instant\nimport scala.concurrent.duration._\nimport org.apache.http.HttpRequest\nimport org.apache.http.HttpResponse\nimport org.apache.http.entity.StringEntity\nimport org.apache.http.localserver.LocalServerTestBase\nimport org.apache.http.protocol.HttpContext\nimport org.apache.http.protocol.HttpRequestHandler\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.JsObject\nimport common.StreamLogging\nimport common.WskActorSystem\nimport org.apache.http.conn.HttpHostConnectException\nimport scala.concurrent.Await\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.ApacheBlockingContainerClient\nimport org.apache.openwhisk.core.containerpool.ContainerHealthError\nimport org.apache.openwhisk.core.containerpool.RetryableConnectionError\nimport org.apache.openwhisk.core.entity.ActivationResponse.Timeout\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.ActivationResponse._\n\n/**\n * Unit tests for ApacheBlockingContainerClient which communicate with containers.\n */\n@RunWith(classOf[JUnitRunner])\nclass ApacheBlockingContainerClientTests\n    extends AnyFlatSpec\n    with Matchers\n    with BeforeAndAfter\n    with BeforeAndAfterAll\n    with StreamLogging\n    with WskActorSystem {\n\n  implicit val transid = TransactionId.testing\n  implicit val ec = actorSystem.dispatcher\n\n  var testHang: FiniteDuration = 0.second\n  var testStatusCode: Int = 200\n  var testResponse: String = null\n\n  val mockServer = new LocalServerTestBase {\n    override def setUp() = {\n      super.setUp()\n      this.serverBootstrap.registerHandler(\"/init\", new HttpRequestHandler() {\n        override def handle(request: HttpRequest, response: HttpResponse, context: HttpContext) = {\n          if (testHang.length > 0) {\n            Thread.sleep(testHang.toMillis)\n          }\n          response.setStatusCode(testStatusCode);\n          if (testResponse != null) {\n            response.setEntity(new StringEntity(testResponse, StandardCharsets.UTF_8))\n          }\n        }\n      })\n    }\n  }\n\n  mockServer.setUp()\n  val httpHost = mockServer.start()\n  val hostWithPort = s\"${httpHost.getHostName}:${httpHost.getPort}\"\n\n  before {\n    testHang = 0.second\n    testStatusCode = 200\n    testResponse = null\n    stream.reset()\n  }\n\n  override def afterAll = {\n    mockServer.shutDown()\n  }\n\n  behavior of \"ApacheBlockingContainerClient\"\n\n  it should \"not wait longer than set timeout\" in {\n    val timeout = 5.seconds\n    val connection = new ApacheBlockingContainerClient(hostWithPort, timeout)\n    testHang = timeout * 2\n    val start = Instant.now()\n    val result = Await.result(connection.post(\"/init\", JsObject.empty, 1.B, 1.B, retry = true), 10.seconds)\n\n    val end = Instant.now()\n    val waited = end.toEpochMilli - start.toEpochMilli\n    result shouldBe 'left\n    waited should be > timeout.toMillis\n    waited should be < (timeout * 2).toMillis\n  }\n\n  it should \"handle empty entity response\" in {\n    val timeout = 5.seconds\n    val connection = new ApacheBlockingContainerClient(hostWithPort, timeout)\n    testStatusCode = 204\n    val result = Await.result(connection.post(\"/init\", JsObject.empty, 1.B, 1.B, retry = true), 10.seconds)\n    result shouldBe Left(NoResponseReceived())\n  }\n\n  it should \"retry till timeout on HttpHostConnectException\" in {\n    val timeout = 5.seconds\n    val badHostAndPort = \"0.0.0.0:12345\"\n    val connection = new ApacheBlockingContainerClient(badHostAndPort, timeout)\n    testStatusCode = 204\n    val start = Instant.now()\n    val result = Await.result(connection.post(\"/init\", JsObject.empty, 1.B, 1.B, retry = true), 10.seconds)\n    val end = Instant.now()\n    val waited = end.toEpochMilli - start.toEpochMilli\n    result match {\n      case Left(Timeout(RetryableConnectionError(_: HttpHostConnectException))) => // all good\n      case _ =>\n        fail(s\"$result was not a Timeout(RetryableConnectionError(HttpHostConnectException)))\")\n    }\n\n    waited should be > timeout.toMillis\n    waited should be < (timeout * 2).toMillis\n  }\n\n  it should \"throw ContainerHealthError on HttpHostConnectException if reschedule==true\" in {\n    val timeout = 5.seconds\n    val badHostAndPort = \"0.0.0.0:12345\"\n    val connection = new ApacheBlockingContainerClient(badHostAndPort, timeout)\n    assertThrows[ContainerHealthError] {\n      Await.result(connection.post(\"/run\", JsObject.empty, 1.B, 1.B, retry = false, reschedule = true), 10.seconds)\n    }\n  }\n\n  it should \"not truncate responses within limit\" in {\n    val timeout = 1.minute.toMillis\n    val connection = new ApacheBlockingContainerClient(hostWithPort, timeout.millis)\n    Seq(true, false).foreach { success =>\n      Seq(null, \"\", \"abc\", \"\"\"{\"a\":\"B\"}\"\"\", \"\"\"[\"a\", \"b\"]\"\"\").foreach { r =>\n        testStatusCode = if (success) 200 else 500\n        testResponse = r\n        val result = Await.result(connection.post(\"/init\", JsObject.empty, 50.B, 50.B, retry = true), 10.seconds)\n        result shouldBe Right {\n          ContainerResponse(okStatus = success, if (r != null) r else \"\", None)\n        }\n      }\n    }\n  }\n\n  it should \"truncate responses that exceed limit\" in {\n    val timeout = 1.minute.toMillis\n    val limit = 2.B\n    val truncationLimit = 1.B\n    val connection = new ApacheBlockingContainerClient(hostWithPort, timeout.millis)\n    Seq(true, false).foreach { success =>\n      Seq(\"abc\", \"\"\"{\"a\":\"B\"}\"\"\", \"\"\"[\"a\", \"b\"]\"\"\").foreach { r =>\n        testStatusCode = if (success) 200 else 500\n        testResponse = r\n        val result =\n          Await.result(connection.post(\"/init\", JsObject.empty, limit, truncationLimit, retry = true), 10.seconds)\n        result shouldBe Right {\n          ContainerResponse(okStatus = success, r.take(truncationLimit.toBytes.toInt), Some((r.length.B, limit)))\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/docker/test/DockerClientTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker.test\n\nimport org.apache.pekko.actor.ActorSystem\nimport java.util.concurrent.Semaphore\n\nimport scala.concurrent.Await\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.concurrent.Promise\nimport scala.util.Success\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.concurrent.Eventually\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.time.{Seconds, Span}\nimport common.{StreamLogging, WskActorSystem}\nimport org.apache.openwhisk.common.LogMarker\nimport org.apache.openwhisk.common.LoggingMarkers.INVOKER_DOCKER_CMD\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.ContainerAddress\nimport org.apache.openwhisk.core.containerpool.ContainerId\nimport org.apache.openwhisk.core.containerpool.docker._\nimport org.apache.openwhisk.utils.retry\n\n@RunWith(classOf[JUnitRunner])\nclass DockerClientTests\n    extends AnyFlatSpec\n    with Matchers\n    with StreamLogging\n    with BeforeAndAfterEach\n    with Eventually\n    with WskActorSystem {\n\n  override def beforeEach = stream.reset()\n\n  implicit override val patienceConfig = PatienceConfig(timeout = scaled(Span(5, Seconds)))\n\n  implicit val transid = TransactionId.testing\n  val id = ContainerId(\"55db56ee082239428b27d3728b4dd324c09068458aad9825727d5bfc1bba6d52\")\n\n  val commandTimeout = 500.milliseconds\n  def await[A](f: Future[A], timeout: FiniteDuration = commandTimeout) = Await.result(f, timeout)\n\n  val dockerCommand = \"docker\"\n\n  /** Returns a DockerClient with a mocked result for 'executeProcess' */\n  def dockerClient(execResult: => Future[String]) = new DockerClient()(global) {\n    override val dockerCmd = Seq(dockerCommand)\n    override def getClientVersion() = \"mock-test-client\"\n    override def executeProcess(args: Seq[String], timeout: Duration)(implicit ec: ExecutionContext, as: ActorSystem) =\n      execResult\n  }\n\n  behavior of \"DockerContainerId\"\n\n  it should \"convert a proper container ID\" in {\n    DockerContainerId.parse(id.asString) shouldBe Success(id)\n  }\n\n  it should \"reject improper container IDs with IllegalArgumentException\" in {\n    def verifyFailure(improperId: String) = {\n      val iae = the[IllegalArgumentException] thrownBy DockerContainerId.parse(improperId).get\n      iae.getMessage should include(improperId)\n    }\n\n    Seq[(String, String)](\n      (\"\", \"String empty (too short)\"),\n      (\"1\" * 63, \"String too short\"),\n      (\"1\" * 65, \"String too long\"),\n      ((\"1\" * 63) + \"x\", \"Improper characters\"),\n      (\"abcxdef\", \"Improper characters and too short\")).foreach {\n      case (improperId, clue) =>\n        withClue(s\"${clue} - length('${improperId}') = ${improperId.length}: \") {\n          verifyFailure(improperId)\n        }\n    }\n  }\n\n  behavior of \"DockerClient\"\n\n  it should \"return a list of containers and pass down the correct arguments when using 'ps'\" in {\n    val containers = Seq(\"1\", \"2\", \"3\")\n    val dc = dockerClient { Future.successful(containers.mkString(\"\\n\")) }\n\n    val filters = Seq(\"name\" -> \"wsk\", \"label\" -> \"docker\")\n    await(dc.ps(filters, all = true)) shouldBe containers.map(ContainerId.apply)\n\n    val firstLine = logLines.head\n    firstLine should include(s\"${dockerCommand} ps\")\n    firstLine should include(\"--quiet\")\n    firstLine should include(\"--no-trunc\")\n    firstLine should include(\"--all\")\n    filters.foreach {\n      case (k, v) => firstLine should include(s\"--filter $k=$v\")\n    }\n  }\n\n  it should \"throw NoSuchElementException if specified network does not exist when using 'inspectIPAddress'\" in {\n    val dc = dockerClient { Future.successful(\"<no value>\") }\n\n    a[NoSuchElementException] should be thrownBy await(dc.inspectIPAddress(id, \"foo network\"))\n  }\n\n  it should \"collapse multiple parallel pull calls into just one\" in {\n    // Delay execution of the pull command\n    val pullPromise = Promise[String]()\n    var commandsRun = 0\n    val dc = dockerClient {\n      commandsRun += 1\n      pullPromise.future\n    }\n\n    val image = \"testimage\"\n\n    // Pull first, command should be run\n    dc.pull(image)\n    commandsRun shouldBe 1\n\n    // Pull again, command should not be run\n    dc.pull(image)\n    commandsRun shouldBe 1\n\n    // Finish the pulls above\n    pullPromise.success(\"pulled\")\n\n    retry {\n      // Pulling again should execute the command again\n      await(dc.pull(image))\n      commandsRun shouldBe 2\n    }\n  }\n\n  it should \"properly clean up failed pulls\" in {\n    // Delay execution of the pull command\n    val pullPromise = Promise[String]()\n    var commandsRun = 0\n    val dc = dockerClient {\n      commandsRun += 1\n      pullPromise.future\n    }\n\n    val image = \"testimage\"\n\n    // Pull first, command should be run\n    dc.pull(image)\n    commandsRun shouldBe 1\n\n    // Pull again, command should not be run\n    dc.pull(image)\n    commandsRun shouldBe 1\n\n    // Finish the pulls above\n    pullPromise.failure(new Throwable())\n\n    retry {\n      // Pulling again should execute the command again\n      Await.ready(dc.pull(image), commandTimeout)\n      commandsRun shouldBe 2\n    }\n  }\n\n  it should \"limit the number of concurrent docker run invocations\" in {\n    // Delay execution of Docker run command\n    val firstRunPromise = Promise[String]()\n\n    val firstContainerId = ContainerId(\"1\" * 64)\n    val secondContainerId = ContainerId(\"2\" * 64)\n\n    var runCmdCount = 0\n    val dc = new DockerClient()(global) {\n      override val dockerCmd = Seq(dockerCommand)\n      override def getClientVersion() = \"mock-test-client\"\n      override def executeProcess(args: Seq[String], timeout: Duration)(implicit ec: ExecutionContext,\n                                                                        as: ActorSystem) = {\n        runCmdCount += 1\n        runCmdCount match {\n          case 1 => firstRunPromise.future\n          case 2 => Future.successful(secondContainerId.asString)\n          case _ => Future.failed(new Throwable())\n        }\n      }\n      // Need to override the semaphore, otherwise the tested code will still\n      // create the semaphore with the original value of maxParallelRuns.\n      override val maxParallelRuns = 1\n      override val runSemaphore = new Semaphore( /* permits= */ maxParallelRuns, /* fair= */ true)\n    }\n\n    val image = \"image\"\n    val args = Seq(\"args\")\n\n    val firstRunResult = dc.run(image, args)\n    val secondRunResult = dc.run(image, args)\n\n    // The tested code won't reach the mocked executeProcess() and thus, increase runCmdCount,\n    // until at least one Future is successfully completed. For this reason, it takes\n    // some time until the following matcher is successful.\n    eventually { runCmdCount shouldBe 1 }\n\n    // Complete the first Docker run command so that the second is eligible to run\n    firstRunPromise.success(firstContainerId.asString)\n\n    // Cannot assert that the first Docker run always obtains the first container because\n    // the tested code uses Futures so that sequence may differ from test run to test run.\n    val firstResultContainerId = await(firstRunResult)\n\n    // Now, second command should be complete\n    eventually { runCmdCount shouldBe 2 }\n\n    val secondResultContainerId = await(secondRunResult)\n    Set(firstResultContainerId, secondResultContainerId) should contain theSameElementsAs Set(\n      firstContainerId,\n      secondContainerId)\n  }\n\n  it should \"tolerate docker run errors when limiting the number of concurrent docker run invocations\" in {\n    val secondContainerId = ContainerId(\"2\" * 64)\n\n    var runCmdCount = 0\n    val dc = new DockerClient()(global) {\n      override val dockerCmd = Seq(dockerCommand)\n      override def getClientVersion() = \"mock-test-client\"\n      override def executeProcess(args: Seq[String], timeout: Duration)(implicit ec: ExecutionContext,\n                                                                        as: ActorSystem) = {\n        runCmdCount += 1\n        println(s\"runCmdCount=${runCmdCount}, args.last=${args.last}\")\n        runCmdCount match {\n          case 1 => Future.failed(ProcessUnsuccessfulException(ExitStatus(1), \"\", \"\"))\n          case 2 => Future.successful(secondContainerId.asString)\n          case _ => Future.failed(new Throwable())\n        }\n      }\n      // Need to override the semaphore, otherwise the tested code will still\n      // create the semaphore with the original value of maxParallelRuns.\n      override val maxParallelRuns = 1\n      override val runSemaphore = new Semaphore( /* permits= */ maxParallelRuns, /* fair= */ true)\n    }\n\n    val image = \"image\"\n    val args = Seq(\"args\")\n\n    // Kick off the first Docker run command - it will fail.\n    val firstRunResult = dc.run(image, args)\n\n    an[Exception] should be thrownBy await(firstRunResult)\n    runCmdCount shouldBe 1\n\n    // Now kick off the second Docker run command - it is expected to succeed.\n    // If this command completes without timeout, the concurrency limit properly\n    // deals with errors.\n    val secondRunResult = dc.run(image, args)\n\n    await(secondRunResult) shouldBe secondContainerId\n    runCmdCount shouldBe 2\n  }\n\n  it should \"write proper log markers on a successful command\" in {\n    // a dummy string works here as we do not assert any output\n    // from the methods below\n    val stdout = \"stdout\"\n    val dc = dockerClient { Future.successful(stdout) }\n\n    /** Awaits the command and checks for proper logging. */\n    def runAndVerify(f: Future[_], cmd: String, args: Seq[String] = Seq.empty[String]) = {\n      val result = await(f)\n\n      logLines.head should include((Seq(dockerCommand, cmd) ++ args).mkString(\" \"))\n\n      val start = LogMarker.parse(logLines.head)\n      start.token.toStringWithSubAction shouldBe INVOKER_DOCKER_CMD(cmd).toStringWithSubAction\n\n      val end = LogMarker.parse(logLines.last)\n      end.token.toStringWithSubAction shouldBe INVOKER_DOCKER_CMD(cmd).asFinish.toStringWithSubAction\n\n      stream.reset()\n      result\n    }\n\n    runAndVerify(dc.pause(id), \"pause\", Seq(id.asString))\n    runAndVerify(dc.unpause(id), \"unpause\", Seq(id.asString))\n    runAndVerify(dc.rm(id), \"rm\", Seq(\"-f\", id.asString))\n    runAndVerify(dc.ps(), \"ps\")\n\n    val network = \"userland\"\n    val inspectArgs = Seq(\"--format\", s\"{{.NetworkSettings.Networks.${network}.IPAddress}}\", id.asString)\n    runAndVerify(dc.inspectIPAddress(id, network), \"inspect\", inspectArgs) shouldBe ContainerAddress(stdout)\n\n    val image = \"image\"\n    val runArgs = Seq(\"--memory\", \"256m\", \"--cpushares\", \"1024\")\n    runAndVerify(dc.run(image, runArgs), \"run\", Seq(\"-d\") ++ runArgs ++ Seq(image)) shouldBe ContainerId(stdout)\n    runAndVerify(dc.pull(image), \"pull\", Seq(image))\n  }\n\n  it should \"write proper log markers on a failing command\" in {\n    val dc = dockerClient { Future.failed(new RuntimeException()) }\n\n    /** Awaits the command, asserts the exception and checks for proper logging. */\n    def runAndVerify(f: Future[_], cmd: String) = {\n      a[RuntimeException] should be thrownBy await(f)\n\n      val start = LogMarker.parse(logLines.head)\n      start.token.toStringWithSubAction shouldBe INVOKER_DOCKER_CMD(cmd).toStringWithSubAction\n\n      val end = LogMarker.parse(logLines.last)\n      end.token.toStringWithSubAction shouldBe INVOKER_DOCKER_CMD(cmd).asError.toStringWithSubAction\n\n      stream.reset()\n    }\n\n    runAndVerify(dc.pause(id), \"pause\")\n    runAndVerify(dc.unpause(id), \"unpause\")\n    runAndVerify(dc.rm(id), \"rm\")\n    runAndVerify(dc.ps(), \"ps\")\n    runAndVerify(dc.inspectIPAddress(id, \"network\"), \"inspect\")\n    runAndVerify(dc.run(\"image\"), \"run\")\n    runAndVerify(dc.pull(\"image\"), \"pull\")\n  }\n\n  it should \"fail with BrokenDockerContainer when run returns with exit status 125 and a container ID\" in {\n    val dc = dockerClient {\n      Future.failed(\n        ProcessUnsuccessfulException(\n          exitStatus = ExitStatus(125),\n          stdout = id.asString,\n          stderr =\n            \"\"\"/usr/bin/docker: Error response from daemon: mkdir /var/run/docker.1.1/libcontainerd.1.1/55db56ee082239428b27d3728b4dd324c09068458aad9825727d5bfc1bba6d52: no space left on device.\"\"\"))\n    }\n    val bdc = the[BrokenDockerContainer] thrownBy await(dc.run(\"image\", Seq.empty))\n    bdc.id shouldBe id\n  }\n\n  it should \"fail with ProcessRunningException when run returns with exit code !=125, no container ID or timeout\" in {\n    def runAndVerify(pre: ProcessRunningException, clue: String) = {\n      val dc = dockerClient { Future.failed(pre) }\n      withClue(s\"${clue} - exitStatus = ${pre.exitStatus}, stdout = '${pre.stdout}', stderr = '${pre.stderr}': \") {\n        the[ProcessRunningException] thrownBy await(dc.run(\"image\", Seq.empty)) shouldBe pre\n      }\n    }\n\n    Seq[(ProcessRunningException, String)](\n      (ProcessUnsuccessfulException(ExitStatus(125), \"\", \"Unknown flag: --foo\"), \"No container ID\"),\n      (ProcessUnsuccessfulException(ExitStatus(1), \"\", \"\"), \"Exit code not 125 and no container ID\"),\n      (ProcessTimeoutException(1.second, ExitStatus(125), id.asString, \"\"), \"Timeout instead of unsuccessful command\"))\n      .foreach {\n        case (pre, clue) => runAndVerify(pre, clue)\n      }\n  }\n\n  it should \"fail with BrokenDockerContainer when run returns with exit status 127 and a container ID\" in {\n    val dc = dockerClient {\n      Future.failed(\n        ProcessUnsuccessfulException(\n          exitStatus = ExitStatus(127),\n          stdout = id.asString,\n          stderr = \"\"\"Error response from daemon: OCI runtime create failed: executable file not found\"\"\"))\n    }\n    val bdc = the[BrokenDockerContainer] thrownBy await(dc.run(\"image\", Seq.empty))\n    bdc.id shouldBe id\n  }\n\n  it should \"fail with ProcessTimeoutException when command times out\" in {\n    val expectedPTE =\n      ProcessTimeoutException(timeout = 10.seconds, exitStatus = ExitStatus(143), stdout = \"stdout\", stderr = \"stderr\")\n    val dc = dockerClient {\n      Future.failed(expectedPTE)\n    }\n    Seq[(Future[_], String)](\n      (dc.run(\"image\", Seq.empty), \"run\"),\n      (dc.inspectIPAddress(id, \"network\"), \"inspectIPAddress\"),\n      (dc.pause(id), \"pause\"),\n      (dc.unpause(id), \"unpause\"),\n      (dc.rm(id), \"rm\"),\n      (dc.ps(), \"ps\"),\n      (dc.pull(\"image\"), \"pull\"),\n      (dc.isOomKilled(id), \"isOomKilled\"))\n      .foreach {\n        case (cmd, clue) =>\n          withClue(s\"command '$clue' - \") {\n            the[ProcessTimeoutException] thrownBy await(cmd) shouldBe expectedPTE\n          }\n      }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/docker/test/DockerClientWithFileAccessTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker.test\n\nimport java.io.File\n\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.concurrent.Await\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.Future\nimport scala.concurrent.duration.{Duration, DurationInt, FiniteDuration}\nimport scala.language.reflectiveCalls\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.matchers.should.Matchers\nimport common.{StreamLogging, WskActorSystem}\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.ContainerId\nimport org.apache.openwhisk.core.containerpool.ContainerAddress\nimport org.apache.openwhisk.core.containerpool.docker.DockerClientWithFileAccess\n\n@RunWith(classOf[JUnitRunner])\nclass DockerClientWithFileAccessTestsIp\n    extends AnyFlatSpec\n    with Matchers\n    with StreamLogging\n    with BeforeAndAfterEach\n    with WskActorSystem {\n\n  override def beforeEach = stream.reset()\n\n  implicit val transid = TransactionId.testing\n  val id = ContainerId(\"Id\")\n\n  def await[A](f: Future[A], timeout: FiniteDuration = 500.milliseconds) = Await.result(f, timeout)\n\n  val dockerCommand = \"docker\"\n  val networkInConfigFile = \"networkConfig\"\n  val networkInDockerInspect = \"networkInspect\"\n  val ipInConfigFile = ContainerAddress(\"10.0.0.1\")\n  val ipInDockerInspect = ContainerAddress(\"10.0.0.2\")\n  val dockerConfig =\n    JsObject(\n      \"NetworkSettings\" ->\n        JsObject(\n          \"Networks\" ->\n            JsObject(networkInConfigFile ->\n              JsObject(\"IPAddress\" -> JsString(ipInConfigFile.host)))))\n\n  /** Returns a DockerClient with mocked results */\n  def dockerClient(execResult: Future[String] = Future.successful(ipInDockerInspect.host),\n                   readResult: Future[JsObject] = Future.successful(dockerConfig)) =\n    new DockerClientWithFileAccess()(global) {\n      override val dockerCmd = Seq(dockerCommand)\n      override def getClientVersion() = \"mock-test-client\"\n      override def executeProcess(args: Seq[String], timeout: Duration)(implicit ec: ExecutionContext,\n                                                                        as: ActorSystem) = execResult\n      override def configFileContents(configFile: File) = readResult\n      // Make protected ipAddressFromFile available for testing - requires reflectiveCalls\n      def publicIpAddressFromFile(id: ContainerId, network: String): Future[ContainerAddress] =\n        ipAddressFromFile(id, network)\n    }\n\n  behavior of \"DockerClientWithFileAccess - ipAddressFromFile\"\n\n  it should \"throw NoSuchElementException if specified network is not in configuration file\" in {\n    val dc = dockerClient()\n\n    a[NoSuchElementException] should be thrownBy await(dc.publicIpAddressFromFile(id, \"foo network\"))\n  }\n\n  behavior of \"DockerClientWithFileAccess - inspectIPAddress\"\n\n  it should \"read from config file\" in {\n    val dc = dockerClient()\n\n    await(dc.inspectIPAddress(id, networkInConfigFile)) shouldBe ipInConfigFile\n    logLines.foreach { _ should not include (s\"${dockerCommand} inspect\") }\n  }\n\n  it should \"fall back to 'docker inspect' if config file cannot be read\" in {\n    val dc = dockerClient(readResult = Future.failed(new RuntimeException()))\n\n    await(dc.inspectIPAddress(id, networkInDockerInspect)) shouldBe ipInDockerInspect\n    logLines.head should include(s\"${dockerCommand} inspect\")\n  }\n\n  it should \"throw NoSuchElementException if specified network does not exist\" in {\n    val dc = dockerClient(execResult = Future.successful(\"<no value>\"))\n\n    a[NoSuchElementException] should be thrownBy await(dc.inspectIPAddress(id, \"foo network\"))\n  }\n}\n\n@RunWith(classOf[JUnitRunner])\nclass DockerClientWithFileAccessTestsOom\n    extends AnyFlatSpec\n    with Matchers\n    with StreamLogging\n    with BeforeAndAfterEach\n    with WskActorSystem {\n  override def beforeEach = stream.reset()\n\n  implicit val transid = TransactionId.testing\n  val id = ContainerId(\"Id\")\n\n  def await[A](f: Future[A], timeout: FiniteDuration = 500.milliseconds) = Await.result(f, timeout)\n\n  def dockerClient(readResult: Future[JsObject]) =\n    new DockerClientWithFileAccess()(global) {\n      override val dockerCmd = Seq(\"docker\")\n      override def getClientVersion() = \"mock-test-client\"\n      override def configFileContents(configFile: File) = readResult\n    }\n\n  def stateObject(oom: Boolean) = JsObject(\"State\" -> JsObject(\"OOMKilled\" -> oom.toJson))\n\n  behavior of \"DockerClientWithFileAccess - isOomKilled\"\n\n  it should \"return the state of the container respectively\" in {\n    val dcTrue = dockerClient(Future.successful(stateObject(true)))\n    await(dcTrue.isOomKilled(id)) shouldBe true\n\n    val dcFalse = dockerClient(Future.successful(stateObject(false)))\n    await(dcFalse.isOomKilled(id)) shouldBe false\n  }\n\n  it should \"default to 'false' if the json structure is unparseable\" in {\n    val dc = dockerClient(Future.successful(JsObject.empty))\n    await(dc.isOomKilled(id)) shouldBe false\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/docker/test/DockerContainerFactoryTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker.test\n\nimport common.StreamLogging\nimport common.TimingHelpers\nimport common.WskActorSystem\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.Await\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.core.containerpool.{\n  ContainerAddress,\n  ContainerArgsConfig,\n  ContainerId,\n  RuntimesRegistryConfig\n}\nimport org.apache.openwhisk.core.containerpool.docker.DockerApiWithFileAccess\nimport org.apache.openwhisk.core.containerpool.docker.DockerContainerFactory\nimport org.apache.openwhisk.core.containerpool.docker.DockerContainerFactoryConfig\nimport org.apache.openwhisk.core.containerpool.docker.RuncApi\nimport org.apache.openwhisk.core.entity.{ByteSize, ExecManifest, InvokerInstanceId}\nimport org.apache.openwhisk.core.entity.size._\nimport pureconfig._\nimport pureconfig.generic.auto._\n\n@RunWith(classOf[JUnitRunner])\nclass DockerContainerFactoryTests\n    extends AnyFlatSpec\n    with Matchers\n    with MockFactory\n    with StreamLogging\n    with BeforeAndAfterEach\n    with WskActorSystem\n    with TimingHelpers {\n\n  implicit val config = new WhiskConfig(ExecManifest.requiredProperties)\n  ExecManifest.initialize(config) should be a 'success\n  val runtimesRegistryConfig = loadConfigOrThrow[RuntimesRegistryConfig](ConfigKeys.runtimesRegistry)\n  val userImagesRegistryConfig = loadConfigOrThrow[RuntimesRegistryConfig](ConfigKeys.userImagesRegistry)\n\n  behavior of \"DockerContainerFactory\"\n\n  val defaultUserMemory: ByteSize = 1024.MB\n\n  it should \"set the docker run args based on ContainerArgsConfig\" in {\n\n    val image = ExecManifest.runtimesManifest.manifests(\"nodejs:20\").image\n\n    implicit val tid = TransactionId.testing\n    val dockerApiStub = mock[DockerApiWithFileAccess]\n    //setup run expectation\n    (dockerApiStub\n      .run(_: String, _: Seq[String])(_: TransactionId))\n      .expects(\n        image.resolveImageName(Some(runtimesRegistryConfig.url)),\n        List(\n          \"--cpu-shares\",\n          \"32\", //should be calculated as 1024/(numcore * sharefactor) via ContainerFactory.cpuShare\n          \"--memory\",\n          \"10m\",\n          \"--memory-swap\",\n          \"10m\",\n          \"--network\",\n          \"net1\",\n          \"-e\",\n          \"__OW_API_HOST=\",\n          \"-e\",\n          \"k1=v1\",\n          \"-e\",\n          \"k2=v2\",\n          \"-e\",\n          \"k3=\",\n          \"--dns\",\n          \"dns1\",\n          \"--dns\",\n          \"dns2\",\n          \"--name\",\n          \"testContainer\",\n          \"--extra1\",\n          \"e1\",\n          \"--extra1\",\n          \"e2\",\n          \"--extra2\",\n          \"e3\",\n          \"--extra2\",\n          \"e4\"),\n        *)\n      .returning(Future.successful { ContainerId(\"fakecontainerid\") })\n    //setup inspect expectation\n    (dockerApiStub\n      .inspectIPAddress(_: ContainerId, _: String)(_: TransactionId))\n      .expects(ContainerId(\"fakecontainerid\"), \"net1\", *)\n      .returning(Future.successful { ContainerAddress(\"1.2.3.4\", 1234) })\n    //setup rm expectation\n    (dockerApiStub\n      .rm(_: ContainerId)(_: TransactionId))\n      .expects(ContainerId(\"fakecontainerid\"), *)\n      .returning(Future.successful(()))\n    //setup clientVersion exceptation\n    (dockerApiStub.clientVersion _)\n      .expects()\n      .returning(\"mock_test_client\")\n\n    val factory =\n      new DockerContainerFactory(\n        InvokerInstanceId(0, userMemory = defaultUserMemory),\n        Map.empty,\n        ContainerArgsConfig(\n          \"net1\",\n          Seq(\"dns1\", \"dns2\"),\n          Seq.empty,\n          Seq.empty,\n          Seq(\"k1=v1\", \"k2=v2\", \"k3\"),\n          Map(\"extra1\" -> Set(\"e1\", \"e2\"), \"extra2\" -> Set(\"e3\", \"e4\"))),\n        runtimesRegistryConfig,\n        userImagesRegistryConfig,\n        DockerContainerFactoryConfig(true))(actorSystem, executionContext, logging, dockerApiStub, mock[RuncApi])\n\n    val cf = factory.createContainer(tid, \"testContainer\", image, false, 10.MB, 32, None)\n\n    val c = Await.result(cf, 5000.milliseconds)\n\n    Await.result(c.destroy(), 500.milliseconds)\n\n  }\n\n  it should \"set the docker run args with cpu limit when provided\" in {\n\n    val image = ExecManifest.runtimesManifest.manifests(\"nodejs:20\").image\n\n    implicit val tid = TransactionId.testing\n    val dockerApiStub = mock[DockerApiWithFileAccess]\n    //setup run expectation\n    (dockerApiStub\n      .run(_: String, _: Seq[String])(_: TransactionId))\n      .expects(\n        image.resolveImageName(Some(runtimesRegistryConfig.url)),\n        List(\n          \"--cpu-shares\",\n          \"32\", //should be calculated as 1024/(numcore * sharefactor) via ContainerFactory.cpuShare\n          \"--memory\",\n          \"10m\",\n          \"--memory-swap\",\n          \"10m\",\n          \"--network\",\n          \"net1\",\n          \"-e\",\n          \"__OW_API_HOST=\",\n          \"-e\",\n          \"k1=v1\",\n          \"-e\",\n          \"k2=v2\",\n          \"-e\",\n          \"k3=\",\n          \"--dns\",\n          \"dns1\",\n          \"--dns\",\n          \"dns2\",\n          \"--name\",\n          \"testContainer\",\n          \"--cpus\",\n          \"0.5\",\n          \"--extra1\",\n          \"e1\",\n          \"--extra1\",\n          \"e2\",\n          \"--extra2\",\n          \"e3\",\n          \"--extra2\",\n          \"e4\"),\n        *)\n      .returning(Future.successful { ContainerId(\"fakecontainerid\") })\n    //setup inspect expectation\n    (dockerApiStub\n      .inspectIPAddress(_: ContainerId, _: String)(_: TransactionId))\n      .expects(ContainerId(\"fakecontainerid\"), \"net1\", *)\n      .returning(Future.successful { ContainerAddress(\"1.2.3.4\", 1234) })\n    //setup rm expectation\n    (dockerApiStub\n      .rm(_: ContainerId)(_: TransactionId))\n      .expects(ContainerId(\"fakecontainerid\"), *)\n      .returning(Future.successful(()))\n    //setup clientVersion exceptation\n    (dockerApiStub.clientVersion _)\n      .expects()\n      .returning(\"mock_test_client\")\n\n    val factory =\n      new DockerContainerFactory(\n        InvokerInstanceId(0, userMemory = defaultUserMemory),\n        Map.empty,\n        ContainerArgsConfig(\n          \"net1\",\n          Seq(\"dns1\", \"dns2\"),\n          Seq.empty,\n          Seq.empty,\n          Seq(\"k1=v1\", \"k2=v2\", \"k3\"),\n          Map(\"extra1\" -> Set(\"e1\", \"e2\"), \"extra2\" -> Set(\"e3\", \"e4\"))),\n        runtimesRegistryConfig,\n        userImagesRegistryConfig,\n        DockerContainerFactoryConfig(true))(actorSystem, executionContext, logging, dockerApiStub, mock[RuncApi])\n\n    val cf = factory.createContainer(tid, \"testContainer\", image, false, 10.MB, 32, Some(0.5))\n\n    val c = Await.result(cf, 5000.milliseconds)\n\n    Await.result(c.destroy(), 500.milliseconds)\n\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/docker/test/DockerContainerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker.test\n\nimport java.io.IOException\nimport java.time.Instant\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.util.ByteString\nimport common.TimingHelpers\n\nimport scala.collection.mutable\nimport scala.concurrent.Await\nimport scala.concurrent.duration._\nimport scala.concurrent.Future\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.apache.openwhisk.core.containerpool.logging.{DockerToActivationLogStore, LogLine}\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.matchers.should.Matchers\nimport common.{StreamLogging, WskActorSystem}\nimport spray.json._\nimport org.apache.openwhisk.common.LoggingMarkers._\nimport org.apache.openwhisk.common.LogMarker\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.containerpool.docker._\nimport org.apache.openwhisk.core.entity.{ActivationResponse, ByteSize}\nimport org.apache.openwhisk.core.entity.ActivationResponse.ContainerResponse\nimport org.apache.openwhisk.core.entity.ActivationResponse.Timeout\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\nimport DockerContainerTests._\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\n\nobject DockerContainerTests {\n\n  /** Awaits the given future, throws the exception enclosed in Failure. */\n  def await[A](f: Future[A], timeout: FiniteDuration = 500.milliseconds) = Await.result[A](f, timeout)\n\n  /** Creates an interval starting at EPOCH with the given duration. */\n  def intervalOf(duration: FiniteDuration) = Interval(Instant.EPOCH, Instant.ofEpochMilli(duration.toMillis))\n\n  def toRawLog(log: Seq[LogLine], appendSentinel: Boolean = true): ByteString = {\n    val appendedLog = if (appendSentinel) {\n      val lastTime = log.lastOption.map { case LogLine(time, _, _) => time }.getOrElse(Instant.EPOCH.toString)\n      log :+\n        LogLine(lastTime, \"stderr\", s\"${Container.ACTIVATION_LOG_SENTINEL}\\n\") :+\n        LogLine(lastTime, \"stdout\", s\"${Container.ACTIVATION_LOG_SENTINEL}\\n\")\n    } else {\n      log\n    }\n    ByteString(appendedLog.map(_.toJson.compactPrint).mkString(\"\", \"\\n\", \"\\n\"))\n  }\n}\n\n/**\n * Unit tests for ContainerPool schedule\n */\n@RunWith(classOf[JUnitRunner])\nclass DockerContainerTests\n    extends AnyFlatSpec\n    with Matchers\n    with MockFactory\n    with StreamLogging\n    with BeforeAndAfterEach\n    with WskActorSystem\n    with TimingHelpers {\n\n  override def beforeEach() = {\n    stream.reset()\n  }\n\n  /** Reads logs into memory and awaits them */\n  def awaitLogs(source: Source[ByteString, Any], timeout: FiniteDuration = 500.milliseconds): Vector[String] =\n    Await.result(source.via(DockerToActivationLogStore.toFormattedString).runWith(Sink.seq[String]), timeout).toVector\n\n  val containerId = ContainerId(\"id\")\n\n  /**\n   * Constructs a testcontainer with overridden IO methods. Results of the override can be provided\n   * as parameters.\n   */\n  def dockerContainer(id: ContainerId = containerId, addr: ContainerAddress = ContainerAddress(\"ip\"))(\n    ccRes: Future[RunResult] =\n      Future.successful(RunResult(intervalOf(1.millisecond), Right(ContainerResponse(true, \"\", None)))),\n    awaitLogs: FiniteDuration = 2.seconds)(implicit docker: DockerApiWithFileAccess, runc: RuncApi): DockerContainer = {\n\n    new DockerContainer(id, addr, true) {\n      override protected def callContainer(\n        path: String,\n        body: JsObject,\n        timeout: FiniteDuration,\n        concurrent: Int,\n        maxResponse: ByteSize,\n        truncation: ByteSize,\n        retry: Boolean = false,\n        reschedule: Boolean = false)(implicit transid: TransactionId): Future[RunResult] = {\n        ccRes\n      }\n      override protected val logCollectingIdleTimeout = awaitLogs\n      override protected val filePollInterval = 1.millisecond\n    }\n  }\n\n  behavior of \"DockerContainer\"\n\n  implicit val transid = TransactionId.testing\n  val parameters = Map(\n    \"--cap-drop\" -> Set(\"NET_RAW\", \"NET_ADMIN\"),\n    \"--ulimit\" -> Set(\"nofile=1024:1024\"),\n    \"--pids-limit\" -> Set(\"1024\"))\n\n  /*\n   * CONTAINER CREATION\n   */\n  it should \"create a new instance\" in {\n    implicit val docker = new TestDockerClient\n    implicit val runc = stub[RuncApi]\n\n    val image = \"image\"\n    val memory = 128.MB\n    val cpuShares = 1\n    val cpuLimit = 0.5\n    val environment = Map(\"test\" -> \"hi\")\n    val network = \"testwork\"\n    val name = \"myContainer\"\n    val container = DockerContainer.create(\n      transid = transid,\n      image = Right(ImageName(image)),\n      memory = memory,\n      cpuShares = cpuShares,\n      cpuLimit = Some(cpuLimit),\n      environment = environment,\n      network = network,\n      name = Some(name),\n      dockerRunParameters = parameters)\n\n    await(container)\n\n    docker.pulls should have size 0\n    docker.runs should have size 1\n    docker.inspects should have size 1\n    docker.rms should have size 0\n\n    val (testImage, args) = docker.runs.head\n    testImage shouldBe \"image\"\n\n    // Assert fixed values are passed as well\n    args should contain allOf (\"--cap-drop\", \"NET_RAW\", \"NET_ADMIN\")\n    args should contain inOrder (\"--ulimit\", \"nofile=1024:1024\")\n    args should contain inOrder (\"--pids-limit\", \"1024\") // OW PR 2119\n\n    // Assert proper parameter translation\n    args should contain inOrder (\"--memory\", s\"${memory.toMB}m\")\n    args should contain inOrder (\"--memory-swap\", s\"${memory.toMB}m\")\n    args should contain inOrder (\"--cpu-shares\", cpuShares.toString)\n    args should contain inOrder (\"--network\", network)\n    args should contain inOrder (\"--name\", name)\n    args should contain inOrder (\"--cpus\", cpuLimit.toString)\n\n    // Assert proper environment passing\n    args should contain allOf (\"-e\", \"test=hi\")\n  }\n\n  it should \"pull a user provided image before creating the container\" in {\n    implicit val docker = new TestDockerClient\n    implicit val runc = stub[RuncApi]\n\n    val container =\n      DockerContainer.create(transid = transid, image = Left(ImageName(\"image\")), dockerRunParameters = parameters)\n    await(container)\n\n    docker.pulls should have size 1\n    docker.runs should have size 1\n    docker.inspects should have size 1\n    docker.rms should have size 0\n  }\n\n  it should \"remove the container if inspect fails\" in {\n    implicit val docker = new TestDockerClient {\n      override def inspectIPAddress(id: ContainerId,\n                                    network: String)(implicit transid: TransactionId): Future[ContainerAddress] = {\n        inspects += ((id, network))\n        Future.failed(new RuntimeException())\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container =\n      DockerContainer.create(transid = transid, image = Right(ImageName(\"image\")), dockerRunParameters = parameters)\n    a[WhiskContainerStartupError] should be thrownBy await(container)\n\n    docker.pulls should have size 0\n    docker.runs should have size 1\n    docker.inspects should have size 1\n    docker.rms should have size 1\n  }\n\n  it should \"provide a proper error if run fails for blackbox containers\" in {\n    implicit val docker = new TestDockerClient {\n      override def run(image: String,\n                       args: Seq[String] = Seq.empty[String])(implicit transid: TransactionId): Future[ContainerId] = {\n        runs += ((image, args))\n        Future.failed(ProcessUnsuccessfulException(ExitStatus(1), \"\", \"\"))\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container =\n      DockerContainer.create(transid = transid, image = Left(ImageName(\"image\")), dockerRunParameters = parameters)\n    a[WhiskContainerStartupError] should be thrownBy await(container)\n\n    docker.pulls should have size 1\n    docker.runs should have size 1\n    docker.inspects should have size 0\n    docker.rms should have size 0\n  }\n\n  it should \"remove the container if run fails with a broken container\" in {\n    implicit val docker = new TestDockerClient {\n      override def run(image: String,\n                       args: Seq[String] = Seq.empty[String])(implicit transid: TransactionId): Future[ContainerId] = {\n        runs += ((image, args))\n        Future.failed(BrokenDockerContainer(containerId, \"Broken container\"))\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container =\n      DockerContainer.create(transid = transid, image = Right(ImageName(\"image\")), dockerRunParameters = parameters)\n    a[WhiskContainerStartupError] should be thrownBy await(container)\n\n    docker.pulls should have size 0\n    docker.runs should have size 1\n    docker.inspects should have size 0\n    docker.rms should have size 1\n  }\n\n  it should \"provide a proper error if inspect fails for blackbox containers\" in {\n    implicit val docker = new TestDockerClient {\n      override def inspectIPAddress(id: ContainerId,\n                                    network: String)(implicit transid: TransactionId): Future[ContainerAddress] = {\n        inspects += ((id, network))\n        Future.failed(new RuntimeException())\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container =\n      DockerContainer.create(transid = transid, image = Left(ImageName(\"image\")), dockerRunParameters = parameters)\n    a[WhiskContainerStartupError] should be thrownBy await(container)\n\n    docker.pulls should have size 1\n    docker.runs should have size 1\n    docker.inspects should have size 1\n    docker.rms should have size 1\n  }\n\n  it should \"return a specific error if pulling a user provided image failed (given the image does not define a tag)\" in {\n    implicit val docker = new TestDockerClient {\n      override def pull(image: String)(implicit transid: TransactionId): Future[Unit] = {\n        pulls += image\n        Future.failed(new RuntimeException())\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val imageName = \"image\"\n    val container =\n      DockerContainer.create(transid = transid, image = Left(ImageName(imageName)), dockerRunParameters = parameters)\n    val exception = the[BlackboxStartupError] thrownBy await(container)\n    exception.msg shouldBe Messages.imagePullError(imageName)\n\n    docker.pulls should have size 1\n    docker.runs should have size 0 // run is **not** called as a backup measure because no tag is defined\n    docker.inspects should have size 0\n    docker.rms should have size 0\n  }\n\n  it should \"recover a failed image pull if the subsequent docker run succeeds\" in {\n    implicit val docker = new TestDockerClient {\n      override def pull(image: String)(implicit transid: TransactionId): Future[Unit] = {\n        pulls += image\n        Future.failed(new RuntimeException())\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container =\n      DockerContainer.create(\n        transid = transid,\n        image = Left(ImageName(\"image\", tag = Some(\"prod\"))),\n        dockerRunParameters = parameters)\n\n    noException should be thrownBy await(container)\n\n    docker.pulls should have size 1\n    docker.runs should have size 1 // run is called as a backup measure in case the image is locally available\n    docker.inspects should have size 1\n    docker.rms should have size 0\n  }\n\n  it should \"throw a pull exception if a recovering docker run fails as well\" in {\n    implicit val docker = new TestDockerClient {\n      override def pull(image: String)(implicit transid: TransactionId): Future[Unit] = {\n        pulls += image\n        Future.failed(new RuntimeException())\n      }\n      override def run(image: String, args: Seq[String])(implicit transid: TransactionId): Future[ContainerId] = {\n        runs += ((image, args))\n        Future.failed(new RuntimeException())\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val imageName = ImageName(\"image\", tag = Some(\"prod\"))\n    val container =\n      DockerContainer.create(transid = transid, image = Left(imageName), dockerRunParameters = parameters)\n\n    val exception = the[BlackboxStartupError] thrownBy await(container)\n    exception.msg shouldBe Messages.imagePullError(imageName.resolveImageName())\n\n    docker.pulls should have size 1\n    docker.runs should have size 1 // run is called as a backup measure in case the image is locally available\n    docker.inspects should have size 0 // inspect is never called because the run failed as well\n    docker.rms should have size 0\n  }\n\n  /*\n   * DOCKER COMMANDS\n   */\n  it should \"pause and resume container via runc\" in {\n    implicit val docker = new TestDockerClient\n    implicit val runc = new TestRuncClient\n\n    val id = ContainerId(\"id\")\n    val container = new DockerContainer(id, ContainerAddress(\"ip\"), true)\n\n    val suspend = container.suspend()\n    val resume = container.resume()\n\n    await(suspend)\n    await(resume)\n\n    docker.unpauses should have size 0\n    docker.pauses should have size 0\n\n    runc.pauses should have size 1\n    runc.resumes should have size 1\n  }\n\n  it should \"pause and unpause container via docker\" in {\n    implicit val docker = new TestDockerClient\n    implicit val runc = new TestRuncClient\n\n    val id = ContainerId(\"id\")\n    val container = new DockerContainer(id, ContainerAddress(\"ip\"), false)\n\n    val suspend = container.suspend()\n    val resume = container.resume()\n\n    await(suspend)\n    await(resume)\n\n    docker.unpauses should have size 1\n    docker.pauses should have size 1\n\n    runc.pauses should have size 0\n    runc.resumes should have size 0\n  }\n\n  it should \"destroy a container via Docker\" in {\n    implicit val docker = stub[DockerApiWithFileAccess]\n    implicit val runc = stub[RuncApi]\n\n    val id = ContainerId(\"id\")\n    val container = new DockerContainer(id, ContainerAddress(\"ip\"), true)\n\n    container.destroy()\n\n    (docker.rm(_: ContainerId)(_: TransactionId)).verify(id, transid)\n  }\n\n  /*\n   * INITIALIZE\n   *\n   * Only tests for quite simple cases. Disambiguation of errors is delegated to ActivationResponse\n   * and so are the tests for those.\n   */\n  it should \"initialize a container\" in {\n    implicit val docker = stub[DockerApiWithFileAccess]\n    implicit val runc = stub[RuncApi]\n\n    val initTimeout = 1.second\n    val interval = intervalOf(1.millisecond)\n    val container = dockerContainer() {\n      Future.successful(RunResult(interval, Right(ContainerResponse(true, \"\", None))))\n    }\n\n    val initInterval = container.initialize(JsObject.empty, initTimeout, 1)\n    await(initInterval, initTimeout) shouldBe interval\n\n    // assert the starting log is there\n    val start = LogMarker.parse(logLines.head)\n    start.token shouldBe INVOKER_ACTIVATION_INIT\n\n    // assert the end log is there\n    val end = LogMarker.parse(logLines.last)\n    end.token shouldBe INVOKER_ACTIVATION_INIT.asFinish\n    end.deltaToMarkerStart shouldBe Some(interval.duration.toMillis)\n  }\n\n  it should \"properly deal with a timeout during initialization\" in {\n    implicit val docker = stub[DockerApiWithFileAccess]\n    implicit val runc = stub[RuncApi]\n\n    val initTimeout = 1.second\n    val interval = intervalOf(initTimeout + 1.nanoseconds)\n\n    val container = dockerContainer() {\n      Future.successful(RunResult(interval, Left(Timeout(new Throwable()))))\n    }\n\n    val init = container.initialize(JsObject.empty, initTimeout, 1)\n\n    val error = the[InitializationError] thrownBy await(init, initTimeout)\n    error.interval shouldBe interval\n    error.response.statusCode shouldBe ActivationResponse.DeveloperError\n\n    // assert the finish log is there\n    val end = LogMarker.parse(logLines.last)\n    end.token shouldBe INVOKER_ACTIVATION_INIT.asFinish\n  }\n\n  /*\n   * RUN\n   *\n   * Only tests for quite simple cases. Disambiguation of errors is delegated to ActivationResponse\n   * and so are the tests for those.\n   */\n  it should \"run a container\" in {\n    implicit val docker = stub[DockerApiWithFileAccess]\n    implicit val runc = stub[RuncApi]\n\n    val interval = intervalOf(1.millisecond)\n    val result = JsObject.empty\n    val container = dockerContainer() {\n      Future.successful(RunResult(interval, Right(ContainerResponse(true, result.compactPrint, None))))\n    }\n\n    val runResult = container.run(JsObject.empty, JsObject.empty, 1.second, 1, 1.MB, 1.MB)\n    await(runResult) shouldBe (interval, ActivationResponse.success(Some(result), Some(2)))\n\n    // assert the starting log is there\n    val start = LogMarker.parse(logLines.head)\n    start.token shouldBe INVOKER_ACTIVATION_RUN\n\n    // assert the end log is there\n    val end = LogMarker.parse(logLines.last)\n    end.token shouldBe INVOKER_ACTIVATION_RUN.asFinish\n    end.deltaToMarkerStart shouldBe Some(interval.duration.toMillis)\n  }\n\n  it should \"throw ContainerHealthError if runtime container returns 503 response\" in {\n    implicit val docker = stub[DockerApiWithFileAccess]\n    implicit val runc = stub[RuncApi]\n\n    val interval = intervalOf(1.millisecond)\n    val result = JsObject.empty\n    val container = dockerContainer() {\n      Future.successful(RunResult(interval, Right(ContainerResponse(503, result.compactPrint, None))))\n    }\n\n    val initResult = container.initialize(JsObject.empty, 1.second, 1)\n    an[ContainerHealthError] should be thrownBy await(initResult)\n\n    val runResult = container.run(JsObject.empty, JsObject.empty, 1.second, 1, 1.MB, 1.MB)\n    an[ContainerHealthError] should be thrownBy await(runResult)\n  }\n\n  it should \"properly deal with a timeout during run\" in {\n    implicit val docker = stub[DockerApiWithFileAccess]\n    implicit val runc = stub[RuncApi]\n\n    val runTimeout = 1.second\n    val interval = intervalOf(runTimeout + 1.nanoseconds)\n\n    val container = dockerContainer() {\n      Future.successful(RunResult(interval, Left(Timeout(new Throwable()))))\n    }\n\n    val runResult = container.run(JsObject.empty, JsObject.empty, runTimeout, 1, 1.MB, 1.MB)\n    await(runResult) shouldBe (interval, ActivationResponse.developerError(\n      Messages.timedoutActivation(runTimeout, false)))\n\n    // assert the finish log is there\n    val end = LogMarker.parse(logLines.last)\n    end.token shouldBe INVOKER_ACTIVATION_RUN.asFinish\n  }\n\n  /*\n   * LOGS\n   */\n  it should \"read a simple log with sentinel\" in {\n    val expectedLogEntry = LogLine(Instant.EPOCH.toString, \"stdout\", \"This is a log entry.\\n\")\n    val rawLog = toRawLog(Seq(expectedLogEntry), appendSentinel = true)\n\n    implicit val docker = new TestDockerClient {\n      override def rawContainerLogs(containerId: ContainerId,\n                                    fromPos: Long,\n                                    pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n        rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n        Source.single(rawLog)\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container = dockerContainer(id = containerId)()\n    // Read with tight limit to verify that no truncation occurs\n    val processedLogs = awaitLogs(container.logs(limit = rawLog.length.bytes, waitForSentinel = true))\n\n    docker.rawContainerLogsInvocations should have size 1\n    val (id, fromPos, pollInterval) = docker.rawContainerLogsInvocations(0)\n    id shouldBe containerId\n    fromPos shouldBe 0\n    pollInterval shouldBe 'defined\n\n    processedLogs should have size 1\n    processedLogs shouldBe Vector(expectedLogEntry.toFormattedString)\n  }\n\n  it should \"read a simple log without sentinel\" in {\n    val expectedLogEntry = LogLine(Instant.EPOCH.toString, \"stdout\", \"This is a log entry.\\n\")\n    val rawLog = toRawLog(Seq(expectedLogEntry), appendSentinel = false)\n\n    implicit val docker = new TestDockerClient {\n      override def rawContainerLogs(containerId: ContainerId,\n                                    fromPos: Long,\n                                    pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n        rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n        Source.single(rawLog)\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container = dockerContainer(id = containerId)()\n    // Read without tight limit so that the full read result is processed\n    val processedLogs = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = false))\n\n    docker.rawContainerLogsInvocations should have size 1\n    val (id, fromPos, pollInterval) = docker.rawContainerLogsInvocations(0)\n    id shouldBe containerId\n    fromPos shouldBe 0\n    pollInterval should not be 'defined\n\n    processedLogs should have size 1\n    processedLogs shouldBe Vector(expectedLogEntry.toFormattedString)\n  }\n\n  it should \"fail log reading if error occurs during file reading\" in {\n    implicit val docker = new TestDockerClient {\n      override def rawContainerLogs(containerId: ContainerId,\n                                    fromPos: Long,\n                                    pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n        rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n        Source.failed(new IOException)\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container = dockerContainer()()\n    an[IOException] should be thrownBy awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true))\n\n    docker.rawContainerLogsInvocations should have size 1\n    val (id, fromPos, _) = docker.rawContainerLogsInvocations(0)\n    id shouldBe containerId\n    fromPos shouldBe 0\n  }\n\n  it should \"read two consecutive logs with sentinel\" in {\n    val firstLogEntry = LogLine(Instant.EPOCH.toString, \"stdout\", \"This is the first log.\\n\")\n    val secondLogEntry = LogLine(Instant.EPOCH.plusSeconds(1L).toString, \"stderr\", \"This is the second log.\\n\")\n    val firstRawLog = toRawLog(Seq(firstLogEntry), appendSentinel = true)\n    val secondRawLog = toRawLog(Seq(secondLogEntry), appendSentinel = true)\n    val returnValues = mutable.Queue(firstRawLog, secondRawLog)\n\n    implicit val docker = new TestDockerClient {\n      override def rawContainerLogs(containerId: ContainerId,\n                                    fromPos: Long,\n                                    pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n        rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n        Source.single(returnValues.dequeue())\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container = dockerContainer()()\n    // Read without tight limit so that the full read result is processed\n    val processedFirstLog = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true))\n    val processedSecondLog = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true))\n\n    docker.rawContainerLogsInvocations should have size 2\n    val (_, fromPos1, _) = docker.rawContainerLogsInvocations(0)\n    fromPos1 shouldBe 0\n    val (_, fromPos2, _) = docker.rawContainerLogsInvocations(1)\n    fromPos2 shouldBe firstRawLog.length // second read should start behind the first line\n\n    processedFirstLog should have size 1\n    processedFirstLog shouldBe Vector(firstLogEntry.toFormattedString)\n    processedSecondLog should have size 1\n    processedSecondLog shouldBe Vector(secondLogEntry.toFormattedString)\n  }\n\n  it should \"eventually terminate even if no sentinels can be found\" in {\n\n    val expectedLog = Seq(LogLine(Instant.EPOCH.toString, \"stdout\", s\"This is log entry.\\n\"))\n    val rawLog = toRawLog(expectedLog, appendSentinel = false)\n\n    implicit val docker = new TestDockerClient {\n      override def rawContainerLogs(containerId: ContainerId,\n                                    fromPos: Long,\n                                    pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n        rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n        // \"Fakes\" an infinite source with only 1 entry\n        Source.tick(0.milliseconds, 10.seconds, rawLog)\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val waitForLogs = 100.milliseconds\n    val container = dockerContainer()(awaitLogs = waitForLogs)\n    // Read without tight limit so that the full read result is processed\n\n    val (interval, processedLog) = durationOf(awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true)))\n\n    interval.toMillis should (be >= waitForLogs.toMillis and be < (waitForLogs * 2).toMillis)\n\n    docker.rawContainerLogsInvocations should have size 1\n\n    processedLog should have size expectedLog.length + 1 //error log should be appended\n    processedLog.head shouldBe expectedLog.head.toFormattedString\n    processedLog(1) should include(Messages.logFailure)\n  }\n\n  it should \"truncate logs and advance reading position to end of current read\" in {\n    val firstLogFirstEntry = LogLine(Instant.EPOCH.toString, \"stdout\", \"This is the first line in first log.\\n\")\n    val firstLogSecondEntry =\n      LogLine(Instant.EPOCH.plusMillis(1L).toString, \"stderr\", \"This is the second line in first log.\\n\")\n\n    val secondLogFirstEntry =\n      LogLine(Instant.EPOCH.plusMillis(2L).toString, \"stdout\", \"This is the first line in second log.\\n\")\n    val secondLogSecondEntry =\n      LogLine(Instant.EPOCH.plusMillis(3L).toString, \"stdout\", \"This is the second line in second log.\\n\")\n    val secondLogLimit = 4\n\n    val thirdLogFirstEntry =\n      LogLine(Instant.EPOCH.plusMillis(4L).toString, \"stdout\", \"This is the first line in third log.\\n\")\n\n    val firstRawLog = toRawLog(Seq(firstLogFirstEntry, firstLogSecondEntry), appendSentinel = false)\n    val secondRawLog = toRawLog(Seq(secondLogFirstEntry, secondLogSecondEntry), appendSentinel = false)\n    val thirdRawLog = toRawLog(Seq(thirdLogFirstEntry), appendSentinel = true)\n\n    val returnValues = mutable.Queue(firstRawLog, secondRawLog, thirdRawLog)\n\n    implicit val docker = new TestDockerClient {\n      override def rawContainerLogs(containerId: ContainerId,\n                                    fromPos: Long,\n                                    pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n        rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n        Source.single(returnValues.dequeue())\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container = dockerContainer()()\n    val processedFirstLog = awaitLogs(container.logs(limit = (firstRawLog.length - 1).bytes, waitForSentinel = true))\n    val processedSecondLog =\n      awaitLogs(container.logs(limit = (secondRawLog.length - 1).bytes, waitForSentinel = false))\n    val processedThirdLog = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true))\n\n    docker.rawContainerLogsInvocations should have size 3\n    val (_, fromPos1, _) = docker.rawContainerLogsInvocations(0)\n    fromPos1 shouldBe 0\n    val (_, fromPos2, _) = docker.rawContainerLogsInvocations(1)\n    fromPos2 shouldBe firstRawLog.length // second read should start behind full content of first read\n    val (_, fromPos3, _) = docker.rawContainerLogsInvocations(2)\n    fromPos3 shouldBe firstRawLog.length + secondRawLog.length // third read should start behind full content of first and second read\n\n    processedFirstLog should have size 2\n    processedFirstLog(0) shouldBe firstLogFirstEntry.toFormattedString\n    // Allowing just 1 byte less than the JSON structure causes the entire line to drop\n    processedFirstLog(1) should include(Messages.truncateLogs((firstRawLog.length - 1).bytes))\n\n    processedSecondLog should have size 2\n    processedSecondLog(0) shouldBe secondLogFirstEntry.toFormattedString\n    processedSecondLog(1) should include(Messages.truncateLogs((secondRawLog.length - 1).bytes))\n\n    processedThirdLog should have size 1\n    processedThirdLog(0) shouldBe thirdLogFirstEntry.toFormattedString\n  }\n\n  it should \"not fail if the last log-line is incomplete\" in {\n    val expectedLogEntry = LogLine(Instant.EPOCH.toString, \"stdout\", \"This is a log entry.\\n\")\n    // \"destroy\" the second log entry by dropping some bytes\n    val rawLog = toRawLog(Seq(expectedLogEntry, expectedLogEntry), appendSentinel = false).dropRight(10)\n\n    implicit val docker = new TestDockerClient {\n      override def rawContainerLogs(containerId: ContainerId,\n                                    fromPos: Long,\n                                    pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n        rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n        Source.single(rawLog)\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container = dockerContainer(id = containerId)()\n    // Read with tight limit to verify that no truncation occurs\n    val processedLogs = awaitLogs(container.logs(limit = rawLog.length.bytes, waitForSentinel = false))\n\n    docker.rawContainerLogsInvocations should have size 1\n    val (id, fromPos, _) = docker.rawContainerLogsInvocations(0)\n    id shouldBe containerId\n    fromPos shouldBe 0\n\n    processedLogs should have size 2\n    processedLogs(0) shouldBe expectedLogEntry.toFormattedString\n    processedLogs(1) should include(Messages.logFailure)\n  }\n\n  it should \"include an incomplete warning if sentinels have not been found only if we wait for sentinels\" in {\n    val expectedLogEntry = LogLine(Instant.EPOCH.toString, \"stdout\", \"This is a log entry.\\n\")\n    val rawLog = toRawLog(Seq(expectedLogEntry, expectedLogEntry), appendSentinel = false)\n\n    implicit val docker = new TestDockerClient {\n      override def rawContainerLogs(containerId: ContainerId,\n                                    fromPos: Long,\n                                    pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n        rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n        Source.single(rawLog)\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container = dockerContainer(id = containerId)()\n    // Read with tight limit to verify that no truncation occurs\n    val processedLogs = awaitLogs(container.logs(limit = rawLog.length.bytes, waitForSentinel = true))\n\n    docker.rawContainerLogsInvocations should have size 1\n    val (id, fromPos, _) = docker.rawContainerLogsInvocations(0)\n    id shouldBe containerId\n    fromPos shouldBe 0\n\n    processedLogs should have size 3\n    processedLogs(0) shouldBe expectedLogEntry.toFormattedString\n    processedLogs(1) shouldBe expectedLogEntry.toFormattedString\n    processedLogs(2) should include(Messages.logFailure)\n\n    val processedLogsFalse = awaitLogs(container.logs(limit = rawLog.length.bytes, waitForSentinel = false))\n    processedLogsFalse should have size 2\n    processedLogsFalse(0) shouldBe expectedLogEntry.toFormattedString\n    processedLogsFalse(1) shouldBe expectedLogEntry.toFormattedString\n  }\n\n  it should \"strip sentinel lines if it waits or doesn't wait for them\" in {\n    val expectedLogEntry = LogLine(Instant.EPOCH.toString, \"stdout\", \"This is a log entry.\\n\")\n    val rawLog = toRawLog(Seq(expectedLogEntry), appendSentinel = true)\n\n    implicit val docker = new TestDockerClient {\n      override def rawContainerLogs(containerId: ContainerId,\n                                    fromPos: Long,\n                                    pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n        rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n        Source.single(rawLog)\n      }\n    }\n    implicit val runc = stub[RuncApi]\n\n    val container = dockerContainer(id = containerId)()\n    val processedLogs = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true))\n    processedLogs should have size 1\n    processedLogs(0) shouldBe expectedLogEntry.toFormattedString\n\n    val processedLogsFalse = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = false))\n    processedLogsFalse should have size 1\n    processedLogsFalse(0) shouldBe expectedLogEntry.toFormattedString\n  }\n\n  class TestRuncClient extends RuncApi {\n    var resumes = mutable.Buffer.empty[ContainerId]\n    var pauses = mutable.Buffer.empty[ContainerId]\n\n    override def resume(id: ContainerId)(implicit transid: TransactionId): Future[Unit] = {\n      resumes += id\n      Future.successful(())\n    }\n\n    override def pause(id: ContainerId)(implicit transid: TransactionId): Future[Unit] = {\n      pauses += id\n      Future.successful(())\n    }\n  }\n\n  class TestDockerClient extends DockerApiWithFileAccess {\n    var runs = mutable.Buffer.empty[(String, Seq[String])]\n    var inspects = mutable.Buffer.empty[(ContainerId, String)]\n    var pauses = mutable.Buffer.empty[ContainerId]\n    var unpauses = mutable.Buffer.empty[ContainerId]\n    var rms = mutable.Buffer.empty[ContainerId]\n    var pulls = mutable.Buffer.empty[String]\n    var rawContainerLogsInvocations = mutable.Buffer.empty[(ContainerId, Long, Option[FiniteDuration])]\n\n    def clientVersion: String = \"mock-test-client\"\n\n    def run(image: String, args: Seq[String] = Seq.empty[String])(\n      implicit transid: TransactionId): Future[ContainerId] = {\n      runs += ((image, args))\n      Future.successful(ContainerId(\"testId\"))\n    }\n\n    def inspectIPAddress(id: ContainerId, network: String)(\n      implicit transid: TransactionId): Future[ContainerAddress] = {\n      inspects += ((id, network))\n      Future.successful(ContainerAddress(\"testIp\"))\n    }\n\n    def pause(id: ContainerId)(implicit transid: TransactionId): Future[Unit] = {\n      pauses += id\n      Future.successful(())\n    }\n\n    def unpause(id: ContainerId)(implicit transid: TransactionId): Future[Unit] = {\n      unpauses += id\n      Future.successful(())\n    }\n\n    def rm(id: ContainerId)(implicit transid: TransactionId): Future[Unit] = {\n      rms += id\n      Future.successful(())\n    }\n\n    def ps(filters: Seq[(String, String)] = Seq.empty, all: Boolean = false)(\n      implicit transid: TransactionId): Future[Seq[ContainerId]] = ???\n\n    def pull(image: String)(implicit transid: TransactionId): Future[Unit] = {\n      pulls += image\n      Future.successful(())\n    }\n\n    override def isOomKilled(id: ContainerId)(implicit transid: TransactionId): Future[Boolean] = ???\n\n    override def rawContainerLogs(containerId: ContainerId,\n                                  fromPos: Long,\n                                  pollInterval: Option[FiniteDuration]): Source[ByteString, Any] = {\n      rawContainerLogsInvocations += ((containerId, fromPos, pollInterval))\n      Source.single(ByteString.empty)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/docker/test/ProcessRunnerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker.test\n\nimport org.apache.pekko.actor.ActorSystem\nimport common.WskActorSystem\n\nimport scala.concurrent.Future\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.openwhisk.core.containerpool.docker._\n\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.duration._\nimport scala.concurrent.Await\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.language.reflectiveCalls // Needed to invoke run() method of structural ProcessRunner extension\n\n@RunWith(classOf[JUnitRunner])\nclass ProcessRunnerTests extends AnyFlatSpec with Matchers with WskActorSystem {\n\n  def await[A](f: Future[A], timeout: FiniteDuration = 2.seconds) = Await.result(f, timeout)\n\n  val processRunner = new ProcessRunner {\n    def run(args: Seq[String], timeout: FiniteDuration = 100.milliseconds)(implicit ec: ExecutionContext,\n                                                                           as: ActorSystem) =\n      executeProcess(args, timeout)(ec, as)\n  }\n\n  behavior of \"ProcessRunner\"\n\n  it should \"run an external command successfully and capture its output\" in {\n    val stdout = \"Output\"\n    await(processRunner.run(Seq(\"echo\", stdout))) shouldBe stdout\n  }\n\n  it should \"run an external command unsuccessfully and capture its output\" in {\n    val exitStatus = ExitStatus(1)\n    val stdout = \"Output\"\n    val stderr = \"Error\"\n\n    val future =\n      processRunner.run(Seq(\"/bin/sh\", \"-c\", s\"echo ${stdout}; echo ${stderr} 1>&2; exit ${exitStatus.statusValue}\"))\n\n    val exception = the[ProcessRunningException] thrownBy await(future)\n    exception shouldBe ProcessUnsuccessfulException(exitStatus, stdout, stderr)\n    exception.getMessage should startWith(\"info: command was unsuccessful\")\n  }\n\n  it should \"terminate an external command after the specified timeout is reached\" in {\n    val timeout = 100.milliseconds\n    // Run \"sleep\" command for 1 second and make sure that stdout and stderr are dropped\n    val future = processRunner.run(Seq(\"/bin/sh\", \"-c\", \"sleep 1 1>/dev/null 2>/dev/null\"), timeout)\n    val exception = the[ProcessTimeoutException] thrownBy await(future)\n    exception shouldBe ProcessTimeoutException(timeout, ExitStatus(143), \"\", \"\")\n    exception.getMessage should startWith(s\"info: command was terminated, took longer than $timeout\")\n  }\n\n  behavior of \"ExitStatus\"\n\n  it should \"provide a proper textual representation\" in {\n    Seq[(Int, String)](\n      (0, \"successful\"),\n      (1, \"unsuccessful\"),\n      (125, \"unsuccessful\"),\n      (126, \"not executable\"),\n      (127, \"not found\"),\n      (128, \"terminated by signal 0\"),\n      (129, \"terminated by signal SIGHUP\"),\n      (130, \"terminated by signal SIGINT\"),\n      (131, \"terminated by signal SIGQUIT\"),\n      (134, \"terminated by signal SIGABRT\"),\n      (137, \"terminated by signal SIGKILL\"),\n      (142, \"terminated by signal SIGALRM\"),\n      (143, \"terminated by signal SIGTERM\"),\n      (144, \"terminated by signal 16\")).foreach {\n      case (statusValue, detailText) =>\n        ExitStatus(statusValue).toString shouldBe s\"$statusValue ($detailText)\"\n    }\n  }\n\n  it should \"properly classify exit status\" in {\n    withClue(\"Exit status 0 is successful - \") { ExitStatus(0).successful shouldBe true }\n    withClue(\"Exit status 1 is not successful - \") { ExitStatus(1).successful shouldBe false }\n    withClue(\"Exit status 143 means terminated by SIGTERM - \") { ExitStatus(143).terminatedBySIGTERM shouldBe true }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/docker/test/RuncClientTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.docker.test\n\nimport org.apache.pekko.actor.ActorSystem\n\nimport scala.concurrent.Future\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.duration._\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.core.containerpool.docker.RuncClient\nimport common.{StreamLogging, WskActorSystem}\nimport org.apache.openwhisk.core.containerpool.ContainerId\nimport org.apache.openwhisk.common.TransactionId\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}\nimport org.apache.openwhisk.common.LogMarker\nimport org.apache.openwhisk.common.LoggingMarkers.INVOKER_RUNC_CMD\n\n@RunWith(classOf[JUnitRunner])\nclass RuncClientTests\n    extends AnyFlatSpec\n    with Matchers\n    with StreamLogging\n    with BeforeAndAfterEach\n    with WskActorSystem\n    with ScalaFutures\n    with IntegrationPatience {\n\n  override def beforeEach = stream.reset()\n\n  implicit val transid = TransactionId.testing\n  val id = ContainerId(\"Id\")\n\n  val runcCommand = \"runc\"\n\n  /** Returns a RuncClient with a mocked result for 'executeProcess' */\n  def runcClient(result: Future[String]) = new RuncClient()(global) {\n    override val runcCmd = Seq(runcCommand)\n    override def executeProcess(args: Seq[String], timeout: Duration)(implicit ec: ExecutionContext, as: ActorSystem) =\n      result\n  }\n\n  /** Calls a runc method based on the name of the method. */\n  def runcProxy(runc: RuncClient, method: String) = {\n    method match {\n      case \"pause\"  => runc.pause(id)\n      case \"resume\" => runc.resume(id)\n    }\n  }\n\n  /** Verifies start and end logs are written correctly. */\n  def verifyLogs(cmd: String, failed: Boolean = false) = {\n    logLines.head should include(s\"${runcCommand} ${cmd} ${id.asString}\")\n\n    // start log maker must be found\n    val start = LogMarker.parse(logLines.head)\n    start.token.toStringWithSubAction should be(INVOKER_RUNC_CMD(cmd).toStringWithSubAction)\n\n    // end log marker must be found\n    val expectedEnd = if (failed) INVOKER_RUNC_CMD(cmd).asError else INVOKER_RUNC_CMD(cmd).asFinish\n    val end = LogMarker.parse(logLines.last)\n    end.token.toStringWithSubAction shouldBe expectedEnd.toStringWithSubAction\n  }\n\n  behavior of \"RuncClient\"\n\n  Seq(\"pause\", \"resume\").foreach { cmd =>\n    it should s\"$cmd a container successfully and create log entries\" in {\n      val rc = runcClient { Future.successful(\"\") }\n      runcProxy(rc, cmd).futureValue\n      verifyLogs(cmd)\n    }\n\n    it should s\"write error markers when $cmd fails\" in {\n      val rc = runcClient { Future.failed(new RuntimeException()) }\n      a[RuntimeException] should be thrownBy runcProxy(rc, cmd).futureValue\n      verifyLogs(cmd, failed = true)\n    }\n\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/kubernetes/test/Fabric8ClientTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.kubernetes.test\n\nimport java.net.HttpURLConnection\n\nimport common.{StreamLogging, WskActorSystem}\nimport io.fabric8.kubernetes.api.model.{EventBuilder, PodBuilder}\nimport io.fabric8.kubernetes.client.utils.HttpClientUtils.createHttpClientForMockServer\nimport io.fabric8.kubernetes.client.{ConfigBuilder, DefaultKubernetesClient}\nimport okhttp3.TlsVersion.TLS_1_0\nimport org.apache.openwhisk.common.{ConfigMapValue, TransactionId}\nimport org.apache.openwhisk.core.containerpool.kubernetes._\nimport org.apache.openwhisk.core.entity.size._\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass Fabric8ClientTests\n    extends AnyFlatSpec\n    with Matchers\n    with WskActorSystem\n    with ScalaFutures\n    with KubeClientSupport\n    with StreamLogging {\n  implicit val tid: TransactionId = TransactionId.testing\n  behavior of \"Fabric8Client\"\n  val runTimeout = 2.seconds\n  def config(configMap: Option[ConfigMapValue] = None, affinity: Option[KubernetesInvokerNodeAffinity] = None) =\n    KubernetesClientConfig(\n      KubernetesClientTimeoutConfig(runTimeout, 2.seconds),\n      affinity.getOrElse(KubernetesInvokerNodeAffinity(false, \"k\", \"v\")),\n      false,\n      None,\n      configMap,\n      Some(KubernetesCpuScalingConfig(300, 3.MB, 1000)),\n      false,\n      Some(Map(\"POD_UID\" -> \"metadata.uid\")),\n      None)\n\n  it should \"fail activation on cold start when apiserver fails\" in {\n\n    //use an invalid client to simulate broken api server\n    def defaultClient = {\n      val config = new ConfigBuilder()\n        .withMasterUrl(\"http://localhost:11111\") //test assumes that port 11111 will fail in some way\n        .withTrustCerts(true)\n        .withTlsVersions(TLS_1_0)\n        .withNamespace(\"test\")\n        .build\n      new DefaultKubernetesClient(createHttpClientForMockServer(config), config)\n    }\n    val restClient = new KubernetesClient(config(), testClient = Some(defaultClient))(executionContext)\n    restClient.run(\"fail\", \"fail\", 256.MB).failed.futureValue shouldBe a[KubernetesPodApiException]\n  }\n  it should \"fail activation on cold start when pod ready times out\" in {\n    val podName = \"failWait\"\n    server\n      .expect()\n      .post()\n      .withPath(\"/api/v1/namespaces/test/pods\")\n      .andReturn(\n        HttpURLConnection.HTTP_CREATED,\n        new PodBuilder().withNewMetadata().withName(podName).endMetadata().build())\n      .once();\n    server\n      .expect()\n      .get()\n      .withPath(\"/api/v1/namespaces/test/pods/failWait\")\n      .andReturn(HttpURLConnection.HTTP_OK, new PodBuilder().withNewMetadata().withName(podName).endMetadata().build())\n      .times(3)\n    server\n      .expect()\n      .get()\n      .withPath(\"/api/v1/namespaces/test/events?fieldSelector=involvedObject.name%3DfailWait\")\n      .andReturn(\n        HttpURLConnection.HTTP_OK,\n        new EventBuilder().withNewMetadata().withName(podName).endMetadata().build())\n      .once\n\n    implicit val patienceConfig = PatienceConfig(timeout = runTimeout + 1.seconds, interval = 0.5.seconds)\n\n    val restClient = new KubernetesClient(config(), testClient = Some(kubeClient))(executionContext)\n    restClient.run(podName, \"anyimage\", 256.MB).failed.futureValue shouldBe a[KubernetesPodReadyTimeoutException]\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/kubernetes/test/KubeClientSupport.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.kubernetes.test\n\nimport common.StreamLogging\nimport io.fabric8.kubernetes.client.server.mock.KubernetesMockServer\nimport io.fabric8.kubernetes.client.utils.HttpClientUtils.createHttpClientForMockServer\nimport io.fabric8.kubernetes.client.{ConfigBuilder, DefaultKubernetesClient}\nimport okhttp3.TlsVersion.TLS_1_0\nimport org.scalatest.{BeforeAndAfterAll, Suite, TestSuite}\n\nimport scala.concurrent.duration._\n\ntrait KubeClientSupport extends TestSuite with BeforeAndAfterAll with StreamLogging {\n  self: Suite =>\n\n  protected def useMockServer = true\n\n  val server = new KubernetesMockServer(false)\n\n  protected lazy val (kubeClient, closeable) = {\n    if (useMockServer) {\n      server.init()\n      def defaultClient = {\n        val config = new ConfigBuilder()\n          .withMasterUrl(server.url(\"/\"))\n          .withTrustCerts(true)\n          .withTlsVersions(TLS_1_0)\n          .withNamespace(\"test\")\n          .build\n        new DefaultKubernetesClient(createHttpClientForMockServer(config), config)\n      }\n      (defaultClient, () => server.destroy())\n    } else {\n      val client = new DefaultKubernetesClient(\n        new ConfigBuilder()\n          .withConnectionTimeout(1.minute.toMillis.toInt)\n          .withRequestTimeout(1.minute.toMillis.toInt)\n          .build())\n      (client, () => client.close())\n    }\n  }\n\n  override def beforeAll(): Unit = {\n    if (!useMockServer) {\n      val kubeconfig = sys.env.get(\"KUBECONFIG\")\n      assume(kubeconfig.isDefined, \"KUBECONFIG env must be defined\")\n      println(s\"Using kubeconfig from ${kubeconfig.get}\")\n    }\n    super.beforeAll()\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    closeable.apply()\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/kubernetes/test/KubernetesClientTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.kubernetes.test\n\nimport java.time.Instant\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.{Concat, Sink, Source}\n\nimport scala.concurrent.Await\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.concurrent.Eventually\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.time.{Seconds, Span}\nimport common.{StreamLogging, WskActorSystem}\nimport okio.Buffer\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.{ContainerAddress, ContainerId}\nimport org.apache.openwhisk.core.containerpool.kubernetes._\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.containerpool.Container.ACTIVATION_LOG_SENTINEL\n\nimport scala.collection.mutable\nimport scala.collection.immutable.Queue\n\n@RunWith(classOf[JUnitRunner])\nclass KubernetesClientTests\n    extends AnyFlatSpec\n    with Matchers\n    with StreamLogging\n    with BeforeAndAfterEach\n    with Eventually\n    with WskActorSystem {\n\n  import KubernetesClientTests._\n\n  val commandTimeout = 500.milliseconds\n  def await[A](f: Future[A], timeout: FiniteDuration = commandTimeout) = Await.result(f, timeout)\n\n  /** Reads logs into memory and awaits them */\n  def awaitLogs(source: Source[TypedLogLine, Any], timeout: FiniteDuration = 1000.milliseconds): Vector[TypedLogLine] =\n    Await.result(source.runWith(Sink.seq[TypedLogLine]), timeout).toVector\n\n  override def beforeEach = stream.reset()\n\n  implicit override val patienceConfig = PatienceConfig(timeout = scaled(Span(5, Seconds)))\n\n  implicit val transid = TransactionId.testing\n  val id = ContainerId(\"55db56ee082239428b27d3728b4dd324c09068458aad9825727d5bfc1bba6d52\")\n  val container = kubernetesContainer(id)\n\n  /** Returns a KubernetesClient with a mocked result for 'executeProcess' */\n  def kubernetesClient(fixture: => Future[String]) = {\n    new KubernetesClient()(executionContext) {\n      override def executeProcess(args: Seq[String], timeout: Duration)(implicit ec: ExecutionContext,\n                                                                        as: ActorSystem) =\n        fixture\n    }\n  }\n\n  def kubernetesContainer(id: ContainerId) =\n    new KubernetesContainer(id, ContainerAddress(\"ip\"), \"ip\", \"docker://\" + id.asString)(kubernetesClient {\n      Future.successful(\"\")\n    }, actorSystem, executionContext, logging)\n\n  behavior of \"KubernetesClient\"\n\n  val firstLog = s\"\"\"2018-02-06T00:00:18.419889342Z first activation\n                   |2018-02-06T00:00:18.419929471Z $ACTIVATION_LOG_SENTINEL\n                   |2018-02-06T00:00:18.419988733Z $ACTIVATION_LOG_SENTINEL\n                   |\"\"\".stripMargin\n  val secondLog = s\"\"\"2018-02-06T00:09:35.38267193Z second activation\n                    |2018-02-06T00:09:35.382990278Z $ACTIVATION_LOG_SENTINEL\n                    |2018-02-06T00:09:35.383116503Z $ACTIVATION_LOG_SENTINEL\n                    |\"\"\".stripMargin\n\n  def firstSource(lastTimestamp: Option[Instant] = None): Source[TypedLogLine, Any] =\n    Source(\n      KubernetesRestLogSourceStage\n        .readLines(new Buffer().writeUtf8(firstLog), lastTimestamp, Queue.empty))\n\n  def secondSource(lastTimestamp: Option[Instant] = None): Source[TypedLogLine, Any] =\n    Source(\n      KubernetesRestLogSourceStage\n        .readLines(new Buffer().writeUtf8(secondLog), lastTimestamp, Queue.empty))\n\n  it should \"forward suspend commands to the client\" in {\n    implicit val kubernetes = new TestKubernetesClient\n    val id = ContainerId(\"id\")\n    val container = new KubernetesContainer(id, ContainerAddress(\"ip\"), \"127.0.0.1\", \"docker://foo\")\n    await(container.suspend())\n    kubernetes.suspends should have size 1\n    kubernetes.suspends(0) shouldBe id\n  }\n\n  it should \"forward resume commands to the client\" in {\n    implicit val kubernetes = new TestKubernetesClient\n    val id = ContainerId(\"id\")\n    val container = new KubernetesContainer(id, ContainerAddress(\"ip\"), \"127.0.0.1\", \"docker://foo\")\n    await(container.resume())\n    kubernetes.resumes should have size 1\n    kubernetes.resumes(0) shouldBe id\n  }\n\n  it should \"return all logs when no sinceTime passed\" in {\n    val client = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        firstSource()\n      }\n    }\n    val logs = awaitLogs(client.logs(container, None))\n    logs should have size 3\n    logs(0) shouldBe TypedLogLine(\"2018-02-06T00:00:18.419889342Z\", \"stdout\", \"first activation\")\n    logs(2) shouldBe TypedLogLine(\"2018-02-06T00:00:18.419988733Z\", \"stdout\", ACTIVATION_LOG_SENTINEL)\n  }\n\n  it should \"return all logs after the one matching sinceTime\" in {\n\n    val testDate: Option[Instant] = \"2018-02-06T00:00:18.419988733Z\"\n    val client = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        Source.combine(firstSource(testDate), secondSource(testDate))(Concat(_))\n      }\n    }\n    val logs = awaitLogs(client.logs(container, testDate))\n    logs should have size 3\n    logs(0) shouldBe TypedLogLine(\"2018-02-06T00:09:35.38267193Z\", \"stdout\", \"second activation\")\n    logs(2) shouldBe TypedLogLine(\"2018-02-06T00:09:35.383116503Z\", \"stdout\", ACTIVATION_LOG_SENTINEL)\n  }\n\n  it should \"return all logs if none match sinceTime\" in {\n    val testDate: Option[Instant] = \"2018-02-06T00:00:18.419988733Z\"\n    val client = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        secondSource(testDate)\n      }\n    }\n    val logs = awaitLogs(client.logs(container, testDate))\n    logs should have size 3\n    logs(0) shouldBe TypedLogLine(\"2018-02-06T00:09:35.38267193Z\", \"stdout\", \"second activation\")\n    logs(2) shouldBe TypedLogLine(\"2018-02-06T00:09:35.383116503Z\", \"stdout\", ACTIVATION_LOG_SENTINEL)\n  }\n\n}\n\nobject KubernetesClientTests {\n  import scala.language.implicitConversions\n  import scala.concurrent.ExecutionContext.Implicits.global\n\n  implicit def strToDate(str: String): Option[Instant] =\n    KubernetesClient.parseK8STimestamp(str).toOption\n\n  implicit def strToInstant(str: String): Instant =\n    strToDate(str).get\n\n  class TestKubernetesClient(implicit as: ActorSystem) extends KubernetesApi with StreamLogging {\n    var runs = mutable.Buffer.empty[(String, String, Map[String, String], Map[String, String])]\n    var rms = mutable.Buffer.empty[ContainerId]\n    var rmByLabels = mutable.Buffer.empty[(String, String)]\n    var resumes = mutable.Buffer.empty[ContainerId]\n    var suspends = mutable.Buffer.empty[ContainerId]\n    var logCalls = mutable.Buffer.empty[(ContainerId, Option[Instant])]\n\n    def run(name: String,\n            image: String,\n            memory: ByteSize = 256.MB,\n            env: Map[String, String] = Map.empty,\n            labels: Map[String, String] = Map.empty)(implicit transid: TransactionId): Future[KubernetesContainer] = {\n      runs += ((name, image, env, labels))\n      implicit val kubernetes = this\n      val containerId = ContainerId(\"id\")\n      val addr: ContainerAddress = ContainerAddress(\"ip\")\n      val workerIP: String = \"127.0.0.1\"\n      val nativeContainerId: String = \"docker://\" + containerId.asString\n      Future.successful(new KubernetesContainer(containerId, addr, workerIP, nativeContainerId))\n    }\n\n    def rm(container: KubernetesContainer)(implicit transid: TransactionId): Future[Unit] = {\n      rms += container.id\n      Future.successful(())\n    }\n\n    override def rm(podName: String)(implicit transid: TransactionId): Future[Unit] = {\n      rms += ContainerId(podName)\n      Future.successful(())\n    }\n    def rm(labels: Map[String, String], ensureUnpause: Boolean = false)(\n      implicit transid: TransactionId): Future[Unit] = {\n      labels.foreach { label =>\n        rmByLabels += ((label._1, label._2))\n      }\n      Future.successful(())\n    }\n\n    def resume(container: KubernetesContainer)(implicit transid: TransactionId): Future[Unit] = {\n      resumes += (container.id)\n      Future.successful({})\n    }\n\n    def suspend(container: KubernetesContainer)(implicit transid: TransactionId): Future[Unit] = {\n      suspends += (container.id)\n      Future.successful({})\n    }\n\n    def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean = false)(\n      implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n      logCalls += ((container.id, sinceTime))\n      Source(List.empty[TypedLogLine])\n    }\n\n    override def addLabel(container: KubernetesContainer, labels: Map[String, String]): Future[Unit] = {\n      Future.successful({})\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/kubernetes/test/KubernetesContainerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.kubernetes.test\n\nimport java.io.IOException\nimport java.time.{Instant, ZoneId}\n\nimport org.apache.pekko.NotUsed\nimport org.apache.pekko.stream.scaladsl.{Flow, Sink, Source}\nimport org.apache.pekko.util.ByteString\nimport common.TimingHelpers\n\nimport scala.concurrent.Await\nimport scala.concurrent.duration._\nimport scala.concurrent.Future\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.matchers.should.Matchers\nimport common.{StreamLogging, WskActorSystem}\nimport spray.json._\nimport org.apache.openwhisk.common.LoggingMarkers._\nimport org.apache.openwhisk.common.LogMarker\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.containerpool.kubernetes._\nimport org.apache.openwhisk.core.containerpool.docker._\nimport org.apache.openwhisk.core.entity.{ActivationResponse, ByteSize}\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.ActivationResponse.ContainerResponse\nimport org.apache.openwhisk.core.entity.ActivationResponse.Timeout\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.core.containerpool.docker.test.DockerContainerTests._\nimport org.scalatest.concurrent.ScalaFutures\n\nimport scala.collection.immutable.Queue\nimport scala.collection.mutable\n\n/**\n * Unit tests for ContainerPool schedule\n */\n@RunWith(classOf[JUnitRunner])\nclass KubernetesContainerTests\n    extends AnyFlatSpec\n    with Matchers\n    with MockFactory\n    with StreamLogging\n    with BeforeAndAfterEach\n    with WskActorSystem\n    with TimingHelpers\n    with ScalaFutures {\n\n  import KubernetesClientTests.TestKubernetesClient\n  import KubernetesContainerTests._\n\n  override def beforeEach() = {\n    stream.reset()\n  }\n\n  def instantDT(instant: Instant): Instant = Instant.from(instant.atZone(ZoneId.of(\"GMT+0\")))\n\n  val Epoch = Instant.EPOCH\n  val EpochDateTime = instantDT(Epoch)\n\n  /** Transforms chunked JsObjects into formatted strings */\n  val toFormattedString: Flow[ByteString, String, NotUsed] =\n    Flow[ByteString].map(_.utf8String.parseJson.convertTo[TypedLogLine].toString)\n\n  val commandTimeout = 500.milliseconds\n  def await[A](f: Future[A], timeout: FiniteDuration = commandTimeout) = Await.result(f, timeout)\n\n  /** Reads logs into memory and awaits them */\n  def awaitLogs(source: Source[ByteString, Any], timeout: FiniteDuration = commandTimeout): Vector[String] =\n    Await.result(source.via(toFormattedString).runWith(Sink.seq[String]), timeout).toVector\n\n  val containerId = ContainerId(\"id\")\n\n  /**\n   * Constructs a testcontainer with overridden IO methods. Results of the override can be provided\n   * as parameters.\n   */\n  def kubernetesContainer(id: ContainerId = containerId, addr: ContainerAddress = ContainerAddress(\"ip\"))(\n    ccRes: Future[RunResult] =\n      Future.successful(RunResult(intervalOf(1.millisecond), Right(ContainerResponse(true, \"\", None)))),\n    awaitLogs: FiniteDuration = 2.seconds)(implicit kubernetes: KubernetesApi): KubernetesContainer = {\n\n    new KubernetesContainer(id, addr, addr.host, \"docker://\" + id.asString) {\n      override protected def callContainer(\n        path: String,\n        body: JsObject,\n        timeout: FiniteDuration,\n        concurrent: Int,\n        maxResponse: ByteSize,\n        truncation: ByteSize,\n        retry: Boolean = false,\n        reschedule: Boolean = false)(implicit transid: TransactionId): Future[RunResult] = {\n        ccRes\n      }\n      override protected val waitForLogs = awaitLogs\n    }\n  }\n\n  behavior of \"KubernetesContainer\"\n\n  implicit val transid = TransactionId.testing\n  val parameters = Map(\n    \"--cap-drop\" -> Set(\"NET_RAW\", \"NET_ADMIN\"),\n    \"--ulimit\" -> Set(\"nofile=1024:1024\"),\n    \"--pids-limit\" -> Set(\"1024\"))\n\n  /*\n   * CONTAINER CREATION\n   */\n  it should \"create a new instance\" in {\n    implicit val kubernetes = new TestKubernetesClient\n\n    val image = \"image\"\n    val userProvidedImage = false\n    val environment = Map(\"test\" -> \"hi\")\n    val labels = Map(\"invoker\" -> \"0\")\n    val name = \"my_Container(1)\"\n    val container = KubernetesContainer.create(\n      transid = transid,\n      image = image,\n      userProvidedImage = userProvidedImage,\n      environment = environment,\n      labels = labels,\n      name = name)\n\n    await(container)\n\n    kubernetes.runs should have size 1\n    kubernetes.rms should have size 0\n\n    val (testName, testImage, testEnv, testLabel) = kubernetes.runs.head\n    testName shouldBe \"my-container1\"\n    testImage shouldBe image\n    testEnv shouldBe environment\n    testLabel shouldBe labels\n  }\n\n  it should \"pull a user provided image before creating the container\" in {\n    implicit val kubernetes = new TestKubernetesClient\n\n    val container =\n      KubernetesContainer.create(transid = transid, name = \"name\", image = \"image\", userProvidedImage = true)\n    await(container)\n\n    kubernetes.runs should have size 1\n    kubernetes.rms should have size 0\n  }\n\n  it should \"provide a proper error if run fails for blackbox containers\" in {\n    implicit val kubernetes = new TestKubernetesClient {\n      override def run(\n        name: String,\n        image: String,\n        memory: ByteSize = 256.MB,\n        env: Map[String, String] = Map.empty,\n        labels: Map[String, String] = Map.empty)(implicit transid: TransactionId): Future[KubernetesContainer] = {\n        Future.failed(ProcessUnsuccessfulException(ExitStatus(1), \"\", \"\"))\n      }\n    }\n\n    val container =\n      KubernetesContainer.create(transid = transid, name = \"name\", image = \"image\", userProvidedImage = true)\n    a[WhiskContainerStartupError] should be thrownBy await(container)\n\n    kubernetes.runs should have size 0\n    kubernetes.rms should have size 1\n  }\n\n  /*\n   * KUBERNETES COMMANDS\n   */\n  it should \"destroy a container via Kubernetes\" in {\n    implicit val kubernetes = stub[KubernetesApi]\n\n    val id = ContainerId(\"id\")\n    val container = new KubernetesContainer(id, ContainerAddress(\"ip\"), \"127.0.0.1\", \"docker://foo\")\n\n    container.destroy()\n\n    (kubernetes.rm(_: KubernetesContainer)(_: TransactionId)).verify(container, transid)\n  }\n\n  /*\n   * INITIALIZE\n   *\n   * Only tests for quite simple cases. Disambiguation of errors is delegated to ActivationResponse\n   * and so are the tests for those.\n   */\n  it should \"initialize a container\" in {\n    implicit val kubernetes = stub[KubernetesApi]\n\n    val initTimeout = 1.second\n    val interval = intervalOf(1.millisecond)\n    val container = kubernetesContainer() {\n      Future.successful(RunResult(interval, Right(ContainerResponse(true, \"\", None))))\n    }\n\n    val initInterval = container.initialize(JsObject.empty, initTimeout, 1)\n    await(initInterval, initTimeout) shouldBe interval\n\n    // assert the starting log is there\n    val start = LogMarker.parse(logLines.head)\n    start.token shouldBe INVOKER_ACTIVATION_INIT\n\n    // assert the end log is there\n    val end = LogMarker.parse(logLines.last)\n    end.token shouldBe INVOKER_ACTIVATION_INIT.asFinish\n    end.deltaToMarkerStart shouldBe Some(interval.duration.toMillis)\n  }\n\n  it should \"properly deal with a timeout during initialization\" in {\n    implicit val kubernetes = stub[KubernetesApi]\n\n    val initTimeout = 1.second\n    val interval = intervalOf(initTimeout + 1.nanoseconds)\n\n    val container = kubernetesContainer() {\n      Future.successful(RunResult(interval, Left(Timeout(new Throwable()))))\n    }\n\n    val init = container.initialize(JsObject.empty, initTimeout, 1)\n\n    val error = the[InitializationError] thrownBy await(init, initTimeout)\n    error.interval shouldBe interval\n    error.response.statusCode shouldBe ActivationResponse.DeveloperError\n\n    // assert the finish log is there\n    val end = LogMarker.parse(logLines.last)\n    end.token shouldBe INVOKER_ACTIVATION_INIT.asFinish\n  }\n\n  /*\n   * RUN\n   *\n   * Only tests for quite simple cases. Disambiguation of errors is delegated to ActivationResponse\n   * and so are the tests for those.\n   */\n  it should \"run a container\" in {\n    implicit val kubernetes = stub[KubernetesApi]\n\n    val interval = intervalOf(1.millisecond)\n    val result = JsObject.empty\n    val container = kubernetesContainer() {\n      Future.successful(RunResult(interval, Right(ContainerResponse(true, result.compactPrint, None))))\n    }\n\n    val runResult = container.run(JsObject.empty, JsObject.empty, 1.second, 1, 1.MB, 1.MB)\n    await(runResult) shouldBe (interval, ActivationResponse.success(Some(result), Some(2)))\n\n    // assert the starting log is there\n    val start = LogMarker.parse(logLines.head)\n    start.token shouldBe INVOKER_ACTIVATION_RUN\n\n    // assert the end log is there\n    val end = LogMarker.parse(logLines.last)\n    end.token shouldBe INVOKER_ACTIVATION_RUN.asFinish\n    end.deltaToMarkerStart shouldBe Some(interval.duration.toMillis)\n  }\n\n  it should \"throw ContainerHealthError if runtime container returns 503 response\" in {\n    implicit val kubernetes = stub[KubernetesApi]\n    val runTimeout = 1.second\n    val interval = intervalOf(1.millisecond)\n    val result = JsObject.empty\n    val container = kubernetesContainer() {\n      Future.successful(RunResult(interval, Right(ContainerResponse(503, result.compactPrint, None))))\n    }\n\n    val initResult = container.initialize(JsObject.empty, 1.second, 1)\n    an[ContainerHealthError] should be thrownBy await(initResult)\n\n    val runResult = container.run(JsObject.empty, JsObject.empty, runTimeout, 1, 1.MB, 1.MB)\n    an[ContainerHealthError] should be thrownBy await(runResult)\n  }\n\n  it should \"properly deal with a timeout during run\" in {\n    implicit val kubernetes = stub[KubernetesApi]\n\n    val runTimeout = 1.second\n    val interval = intervalOf(runTimeout + 1.nanoseconds)\n\n    val container = kubernetesContainer() {\n      Future.successful(RunResult(interval, Left(Timeout(new Throwable()))))\n    }\n\n    val runResult = container.run(JsObject.empty, JsObject.empty, runTimeout, 1, 1.MB, 1.MB)\n    await(runResult) shouldBe (interval, ActivationResponse.developerError(\n      Messages.timedoutActivation(runTimeout, false)))\n\n    // assert the finish log is there\n    val end = LogMarker.parse(logLines.last)\n    end.token shouldBe INVOKER_ACTIVATION_RUN.asFinish\n  }\n\n  /*\n   * LOGS\n   */\n  it should \"read a simple log with sentinel\" in {\n    val expectedLogEntry = TypedLogLine(currentTsp, \"stdout\", \"This is a log entry.\")\n    val logSrc = logSource(expectedLogEntry, appendSentinel = true)\n\n    implicit val kubernetes = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        logCalls += ((container.id, sinceTime))\n        logSrc\n      }\n    }\n\n    val container = kubernetesContainer(id = containerId)()\n    // Read with tight limit to verify that no truncation occurs TODO: Need to figure out how to handle this with the Source-based kubernetes logs\n    val processedLogs = awaitLogs(container.logs(limit = 4096.B, waitForSentinel = true))\n\n    kubernetes.logCalls should have size 1\n    val (id, sinceTime) = kubernetes.logCalls(0)\n    id shouldBe containerId\n    sinceTime shouldBe None\n\n    processedLogs should have size 1\n    processedLogs shouldBe Vector(expectedLogEntry.rawString)\n  }\n\n  it should \"read a simple log without sentinel\" in {\n    val expectedLogEntry = TypedLogLine(currentTsp, \"stdout\", \"This is a log entry.\")\n    val logSrc = logSource(expectedLogEntry, appendSentinel = false)\n\n    implicit val kubernetes = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        logCalls += ((container.id, sinceTime))\n        logSrc\n      }\n    }\n\n    val container = kubernetesContainer(id = containerId)()\n    // Read without tight limit so that the full read result is processed\n    val processedLogs = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = false))\n\n    kubernetes.logCalls should have size 1\n    val (id, sinceTime) = kubernetes.logCalls(0)\n    id shouldBe containerId\n    sinceTime shouldBe None\n\n    processedLogs should have size 1\n    processedLogs shouldBe Vector(expectedLogEntry.rawString)\n  }\n\n  it should \"fail log reading if error occurs during file reading\" in {\n    implicit val kubernetes = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        logCalls += ((container.id, sinceTime))\n        Source.failed(new IOException)\n      }\n    }\n\n    val container = kubernetesContainer()()\n    an[IOException] should be thrownBy awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true))\n\n    kubernetes.logCalls should have size 1\n    val (id, sinceTime) = kubernetes.logCalls(0)\n    id shouldBe containerId\n    sinceTime shouldBe None\n  }\n\n  it should \"read two consecutive logs with sentinel\" in {\n    val firstLog = TypedLogLine(Instant.EPOCH, \"stdout\", \"This is the first log.\")\n    val secondLog = TypedLogLine(Instant.EPOCH.plusSeconds(1l), \"stderr\", \"This is the second log.\")\n    val logSources = mutable.Queue(logSource(firstLog, true), logSource(secondLog, true))\n\n    implicit val kubernetes = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        logCalls += ((container.id, sinceTime))\n        logSources.dequeue()\n      }\n    }\n\n    val container = kubernetesContainer()()\n    // Read without tight limit so that the full read result is processed\n    val processedFirstLog = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true))\n    val processedSecondLog = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true))\n\n    kubernetes.logCalls should have size 2\n    val (_, sinceTime1) = kubernetes.logCalls(0)\n    sinceTime1 shouldBe None\n    val (_, sinceTime2) = kubernetes.logCalls(1)\n    sinceTime2 shouldBe Some(EpochDateTime) // second read should start behind the first line\n\n    processedFirstLog should have size 1\n    processedFirstLog shouldBe Vector(firstLog.rawString)\n    processedSecondLog should have size 1\n    processedSecondLog shouldBe Vector(secondLog.rawString)\n\n  }\n\n  it should \"eventually terminate even if no sentinels can be found\" in {\n    val expectedLog = TypedLogLine(currentTsp, \"stdout\", s\"This is log entry.\")\n    val rawLog = toLogs(expectedLog, appendSentinel = false)\n\n    rawLog should have size 1\n\n    implicit val kubernetes = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        logCalls += ((container.id, sinceTime))\n        // \"Fakes\" an infinite source with only 1 entry\n        Source.tick(0.milliseconds, 10.seconds, rawLog.head)\n      }\n    }\n\n    val waitForLogs = 100.milliseconds\n    val container = kubernetesContainer()(awaitLogs = waitForLogs)\n    // Read without tight limit so that the full read result is processed\n\n    val (interval, processedLog) = durationOf(awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true)))\n\n    interval.toMillis should (be >= waitForLogs.toMillis and be < (waitForLogs * 2).toMillis)\n\n    kubernetes.logCalls should have size 1\n\n    /*    processedLog should have size expectedLog.length\n    processedLog shouldBe expectedLog.map(_.toFormattedString)*/\n  }\n\n  it should \"include an incomplete warning if sentinels have not been found only if we wait for sentinels\" in {\n    val expectedLogEntry =\n      TypedLogLine(currentTsp, \"stdout\", \"This is a log entry.\")\n\n    implicit val kubernetes = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        logCalls += ((container.id, sinceTime))\n        logSource(Queue(expectedLogEntry, expectedLogEntry), appendSentinel = false)\n      }\n    }\n\n    val waitForLogs = 100.milliseconds\n    val container = kubernetesContainer()(awaitLogs = waitForLogs)\n    // Read with tight limit to verify that no truncation occurs\n    val processedLogs = awaitLogs(container.logs(limit = 4096.B, waitForSentinel = true))\n\n    kubernetes.logCalls should have size 1\n    val (id, sinceTime) = kubernetes.logCalls(0)\n    id shouldBe containerId\n    sinceTime shouldBe None\n\n    processedLogs should have size 3\n    processedLogs(0) shouldBe expectedLogEntry.rawString\n    processedLogs(1) shouldBe expectedLogEntry.rawString\n    processedLogs(2) should include(Messages.logFailure)\n\n    val processedLogsFalse = awaitLogs(container.logs(limit = 4096.B, waitForSentinel = false))\n    processedLogsFalse should have size 2\n    processedLogsFalse(0) shouldBe expectedLogEntry.rawString\n    processedLogsFalse(1) shouldBe expectedLogEntry.rawString\n  }\n\n  it should \"strip sentinel lines if it waits or doesn't wait for them\" in {\n    val expectedLogEntry =\n      TypedLogLine(currentTsp, \"stdout\", \"This is a log entry.\")\n\n    implicit val kubernetes = new TestKubernetesClient {\n      override def logs(container: KubernetesContainer, sinceTime: Option[Instant], waitForSentinel: Boolean)(\n        implicit transid: TransactionId): Source[TypedLogLine, Any] = {\n        logCalls += ((container.id, sinceTime))\n        logSource(expectedLogEntry, appendSentinel = true)\n      }\n    }\n\n    val container = kubernetesContainer(id = containerId)()\n    val processedLogs = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = true))\n    processedLogs should have size 1\n    processedLogs(0) shouldBe expectedLogEntry.rawString\n\n    val processedLogsFalse = awaitLogs(container.logs(limit = 1.MB, waitForSentinel = false))\n    processedLogsFalse should have size 1\n    processedLogsFalse(0) shouldBe expectedLogEntry.rawString\n  }\n\n  it should \"delete a pod that failed to start due to KubernetesPodApiException\" in {\n    implicit val kubernetes = new TestKubernetesClient {\n      override def run(\n        name: String,\n        image: String,\n        memory: ByteSize = 256.MB,\n        env: Map[String, String] = Map.empty,\n        labels: Map[String, String] = Map.empty)(implicit transid: TransactionId): Future[KubernetesContainer] = {\n        Future.failed(KubernetesPodApiException(new Exception(\"faking fabric8 failure...\")))\n      }\n    }\n\n    val container =\n      KubernetesContainer.create(transid = transid, name = \"name\", image = \"image\", userProvidedImage = true)\n    container.failed.futureValue shouldBe WhiskContainerStartupError(Messages.resourceProvisionError)\n\n    kubernetes.runs should have size 0\n    kubernetes.rms should have size 1\n  }\n\n  it should \"delete a pod that failed to start due to some other Exception\" in {\n    implicit val kubernetes = new TestKubernetesClient {\n      override def run(\n        name: String,\n        image: String,\n        memory: ByteSize = 256.MB,\n        env: Map[String, String] = Map.empty,\n        labels: Map[String, String] = Map.empty)(implicit transid: TransactionId): Future[KubernetesContainer] = {\n        Future.failed(new Exception(\"faking fabric8 failure...\"))\n      }\n    }\n\n    val container =\n      KubernetesContainer.create(transid = transid, name = \"name\", image = \"image\", userProvidedImage = true)\n    container.failed.futureValue shouldBe a[WhiskContainerStartupError]\n\n    kubernetes.runs should have size 0\n    kubernetes.rms should have size 1\n  }\n  def currentTsp: Instant = Instant.now\n\n}\n\nobject KubernetesContainerTests {\n\n  def logSource(logLine: TypedLogLine, appendSentinel: Boolean): Source[TypedLogLine, Any] =\n    logSource(Queue(logLine), appendSentinel)\n\n  def logSource(logs: Queue[TypedLogLine], appendSentinel: Boolean): Source[TypedLogLine, Any] =\n    Source(toLogs(logs, appendSentinel))\n\n  def toLogs(logLine: TypedLogLine, appendSentinel: Boolean): Queue[TypedLogLine] =\n    toLogs(Queue(logLine), appendSentinel)\n\n  def toLogs(log: Queue[TypedLogLine], appendSentinel: Boolean): Queue[TypedLogLine] =\n    if (appendSentinel) {\n      val lastTime = log.lastOption.map { case TypedLogLine(time, _, _) => time }.getOrElse(Instant.EPOCH)\n      log :+\n        TypedLogLine(lastTime, \"stderr\", s\"${Container.ACTIVATION_LOG_SENTINEL}\") :+\n        TypedLogLine(lastTime, \"stdout\", s\"${Container.ACTIVATION_LOG_SENTINEL}\")\n    } else {\n      log\n    }\n\n  implicit class TypedLogHelper(log: TypedLogLine) {\n    import KubernetesClient.formatK8STimestamp\n\n    def rawString: String = \"%s %s: %s\".format(formatK8STimestamp(log.time).get.trim, log.stream, log.log)\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/kubernetes/test/WhiskPodBuilderTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.kubernetes.test\n\nimport io.fabric8.kubernetes.api.model.policy.PodDisruptionBudgetBuilder\nimport io.fabric8.kubernetes.api.model.{\n  EnvVar,\n  EnvVarSource,\n  IntOrString,\n  LabelSelectorBuilder,\n  ObjectFieldSelector,\n  Pod\n}\nimport io.fabric8.kubernetes.client.utils.Serialization\nimport org.apache.openwhisk.common.{ConfigMapValue, TransactionId}\nimport org.apache.openwhisk.core.containerpool.kubernetes.{\n  KubernetesClientConfig,\n  KubernetesClientTimeoutConfig,\n  KubernetesCpuScalingConfig,\n  KubernetesEphemeralStorageConfig,\n  KubernetesInvokerNodeAffinity,\n  WhiskPodBuilder\n}\nimport org.apache.openwhisk.core.entity.size._\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass WhiskPodBuilderTests extends AnyFlatSpec with Matchers with KubeClientSupport {\n  implicit val tid: TransactionId = TransactionId.testing\n  private val testImage = \"nodejs\"\n  private val memLimit = 10.MB\n  private val name = \"whisk\"\n  private val affinity = KubernetesInvokerNodeAffinity(enabled = true, \"openwhisk-role\", \"invoker\")\n\n  behavior of \"WhiskPodBuilder\"\n\n  def config(configMap: Option[ConfigMapValue] = None, affinity: Option[KubernetesInvokerNodeAffinity] = None) =\n    KubernetesClientConfig(\n      KubernetesClientTimeoutConfig(1.seconds, 2.seconds),\n      affinity.getOrElse(KubernetesInvokerNodeAffinity(false, \"k\", \"v\")),\n      false,\n      None,\n      configMap,\n      Some(KubernetesCpuScalingConfig(300, 3.MB, 1000)),\n      false,\n      Some(Map(\"POD_UID\" -> \"metadata.uid\")),\n      None)\n\n  it should \"build a new pod\" in {\n    val c = config()\n    val builder = new WhiskPodBuilder(kubeClient, c)\n    assertPodSettings(builder, c)\n  }\n  it should \"build set cpu scaled based on memory, if enabled in configuration\" in {\n    val config = KubernetesClientConfig(\n      KubernetesClientTimeoutConfig(1.second, 1.second),\n      KubernetesInvokerNodeAffinity(false, \"k\", \"v\"),\n      true,\n      None,\n      None,\n      Some(KubernetesCpuScalingConfig(300, 3.MB, 1000)),\n      false,\n      None,\n      None)\n    val builder = new WhiskPodBuilder(kubeClient, config)\n\n    val (pod, _) = builder.buildPodSpec(name, testImage, 2.MB, Map(\"foo\" -> \"bar\"), Map(\"fooL\" -> \"barV\"), config)\n    withClue(Serialization.asYaml(pod)) {\n      val c = getActionContainer(pod)\n      //min cpu is: config.millicpus\n      c.getResources.getLimits.asScala.get(\"cpu\").map(_.getAmount) shouldBe Some(\"300\")\n    }\n\n    val (pod2, _) = builder.buildPodSpec(name, testImage, 15.MB, Map(\"foo\" -> \"bar\"), Map(\"fooL\" -> \"barV\"), config)\n    withClue(Serialization.asYaml(pod2)) {\n      val c = getActionContainer(pod2)\n      //max cpu is: config.maxMillicpus\n      c.getResources.getLimits.asScala.get(\"cpu\").map(_.getAmount) shouldBe Some(\"1000\")\n    }\n\n    val (pod3, _) = builder.buildPodSpec(name, testImage, 7.MB, Map(\"foo\" -> \"bar\"), Map(\"fooL\" -> \"barV\"), config)\n    withClue(Serialization.asYaml(pod3)) {\n      val c = getActionContainer(pod3)\n      //scaled cpu is: action mem/config.mem x config.maxMillicpus\n      c.getResources.getLimits.asScala.get(\"cpu\").map(_.getAmount) shouldBe Some(\"600\")\n    }\n\n    val config2 = KubernetesClientConfig(\n      KubernetesClientTimeoutConfig(1.second, 1.second),\n      KubernetesInvokerNodeAffinity(false, \"k\", \"v\"),\n      true,\n      None,\n      None,\n      None,\n      false,\n      None,\n      None)\n    val (pod4, _) = builder.buildPodSpec(name, testImage, 7.MB, Map(\"foo\" -> \"bar\"), Map(\"fooL\" -> \"barV\"), config2)\n    withClue(Serialization.asYaml(pod4)) {\n      val c = getActionContainer(pod4)\n      //if scaling config is not provided, no cpu resources are specified\n      c.getResources.getLimits.asScala.get(\"cpu\").map(_.getAmount) shouldBe None\n    }\n\n  }\n  it should \"set ephemeral storage when configured\" in {\n    val config = KubernetesClientConfig(\n      KubernetesClientTimeoutConfig(1.second, 1.second),\n      KubernetesInvokerNodeAffinity(false, \"k\", \"v\"),\n      true,\n      None,\n      None,\n      Some(KubernetesCpuScalingConfig(300, 3.MB, 1000)),\n      false,\n      None,\n      Some(KubernetesEphemeralStorageConfig(1.GB, 0.0)))\n    val builder = new WhiskPodBuilder(kubeClient, config)\n\n    val (pod, _) = builder.buildPodSpec(name, testImage, 2.MB, Map(\"foo\" -> \"bar\"), Map(\"fooL\" -> \"barV\"), config)\n    withClue(Serialization.asYaml(pod)) {\n      val c = getActionContainer(pod)\n      c.getResources.getLimits.asScala.get(\"ephemeral-storage\").map(_.getAmount) shouldBe Some(\"1024\")\n      c.getResources.getRequests.asScala.get(\"ephemeral-storage\").map(_.getAmount) shouldBe Some(\"1024\")\n    }\n  }\n  it should \"scale ephemeral storage when scale factor is given\" in {\n    val config = KubernetesClientConfig(\n      KubernetesClientTimeoutConfig(1.second, 1.second),\n      KubernetesInvokerNodeAffinity(false, \"k\", \"v\"),\n      true,\n      None,\n      None,\n      Some(KubernetesCpuScalingConfig(300, 3.MB, 1000)),\n      false,\n      None,\n      Some(KubernetesEphemeralStorageConfig(1.GB, 1.25)))\n    val builder = new WhiskPodBuilder(kubeClient, config)\n\n    val (pod, _) = builder.buildPodSpec(name, testImage, 2.MB, Map(\"foo\" -> \"bar\"), Map(\"fooL\" -> \"barV\"), config)\n    withClue(Serialization.asYaml(pod)) {\n      val c = getActionContainer(pod)\n      c.getResources.getLimits.asScala.get(\"ephemeral-storage\").map(_.getAmount) shouldBe Some(\"2.5\")\n      c.getResources.getRequests.asScala.get(\"ephemeral-storage\").map(_.getAmount) shouldBe Some(\"2.5\")\n    }\n  }\n  it should \"use ephemeral storage limit when scale factor suggests larger size\" in {\n    val config = KubernetesClientConfig(\n      KubernetesClientTimeoutConfig(1.second, 1.second),\n      KubernetesInvokerNodeAffinity(false, \"k\", \"v\"),\n      true,\n      None,\n      None,\n      Some(KubernetesCpuScalingConfig(300, 3.MB, 1000)),\n      false,\n      None,\n      Some(KubernetesEphemeralStorageConfig(1.GB, 1000)))\n    val builder = new WhiskPodBuilder(kubeClient, config)\n\n    val (pod, _) = builder.buildPodSpec(name, testImage, 2.MB, Map(\"foo\" -> \"bar\"), Map(\"fooL\" -> \"barV\"), config)\n    withClue(Serialization.asYaml(pod)) {\n      val c = getActionContainer(pod)\n      c.getResources.getLimits.asScala.get(\"ephemeral-storage\").map(_.getAmount) shouldBe Some(\"1024\")\n      c.getResources.getRequests.asScala.get(\"ephemeral-storage\").map(_.getAmount) shouldBe Some(\"1024\")\n    }\n  }\n\n  it should \"extend existing pod template\" in {\n    val template = \"\"\"\n       |---\n       |apiVersion: \"v1\"\n       |kind: \"Pod\"\n       |metadata:\n       |  annotations:\n       |    my-foo : my-bar\n       |  labels:\n       |    my-fool : my-barv\n       |  name: \"testpod\"\n       |  namespace: whiskns\n       |spec:\n       |  containers:\n       |    - name: \"user-action\"\n       |      securityContext:\n       |        capabilities:\n       |          drop:\n       |          - \"TEST_CAP\"\n       |    - name: \"sidecar\"\n       |      image : \"busybox\"\n       |\"\"\".stripMargin\n\n    val c = config(Some(ConfigMapValue(template)))\n    val builder = new WhiskPodBuilder(kubeClient, c)\n    val pod = assertPodSettings(builder, c)\n\n    val ac = getActionContainer(pod)\n    ac.getSecurityContext.getCapabilities.getDrop.asScala should contain(\"TEST_CAP\")\n\n    val sc = pod.getSpec.getContainers.asScala.find(_.getName == \"sidecar\").get\n    sc.getImage shouldBe \"busybox\"\n\n    pod.getMetadata.getLabels.asScala.get(\"my-fool\") shouldBe Some(\"my-barv\")\n    pod.getMetadata.getAnnotations.asScala.get(\"my-foo\") shouldBe Some(\"my-bar\")\n    pod.getMetadata.getNamespace shouldBe \"whiskns\"\n  }\n\n  it should \"build a pod disruption budget for the pod, if enabled\" in {\n    val c = config()\n    val builder = new WhiskPodBuilder(kubeClient, c)\n    assertPodSettings(builder, c)\n  }\n\n  it should \"extend existing pod template with affinity\" in {\n    val template = \"\"\"\n       |apiVersion: \"v1\"\n       |kind: \"Pod\"\n       |spec:\n       |  affinity:\n       |    nodeAffinity:\n       |      requiredDuringSchedulingIgnoredDuringExecution:\n       |        nodeSelectorTerms:\n       |        - matchExpressions:\n       |          - key: \"nodelabel\"\n       |            operator: \"In\"\n       |            values:\n       |            - \"test\"\"\"\".stripMargin\n\n    val c = config(Some(ConfigMapValue(template)), Some(affinity.copy(enabled = true)))\n    val builder =\n      new WhiskPodBuilder(kubeClient, c)\n    val pod = assertPodSettings(builder, c)\n\n    val terms =\n      pod.getSpec.getAffinity.getNodeAffinity.getRequiredDuringSchedulingIgnoredDuringExecution.getNodeSelectorTerms.asScala\n    terms.exists(_.getMatchExpressions.asScala.exists(_.getKey == \"nodelabel\")) shouldBe true\n  }\n\n  private def assertPodSettings(builder: WhiskPodBuilder, config: KubernetesClientConfig): Pod = {\n    val labels = Map(\"fooL\" -> \"barV\")\n    val (pod, pdb) = builder.buildPodSpec(name, testImage, memLimit, Map(\"foo\" -> \"bar\"), labels, config)\n    withClue(Serialization.asYaml(pod)) {\n      val c = getActionContainer(pod)\n      c.getEnv.asScala.shouldBe(Seq(\n        new EnvVar(\"foo\", \"bar\", null),\n        new EnvVar(\"POD_UID\", null, new EnvVarSource(null, new ObjectFieldSelector(null, \"metadata.uid\"), null, null))))\n\n      c.getResources.getLimits.asScala.get(\"memory\").map(_.getAmount) shouldBe Some(\"10\")\n      c.getResources.getLimits.asScala.get(\"cpu\").map(_.getAmount) shouldBe Some(\"900\")\n      c.getSecurityContext.getCapabilities.getDrop.asScala should contain allOf (\"NET_RAW\", \"NET_ADMIN\")\n      c.getPorts.asScala.find(_.getName == \"action\").map(_.getContainerPort) shouldBe Some(8080)\n      c.getImage shouldBe testImage\n\n      pod.getMetadata.getLabels.asScala.get(\"name\") shouldBe Some(name)\n      pod.getMetadata.getLabels.asScala.get(\"fooL\") shouldBe Some(\"barV\")\n      pod.getMetadata.getName shouldBe name\n      pod.getSpec.getRestartPolicy shouldBe \"Always\"\n\n      if (builder.affinityEnabled) {\n        val terms =\n          pod.getSpec.getAffinity.getNodeAffinity.getRequiredDuringSchedulingIgnoredDuringExecution.getNodeSelectorTerms.asScala\n        terms.exists(_.getMatchExpressions.asScala.exists(_.getKey == affinity.key)) shouldBe true\n      }\n    }\n    if (config.pdbEnabled) {\n      println(\"matching pdb...\")\n      pdb shouldBe Some(\n        new PodDisruptionBudgetBuilder().withNewMetadata\n          .withName(name)\n          .addToLabels(labels.asJava)\n          .endMetadata()\n          .withNewSpec()\n          .withMinAvailable(new IntOrString(1))\n          .withSelector(new LabelSelectorBuilder().withMatchLabels(Map(\"name\" -> name).asJava).build())\n          .and\n          .build)\n    }\n    pod\n  }\n\n  private def getActionContainer(pod: Pod) = {\n    pod.getSpec.getContainers.asScala.find(_.getName == \"user-action\").get\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/logging/ElasticSearchLogStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport java.time.ZonedDateTime\n\nimport org.apache.pekko.NotUsed\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.{GET, POST}\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.headers.{Accept, RawHeader}\nimport org.apache.pekko.stream.scaladsl.Flow\nimport org.apache.pekko.testkit.TestKit\nimport common.StreamLogging\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport pureconfig.error.ConfigReaderException\nimport spray.json._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.database.UserContext\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext, Future, Promise}\nimport scala.util.{Success, Try}\n\n@RunWith(classOf[JUnitRunner])\nclass ElasticSearchLogStoreTests\n    extends TestKit(ActorSystem(\"ElasticSearchLogStore\"))\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with ScalaFutures\n    with StreamLogging {\n\n  implicit val ec: ExecutionContext = system.dispatcher\n\n  private val uuid = UUID()\n  private val tenantId = s\"testSpace_${uuid}\"\n  private val user =\n    Identity(Subject(), Namespace(EntityName(tenantId), uuid), BasicAuthenticationAuthKey(uuid, Secret()))\n  private val activationId = ActivationId.generate()\n\n  private val defaultLogSchema =\n    ElasticSearchLogFieldConfig(\"user_logs\", \"message\", \"tenantId\", \"activationId_str\", \"stream_str\", \"time_date\")\n  private val defaultConfig =\n    ElasticSearchLogStoreConfig(\"https\", \"host\", 443, \"/whisk_user_logs/_search\", defaultLogSchema)\n  private val defaultConfigRequiredHeaders =\n    ElasticSearchLogStoreConfig(\n      \"https\",\n      \"host\",\n      443,\n      \"/whisk_user_logs/_search\",\n      defaultLogSchema,\n      Seq(\"x-auth-token\", \"x-auth-project-id\"))\n\n  private val defaultHttpResponse = HttpResponse(\n    StatusCodes.OK,\n    entity = HttpEntity(\n      ContentTypes.`application/json`,\n      s\"\"\"{\"took\":799,\"timed_out\":false,\"_shards\":{\"total\":204,\"successful\":204,\"failed\":0},\"hits\":{\"total\":2,\"max_score\":null,\"hits\":[{\"_index\":\"logstash-2018.03.05.02\",\"_type\":\"user_logs\",\"_id\":\"1c00007f-ecb9-4083-8d2e-4d5e2849621f\",\"_score\":null,\"_source\":{\"time_date\":\"2018-03-05T02:10:38.196689522Z\",\"accountId\":null,\"message\":\"some log stuff\\\\n\",\"type\":\"user_logs\",\"event_uuid\":\"1c00007f-ecb9-4083-8d2e-4d5e2849621f\",\"activationId_str\":\"$activationId\",\"action_str\":\"user@email.com/logs\",\"tenantId\":\"${tenantId}\",\"logmet_cluster\":\"topic1-elasticsearch_1\",\"@timestamp\":\"2018-03-05T02:11:37.687Z\",\"@version\":\"1\",\"stream_str\":\"stdout\",\"timestamp\":\"2018-03-05T02:10:39.131Z\"},\"sort\":[1520215897687]},{\"_index\":\"logstash-2018.03.05.02\",\"_type\":\"user_logs\",\"_id\":\"14c2a5b7-8cad-4ec0-992e-70fab1996465\",\"_score\":null,\"_source\":{\"time_date\":\"2018-03-05T02:10:38.196754258Z\",\"accountId\":null,\"message\":\"more logs\\\\n\",\"type\":\"user_logs\",\"event_uuid\":\"14c2a5b7-8cad-4ec0-992e-70fab1996465\",\"activationId_str\":\"$activationId\",\"action_str\":\"user@email.com/logs\",\"tenantId\":\"tenant\",\"${tenantId}\":\"topic1-elasticsearch_1\",\"@timestamp\":\"2018-03-05T02:11:37.701Z\",\"@version\":\"1\",\"stream_str\":\"stdout\",\"timestamp\":\"2018-03-05T02:10:39.131Z\"},\"sort\":[1520215897701]}]}}\"\"\"))\n  private val defaultPayload = JsObject(\n    \"query\" -> JsObject(\"query_string\" -> JsObject(\"query\" -> JsString(\n      s\"_type: ${defaultConfig.logSchema.userLogs} AND ${defaultConfig.logSchema.tenantId}: $tenantId AND ${defaultConfig.logSchema.activationId}: $activationId\"))),\n    \"sort\" -> JsArray(JsObject(defaultConfig.logSchema.time -> JsObject(\"order\" -> JsString(\"asc\")))),\n    \"from\" -> JsNumber(0)).compactPrint\n  private val defaultHttpRequest = HttpRequest(\n    POST,\n    Uri(s\"/whisk_user_logs/_search\"),\n    List(Accept(MediaTypes.`application/json`)),\n    HttpEntity(ContentTypes.`application/json`, defaultPayload))\n  private val defaultLogStoreHttpRequest =\n    HttpRequest(method = GET, uri = \"https://some.url\", entity = HttpEntity.Empty)\n  private val defaultContext = UserContext(user, defaultLogStoreHttpRequest)\n\n  private val expectedLogs = ActivationLogs(\n    Vector(\"2018-03-05T02:10:38.196689522Z stdout: some log stuff\", \"2018-03-05T02:10:38.196754258Z stdout: more logs\"))\n\n  private val activation = WhiskActivation(\n    namespace = EntityPath(tenantId),\n    name = EntityName(\"name\"),\n    Subject(),\n    activationId = activationId,\n    start = ZonedDateTime.now.toInstant,\n    end = ZonedDateTime.now.toInstant,\n    response = ActivationResponse.success(Some(JsObject(\"res\" -> JsNumber(1)))),\n    logs = expectedLogs,\n    annotations = Parameters(\"limits\", ActionLimits(TimeLimit(1.second), MemoryLimit(128.MB), LogLimit(1.MB)).toJson))\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  private def testFlow(httpResponse: HttpResponse = HttpResponse(), httpRequest: HttpRequest = HttpRequest())\n    : Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), NotUsed] =\n    Flow[(HttpRequest, Promise[HttpResponse])]\n      .mapAsyncUnordered(1) {\n        case (request, userContext) =>\n          request shouldBe httpRequest\n          Future.successful((Success(httpResponse), userContext))\n      }\n\n  private def await[T](awaitable: Future[T], timeout: FiniteDuration = 10.seconds) = Await.result(awaitable, timeout)\n\n  behavior of \"ElasticSearch Log Store\"\n\n  it should \"fail when loading out of box configs since whisk.logstore.elasticsearch does not exist\" in {\n    a[ConfigReaderException[_]] should be thrownBy new ElasticSearchLogStore(system)\n  }\n\n  it should \"get user logs from ElasticSearch when there are no required headers needed\" in {\n    val esLogStore =\n      new ElasticSearchLogStore(\n        system,\n        Some(testFlow(defaultHttpResponse, defaultHttpRequest)),\n        elasticSearchConfig = defaultConfig)\n\n    await(\n      esLogStore.fetchLogs(\n        activation.withoutLogs.namespace.asString,\n        activation.withoutLogs.activationId,\n        None,\n        None,\n        Some(activation.withoutLogs.logs),\n        defaultContext)) shouldBe expectedLogs\n  }\n\n  it should \"get logs from supplied activation record when required headers are not present\" in {\n    val esLogStore =\n      new ElasticSearchLogStore(\n        system,\n        Some(testFlow(defaultHttpResponse, defaultHttpRequest)),\n        elasticSearchConfig = defaultConfigRequiredHeaders)\n\n    await(\n      esLogStore.fetchLogs(\n        activation.namespace.asString,\n        activation.activationId,\n        None,\n        None,\n        Some(activation.logs),\n        defaultContext)) shouldBe expectedLogs\n  }\n\n  it should \"get user logs from ElasticSearch when required headers are needed\" in {\n    val authToken = \"token\"\n    val authProjectId = \"projectId\"\n    val httpRequest = HttpRequest(\n      POST,\n      Uri(s\"/whisk_user_logs/_search\"),\n      List(\n        Accept(MediaTypes.`application/json`),\n        RawHeader(\"x-auth-token\", authToken),\n        RawHeader(\"x-auth-project-id\", authProjectId)),\n      HttpEntity(ContentTypes.`application/json`, defaultPayload))\n    val esLogStore =\n      new ElasticSearchLogStore(\n        system,\n        Some(testFlow(defaultHttpResponse, httpRequest)),\n        elasticSearchConfig = defaultConfigRequiredHeaders)\n    val requiredHeadersHttpRequest = HttpRequest(\n      uri = \"https://some.url\",\n      headers = List(RawHeader(\"x-auth-token\", authToken), RawHeader(\"x-auth-project-id\", authProjectId)),\n      entity = HttpEntity.Empty)\n    val context = UserContext(user, requiredHeadersHttpRequest)\n\n    await(\n      esLogStore.fetchLogs(\n        activation.withoutLogs.namespace.asString,\n        activation.withoutLogs.activationId,\n        None,\n        None,\n        Some(activation.withoutLogs.logs),\n        context)) shouldBe expectedLogs\n  }\n\n  it should \"dynamically replace $UUID in request path\" in {\n    val dynamicPathConfig =\n      ElasticSearchLogStoreConfig(\"https\", \"host\", 443, \"/elasticsearch/logstash-%s*/_search\", defaultLogSchema)\n    val httpRequest = HttpRequest(\n      POST,\n      Uri(s\"/elasticsearch/logstash-${user.namespace.uuid.asString}*/_search\"),\n      List(Accept(MediaTypes.`application/json`)),\n      HttpEntity(ContentTypes.`application/json`, defaultPayload))\n    val esLogStore = new ElasticSearchLogStore(\n      system,\n      Some(testFlow(defaultHttpResponse, httpRequest)),\n      elasticSearchConfig = dynamicPathConfig)\n\n    await(\n      esLogStore.fetchLogs(\n        activation.withoutLogs.namespace.asString,\n        activation.withoutLogs.activationId,\n        None,\n        None,\n        Some(activation.withoutLogs.logs),\n        defaultContext)) shouldBe expectedLogs\n  }\n\n  it should \"fail to connect to invalid host\" in {\n    val esLogStore = new ElasticSearchLogStore(system, elasticSearchConfig = defaultConfig)\n\n    a[Throwable] should be thrownBy await(\n      esLogStore.fetchLogs(\n        activation.namespace.asString,\n        activation.activationId,\n        None,\n        None,\n        Some(activation.logs),\n        defaultContext))\n  }\n\n  it should \"forward errors from ElasticSearch\" in {\n    val httpResponse = HttpResponse(StatusCodes.InternalServerError)\n    val esLogStore =\n      new ElasticSearchLogStore(\n        system,\n        Some(testFlow(httpResponse, defaultHttpRequest)),\n        elasticSearchConfig = defaultConfig)\n\n    a[RuntimeException] should be thrownBy await(\n      esLogStore.fetchLogs(\n        activation.namespace.asString,\n        activation.activationId,\n        None,\n        None,\n        Some(activation.logs),\n        defaultContext))\n  }\n\n  it should \"error when configuration protocol is invalid\" in {\n    val invalidHostConfig =\n      ElasticSearchLogStoreConfig(\"protocol\", \"host\", 443, \"/whisk_user_logs\", defaultLogSchema, Seq.empty)\n\n    a[IllegalArgumentException] should be thrownBy new ElasticSearchLogStore(\n      system,\n      elasticSearchConfig = invalidHostConfig)\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/logging/ElasticSearchRestClientTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport spray.json._\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.NotUsed\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.headers.Accept\nimport org.apache.pekko.stream.scaladsl.Flow\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.POST\nimport common.StreamLogging\nimport org.apache.openwhisk.core.containerpool.logging.ElasticSearchJsonProtocol._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext, Future, Promise}\nimport scala.util.{Success, Try}\n\n@RunWith(classOf[JUnitRunner])\nclass ElasticSearchRestClientTests\n    extends TestKit(ActorSystem(\"ElasticSearchRestClient\"))\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with ScalaFutures\n    with StreamLogging {\n\n  implicit val ec: ExecutionContext = system.dispatcher\n\n  private val defaultResponseSource =\n    \"\"\"{\"stream\":\"stdout\",\"activationId\":\"197d60b33137424ebd60b33137d24ea3\",\"action\":\"guest/someAction\",\"@version\":\"1\",\"@timestamp\":\"2018-03-27T15:48:09.112Z\",\"type\":\"user_logs\",\"tenant\":\"19bc46b1-71f6-4ed5-8c54-816aa4f8c502\",\"message\":\"namespace     : user@email.com\\n\",\"time_date\":\"2018-03-27T15:48:08.716152793Z\"}\"\"\"\n  private val defaultResponse =\n    s\"\"\"{\"took\":2,\"timed_out\":false,\"_shards\":{\"total\":5,\"successful\":5,\"failed\":0},\"hits\":{\"total\":1375,\"max_score\":1.0,\"hits\":[{\"_index\":\"whisk_user_logs\",\"_type\":\"user_logs\",\"_id\":\"AWJoJSwAMGbzgxiD1jr9\",\"_score\":1.0,\"_source\":$defaultResponseSource}]}}\"\"\"\n  private val defaultHttpResponse = HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, defaultResponse))\n  private val defaultHttpRequest = HttpRequest(\n    POST,\n    headers = List(Accept(MediaTypes.`application/json`)),\n    entity = HttpEntity(ContentTypes.`application/json`, EsQuery(EsQueryAll()).toJson.toString))\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  private def testFlow(httpResponse: HttpResponse = HttpResponse(), httpRequest: HttpRequest = HttpRequest())\n    : Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), NotUsed] =\n    Flow[(HttpRequest, Promise[HttpResponse])]\n      .mapAsyncUnordered(1) {\n        case (request, userContext) =>\n          request shouldBe httpRequest\n          Future.successful((Success(httpResponse), userContext))\n      }\n\n  private def await[T](awaitable: Future[T], timeout: FiniteDuration = 10.seconds) = Await.result(awaitable, timeout)\n\n  behavior of \"ElasticSearch Rest Client\"\n\n  it should \"construct a query with must\" in {\n    val queryTerms = Vector(EsQueryBoolMatch(\"someKey1\", \"someValue1\"), EsQueryBoolMatch(\"someKey2\", \"someValue2\"))\n    val queryMust = EsQueryMust(queryTerms)\n\n    EsQuery(queryMust).toJson shouldBe JsObject(\n      \"query\" ->\n        JsObject(\n          \"bool\" ->\n            JsObject(\n              \"must\" ->\n                JsArray(\n                  JsObject(\"match\" -> JsObject(\"someKey1\" -> JsString(\"someValue1\"))),\n                  JsObject(\"match\" -> JsObject(\"someKey2\" -> JsString(\"someValue2\")))))),\n      \"from\" -> 0.toJson)\n\n    // Test must with ranges\n    Seq((EsRangeGte, \"gte\"), (EsRangeGt, \"gt\"), (EsRangeLte, \"lte\"), (EsRangeLt, \"lt\")).foreach {\n      case (rangeArg, rangeValue) =>\n        val queryRange1 = EsQueryRange(\"someKey1\", rangeArg, \"someValue1\")\n        val queryRange2 = EsQueryRange(\"someKey2\", rangeArg, \"someValue2\")\n        val queryTerms = Vector(EsQueryBoolMatch(\"someKey1\", \"someValue1\"), EsQueryBoolMatch(\"someKey2\", \"someValue2\"))\n        val queryMust = EsQueryMust(queryTerms, Vector(queryRange1, queryRange2))\n\n        EsQuery(queryMust).toJson shouldBe JsObject(\n          \"query\" ->\n            JsObject(\n              \"bool\" ->\n                JsObject(\n                  \"must\" ->\n                    JsArray(\n                      JsObject(\"match\" -> JsObject(\"someKey1\" -> JsString(\"someValue1\"))),\n                      JsObject(\"match\" -> JsObject(\"someKey2\" -> JsString(\"someValue2\")))),\n                  \"filter\" ->\n                    JsArray(\n                      JsObject(\"range\" -> JsObject(\"someKey1\" -> JsObject(rangeValue -> \"someValue1\".toJson))),\n                      JsObject(\"range\" -> JsObject(\"someKey2\" -> JsObject(rangeValue -> \"someValue2\".toJson)))))),\n          \"from\" -> 0.toJson)\n    }\n  }\n\n  it should \"construct a query with aggregations\" in {\n    Seq((EsAggMax, \"max\"), (EsAggMin, \"min\")).foreach {\n      case (aggArg, aggValue) =>\n        val queryAgg = EsQueryAggs(\"someAgg\", aggArg, \"someField\")\n\n        EsQuery(EsQueryAll(), aggs = Some(queryAgg)).toJson shouldBe JsObject(\n          \"query\" -> JsObject(\"match_all\" -> JsObject.empty),\n          \"aggs\" -> JsObject(\"someAgg\" -> JsObject(aggValue -> JsObject(\"field\" -> \"someField\".toJson))),\n          \"from\" -> 0.toJson)\n    }\n  }\n\n  it should \"construct a query with match\" in {\n    val queryMatch = EsQueryMatch(\"someField\", \"someValue\")\n\n    EsQuery(queryMatch).toJson shouldBe JsObject(\n      \"query\" -> JsObject(\"match\" -> JsObject(\"someField\" -> JsObject(\"query\" -> \"someValue\".toJson))),\n      \"from\" -> 0.toJson)\n\n    // Test match with types\n    Seq((EsMatchPhrase, \"phrase\"), (EsMatchPhrasePrefix, \"phrase_prefix\")).foreach {\n      case (typeArg, typeValue) =>\n        val queryMatch = EsQueryMatch(\"someField\", \"someValue\", Some(typeArg))\n\n        EsQuery(queryMatch).toJson shouldBe JsObject(\n          \"query\" -> JsObject(\n            \"match\" -> JsObject(\"someField\" -> JsObject(\"query\" -> \"someValue\".toJson, \"type\" -> typeValue.toJson))),\n          \"from\" -> 0.toJson)\n    }\n  }\n\n  it should \"construct a query with term\" in {\n    val queryTerm = EsQueryTerm(\"user\", \"someUser\")\n\n    EsQuery(queryTerm).toJson shouldBe JsObject(\n      \"query\" -> JsObject(\"term\" -> JsObject(\"user\" -> JsString(\"someUser\"))),\n      \"from\" -> 0.toJson)\n  }\n\n  it should \"construct a query with query string\" in {\n    val queryString = EsQueryString(\"_type: someType\")\n\n    EsQuery(queryString).toJson shouldBe JsObject(\n      \"query\" -> JsObject(\"query_string\" -> JsObject(\"query\" -> JsString(\"_type: someType\"))),\n      \"from\" -> 0.toJson)\n  }\n\n  it should \"construct a query with order\" in {\n    Seq((EsOrderAsc, \"asc\"), (EsOrderDesc, \"desc\")).foreach {\n      case (orderArg, orderValue) =>\n        val queryOrder = EsQueryOrder(\"someField\", orderArg)\n\n        EsQuery(EsQueryAll(), Some(queryOrder)).toJson shouldBe JsObject(\n          \"query\" -> JsObject(\"match_all\" -> JsObject.empty),\n          \"sort\" -> JsArray(JsObject(\"someField\" -> JsObject(\"order\" -> orderValue.toJson))),\n          \"from\" -> 0.toJson)\n    }\n  }\n\n  it should \"construct query with size\" in {\n    EsQuery(EsQueryAll(), size = Some(1)).toJson shouldBe JsObject(\n      \"query\" -> JsObject(\"match_all\" -> JsObject.empty),\n      \"size\" -> 1.toJson,\n      \"from\" -> 0.toJson)\n  }\n\n  it should \"construct query with from\" in {\n    EsQuery(EsQueryAll(), from = 1).toJson shouldBe JsObject(\n      \"query\" -> JsObject(\"match_all\" -> JsObject.empty),\n      \"from\" -> 1.toJson)\n  }\n\n  it should \"error when search response does not match expected type\" in {\n    val esClient = new ElasticSearchRestClient(\"https\", \"host\", 443, Some(testFlow(httpRequest = defaultHttpRequest)))\n\n    a[RuntimeException] should be thrownBy await(esClient.search[JsObject](\"/\"))\n  }\n\n  it should \"parse search response into EsSearchResult\" in {\n    val esClient =\n      new ElasticSearchRestClient(\"https\", \"host\", 443, Some(testFlow(defaultHttpResponse, defaultHttpRequest)))\n    val response = await(esClient.search[EsSearchResult](\"/\"))\n\n    response shouldBe 'right\n    response.right.get.hits.hits should have size 1\n    response.right.get.hits.total shouldBe 1375\n    response.right.get.hits.hits(0).source shouldBe defaultResponseSource.parseJson.asJsObject\n  }\n\n  it should \"return status code when HTTP error occurs\" in {\n    val httpResponse = HttpResponse(StatusCodes.InternalServerError)\n    val esClient = new ElasticSearchRestClient(\"https\", \"host\", 443, Some(testFlow(httpResponse, defaultHttpRequest)))\n    val response = await(esClient.search[JsObject](\"/\"))\n\n    response shouldBe 'left\n    response.left.get shouldBe StatusCodes.InternalServerError\n  }\n\n  it should \"perform info request\" in {\n    val responseBody = s\"\"\"{\"cluster_name\" : \"elasticsearch\"}\"\"\"\n    val httpRequest = HttpRequest(headers = List(Accept(MediaTypes.`application/json`)))\n    val httpResponse = HttpResponse(StatusCodes.OK, entity = HttpEntity(ContentTypes.`application/json`, responseBody))\n    val esClient =\n      new ElasticSearchRestClient(\"https\", \"host\", 443, Some(testFlow(httpResponse, httpRequest)))\n    val response = await(esClient.info())\n\n    response shouldBe 'right\n    response.right.get shouldBe responseBody.parseJson.asJsObject\n  }\n\n  it should \"perform index request\" in {\n    val responseBody = s\"\"\"{\"some_index\" : {}}\"\"\"\n    val httpRequest = HttpRequest(uri = Uri(\"some_index\"), headers = List(Accept(MediaTypes.`application/json`)))\n    val httpResponse = HttpResponse(StatusCodes.OK, entity = HttpEntity(ContentTypes.`application/json`, responseBody))\n    val esClient =\n      new ElasticSearchRestClient(\"https\", \"host\", 443, Some(testFlow(httpResponse, httpRequest)))\n    val response = await(esClient.index(\"some_index\"))\n\n    response shouldBe 'right\n    response.right.get shouldBe responseBody.parseJson.asJsObject\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/logging/LogDriverLogStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.TestKit\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.containerpool.ContainerArgsConfig\n\n@RunWith(classOf[JUnitRunner])\nclass LogDriverLogStoreTests\n    extends TestKit(ActorSystem(\"LogDriverLogStore\"))\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll {\n\n  val testConfig = ContainerArgsConfig(\n    network = \"network\",\n    extraArgs =\n      Map(\"log-driver\" -> Set(\"fluentd\"), \"log-opt\" -> Set(\"fluentd-address=localhost:24225\", \"tag=OW_CONTAINER\")))\n  behavior of \"LogDriver LogStore\"\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  it should \"set the container parameters from the config\" in {\n    val logDriverLogStore = new LogDriverLogStore(system)\n    logDriverLogStore.containerParameters shouldBe Map.empty\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/logging/SplunkLogStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging\n\nimport java.time.ZonedDateTime\n\nimport org.apache.pekko.NotUsed\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.javadsl.model.headers.Authorization\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.POST\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.headers.RawHeader\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport org.apache.pekko.stream.scaladsl.Flow\nimport org.apache.pekko.testkit.TestKit\nimport common.StreamLogging\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport pureconfig.error.ConfigReaderException\nimport spray.json._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.database.UserContext\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext, Future, Promise}\nimport scala.util.{Failure, Success, Try}\n\n@RunWith(classOf[JUnitRunner])\nclass SplunkLogStoreTests\n    extends TestKit(ActorSystem(\"SplunkLogStore\"))\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with ScalaFutures\n    with StreamLogging {\n\n  def await[T](awaitable: Future[T], timeout: FiniteDuration = 10.seconds) = Await.result(awaitable, timeout)\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  val testConfig = SplunkLogStoreConfig(\n    \"splunk-host\",\n    8080,\n    \"splunk-user\",\n    \"splunk-pass\",\n    \"splunk-index\",\n    \"log_timestamp\",\n    \"log_stream\",\n    \"log_message\",\n    \"namespace\",\n    \"activation_id\",\n    \"somefield::somevalue\",\n    10.seconds,\n    7.days,\n    22.seconds,\n    disableSNI = false)\n\n  behavior of \"Splunk LogStore\"\n\n  val startTime = \"2007-12-03T10:15:30Z\"\n  val startTimePlusOffset = \"2007-12-03T10:15:08Z\" //queried end time range is endTime-22\n  val endTime = \"2007-12-03T10:15:45Z\"\n  val endTimePlusOffset = \"2007-12-03T10:16:07Z\" //queried end time range is endTime+22\n  val uuid = UUID()\n  val user =\n    Identity(Subject(), Namespace(EntityName(\"testSpace\"), uuid), BasicAuthenticationAuthKey(uuid, Secret()))\n  val request = HttpRequest(\n    method = POST,\n    uri = \"https://some.url\",\n    headers = List(RawHeader(\"key\", \"value\")),\n    entity = HttpEntity(MediaTypes.`application/json`, JsObject.empty.compactPrint))\n\n  val activation = WhiskActivation(\n    namespace = EntityPath(\"ns\"),\n    name = EntityName(\"a\"),\n    Subject(),\n    activationId = ActivationId.generate(),\n    start = ZonedDateTime.parse(startTime).toInstant,\n    end = ZonedDateTime.parse(endTime).toInstant,\n    response = ActivationResponse.success(Some(JsObject(\"res\" -> JsNumber(1)))),\n    annotations = Parameters(\"limits\", ActionLimits(TimeLimit(1.second), MemoryLimit(128.MB), LogLimit(1.MB)).toJson),\n    duration = Some(123))\n\n  val context = UserContext(user, request)\n\n  implicit val ec: ExecutionContext = system.dispatcher\n\n  val testFlow: Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), NotUsed] =\n    Flow[(HttpRequest, Promise[HttpResponse])]\n      .mapAsyncUnordered(1) {\n        case (request, userContext) =>\n          //we use cachedHostConnectionPoolHttps so won't get the host+port with the request\n          Unmarshal(request.entity)\n            .to[FormData]\n            .map { form =>\n              val earliestTime = form.fields.get(\"earliest_time\")\n              val latestTime = form.fields.get(\"latest_time\")\n              val outputMode = form.fields.get(\"output_mode\")\n              val search = form.fields.get(\"search\")\n              val execMode = form.fields.get(\"exec_mode\")\n              val maxTime = form.fields.get(\"max_time\")\n\n              request.uri.path.toString() shouldBe \"/services/search/jobs\"\n              request.headers shouldBe List(Authorization.basic(testConfig.username, testConfig.password))\n              earliestTime shouldBe Some(startTimePlusOffset)\n              latestTime shouldBe Some(endTimePlusOffset)\n              outputMode shouldBe Some(\"json\")\n              execMode shouldBe Some(\"oneshot\")\n              maxTime shouldBe Some(\"10\")\n              search shouldBe Some(\n                s\"\"\"search index=\"${testConfig.index}\" | search ${testConfig.queryConstraints} | search ${testConfig.namespaceField}=${activation.namespace.asString} | search ${testConfig.activationIdField}=${activation.activationId.toString} | spath ${testConfig.logMessageField} | table ${testConfig.logTimestampField}, ${testConfig.logStreamField}, ${testConfig.logMessageField} | reverse\"\"\")\n\n              (\n                Success(\n                  HttpResponse(\n                    StatusCodes.OK,\n                    entity = HttpEntity(\n                      ContentTypes.`application/json`,\n                      \"\"\"{\"preview\":false,\"init_offset\":0,\"messages\":[],\"fields\":[{\"name\":\"log_message\"}],\"results\":[{\"log_timestamp\": \"2007-12-03T10:15:30Z\", \"log_stream\":\"stdout\", \"log_message\":\"some log message\"},{\"log_timestamp\": \"2007-12-03T10:15:31Z\", \"log_stream\":\"stderr\", \"log_message\":\"some other log message\"},{},{\"log_timestamp\": \"2007-12-03T10:15:32Z\", \"log_stream\":\"stderr\"}], \"highlighted\":{}}\"\"\"))),\n                userContext)\n            }\n            .recover {\n              case e =>\n                println(\"failed\")\n                (Failure(e), userContext)\n            }\n      }\n  val failFlow: Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), NotUsed] =\n    Flow[(HttpRequest, Promise[HttpResponse])]\n      .map {\n        case (request, userContext) =>\n          (Success(HttpResponse(StatusCodes.InternalServerError)), userContext)\n\n      }\n\n  it should \"fail when loading out of box configs (because whisk.logstore.splunk doesn't exist)\" in {\n    a[ConfigReaderException[_]] should be thrownBy new SplunkLogStore(system)\n  }\n\n  it should \"find logs based on activation timestamps\" in {\n    //use the a flow that asserts the request structure and provides a response in the expected format\n    val splunkStore = new SplunkLogStore(system, Some(testFlow), testConfig)\n    val result = await(\n      splunkStore.fetchLogs(\n        activation.namespace.asString,\n        activation.activationId,\n        Some(activation.start),\n        Some(activation.end),\n        None,\n        context))\n    result shouldBe ActivationLogs(\n      Vector(\n        \"2007-12-03T10:15:30Z           stdout: some log message\",\n        \"2007-12-03T10:15:31Z           stderr: some other log message\",\n        \"The log message can't be retrieved, key not found: log_timestamp\",\n        \"The log message can't be retrieved, key not found: log_message\"))\n  }\n\n  it should \"fail to connect to bogus host\" in {\n    //use the default http flow with the default bogus-host config\n    val splunkStore = new SplunkLogStore(system, splunkConfig = testConfig)\n    a[Throwable] should be thrownBy await(\n      splunkStore.fetchLogs(activation.namespace.asString, activation.activationId, None, None, None, context))\n  }\n\n  it should \"display an error if API cannot be reached\" in {\n    //use a flow that generates a 500 response\n    val splunkStore = new SplunkLogStore(system, Some(failFlow), testConfig)\n    a[RuntimeException] should be thrownBy await(\n      splunkStore.fetchLogs(activation.namespace.asString, activation.activationId, None, None, None, context))\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/logging/test/DockerToActivationFileLogStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging.test\n\nimport java.time.Instant\n\nimport org.apache.pekko.stream.scaladsl.{Flow, Sink, Source}\nimport org.apache.pekko.testkit.TestProbe\nimport org.apache.pekko.util.ByteString\nimport common.{StreamLogging, WskActorSystem}\nimport org.junit.runner.RunWith\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.logging.{DockerToActivationFileLogStore, LogCollectingException, LogLine}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.Messages\n\n/**\n * Includes the tests for the DockerToActivationLogStore since the behavior towards the activation storage should\n * remain exactly the same.\n */\n@RunWith(classOf[JUnitRunner])\nclass DockerToActivationFileLogStoreTests\n    extends DockerToActivationLogStoreTests\n    with Matchers\n    with WskActorSystem\n    with StreamLogging {\n\n  override def createStore() = new TestLogStoreTo(Sink.ignore)\n\n  def toLoggedEvent(line: LogLine,\n                    namespace: Namespace,\n                    activationId: ActivationId,\n                    actionName: FullyQualifiedEntityName): String = {\n    val event = line.toJson.compactPrint\n    val concatenated =\n      s\"\"\",\"activationId\":\"${activationId.asString}\",\"action\":\"${actionName.asString}\",\"namespace\":\"${namespace.name.asString}\",\"namespaceId\":\"${namespace.uuid.asString}\"\"\"\"\n\n    event.dropRight(1) ++ concatenated ++ \"}\\n\"\n  }\n\n  def toLoggedActivation(activation: WhiskActivation): String = {\n    JsObject(activation.toJson.fields ++ Map(\"namespaceId\" -> user.namespace.uuid.asString.toJson)).compactPrint + \"\\n\"\n  }\n\n  behavior of \"DockerToActivationFileLogStore\"\n\n  it should \"read logs returned by the container,in mem and enrich + write them to the provided sink\" in {\n    val logs = List(LogLine(Instant.now.toString, \"stdout\", \"this is just a test\"))\n\n    val testSource: Source[ByteString, _] = Source(logs.map(line => ByteString(line.toJson.compactPrint)))\n\n    val testActor = TestProbe()\n\n    val container = new TestContainer(testSource)\n    val store =\n      new TestLogStoreTo(Flow[ByteString].map(_.utf8String).to(Sink.actorRef(testActor.ref, (), (_: Throwable) => _)))\n\n    val collected = store.collectLogs(TransactionId.testing, user, successfulActivation, container, action)\n\n    await(collected) shouldBe ActivationLogs(logs.map(_.toFormattedString).toVector)\n    logs.foreach { line =>\n      testActor.expectMsg(\n        toLoggedEvent(line, user.namespace, successfulActivation.activationId, action.fullyQualifiedName(false)))\n    }\n\n    // Last message should be the full activation\n    testActor.expectMsg(toLoggedActivation(successfulActivation))\n  }\n\n  it should \"read logs with log collecting error with developer error\" in {\n    val logs = List(\n      LogLine(Instant.now.toString, \"stdout\", \"this is a log\"),\n      LogLine(Instant.now.toString, \"stderr\", Messages.logFailure))\n\n    val testActor = TestProbe()\n\n    val container = new TestContainer(Source(toByteString(logs)))\n    val store =\n      new TestLogStoreTo(Flow[ByteString].map(_.utf8String).to(Sink.actorRef(testActor.ref, (), (_: Throwable) => _)))\n\n    val ex = the[LogCollectingException] thrownBy await(\n      store.collectLogs(TransactionId.testing, user, developerErrorActivation, container, action))\n    val collectedLogs = ex.partialLogs.logs\n\n    withClue(\"Collected logs should match provided logs:\") {\n      collectedLogs.dropRight(1) shouldBe logs.map(_.toFormattedString).toVector\n    }\n\n    withClue(\"Last line should end with developer error warning:\") {\n      val lastLogLine = collectedLogs.last\n      lastLogLine should endWith(Messages.logWarningDeveloperError)\n    }\n\n    withClue(\"Provided logs should be received by log store:\") {\n      logs.foreach { line =>\n        testActor.expectMsg(\n          toLoggedEvent(line, user.namespace, developerErrorActivation.activationId, action.fullyQualifiedName(false)))\n      }\n    }\n\n    withClue(\"Last line received by log store should contain developer error warning:\") {\n      testActor.expectMsgPF() {\n        case s: String =>\n          val ll = s.parseJson.convertTo[LogLine]\n          ll.log shouldBe Messages.logWarningDeveloperError\n        case _ => fail()\n      }\n    }\n\n    withClue(\"Last message received by log store should be the activation record:\") {\n      testActor\n        .expectMsg(toLoggedActivation(developerErrorActivation))\n    }\n  }\n\n  class TestLogStoreTo(override val writeToFile: Sink[ByteString, _])\n      extends DockerToActivationFileLogStore(actorSystem)\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/logging/test/DockerToActivationLogStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.logging.test\n\nimport org.apache.pekko.actor.ActorSystem\nimport common.{StreamLogging, WskActorSystem}\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.containerpool.logging.{\n  DockerToActivationLogStoreProvider,\n  LogCollectingException,\n  LogLine\n}\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport java.time.Instant\n\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.util.ByteString\nimport spray.json._\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.containerpool.{Container, ContainerAddress, ContainerId}\nimport org.apache.openwhisk.http.Messages\n\nimport scala.concurrent.{Await, ExecutionContext, Future}\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass DockerToActivationLogStoreTests extends AnyFlatSpec with Matchers with WskActorSystem with StreamLogging {\n  def await[T](future: Future[T]) = Await.result(future, 1.minute)\n\n  val uuid = UUID()\n  val user =\n    Identity(Subject(), Namespace(EntityName(\"testSpace\"), uuid), BasicAuthenticationAuthKey(uuid, Secret()))\n  val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n  val action = ExecutableWhiskAction(user.namespace.name.toPath, EntityName(\"actionName\"), exec)\n  val successfulActivation =\n    WhiskActivation(\n      user.namespace.name.toPath,\n      action.name,\n      user.subject,\n      ActivationId.generate(),\n      Instant.EPOCH,\n      Instant.EPOCH)\n  val developerErrorActivation = successfulActivation.copy(response = ActivationResponse.developerError(\"failed\"))\n\n  def toByteString(logs: List[LogLine]) = logs.map(_.toJson.compactPrint).map(ByteString.apply)\n\n  val tid = TransactionId.testing\n\n  def createStore() = DockerToActivationLogStoreProvider.instance(actorSystem)\n\n  behavior of \"DockerLogStore\"\n\n  it should \"read logs into a sequence and parse them into the specified format\" in {\n    val store = createStore()\n\n    val logs = List(\n      LogLine(Instant.now.toString, \"stdout\", \"this is a log\"),\n      LogLine(Instant.now.toString, \"stdout\", \"this is a log too\"))\n    val container = new TestContainer(Source(toByteString(logs)))\n\n    await(store.collectLogs(tid, user, successfulActivation, container, action)) shouldBe ActivationLogs(\n      logs.map(_.toFormattedString).toVector)\n  }\n\n  it should \"read logs into a sequence and parse them into the specified format with developer error\" in {\n    val store = createStore()\n\n    val logs = List(\n      LogLine(Instant.now.toString, \"stdout\", \"this is a log\"),\n      LogLine(Instant.now.toString, \"stdout\", \"this is a log too\"))\n    val container = new TestContainer(Source(toByteString(logs)))\n\n    val collectedLogs = await(store.collectLogs(tid, user, developerErrorActivation, container, action)).logs\n\n    withClue(\"Collected logs should be empty:\") {\n      collectedLogs.dropRight(1) shouldBe logs.map(_.toFormattedString).toVector\n    }\n\n    withClue(\"Last line should end with developer error warning:\") {\n      val lastLogLine = collectedLogs.last\n      lastLogLine should endWith(Messages.logWarningDeveloperError)\n    }\n  }\n\n  it should \"accept an empty log\" in {\n    val store = createStore()\n\n    val logs = List.empty[LogLine]\n    val container = new TestContainer(Source(toByteString(logs)))\n\n    await(store.collectLogs(tid, user, successfulActivation, container, action)) shouldBe ActivationLogs(\n      Vector.empty[String])\n  }\n\n  it should \"accept an empty log with developer error\" in {\n    val store = createStore()\n\n    val logs = List.empty[LogLine]\n    val container = new TestContainer(Source(toByteString(logs)))\n\n    val collectedLogs = await(store.collectLogs(tid, user, developerErrorActivation, container, action)).logs\n\n    withClue(\"Collected logs should be empty:\") {\n      collectedLogs.dropRight(1) shouldBe Vector.empty[String]\n    }\n\n    withClue(\"Last line should end with developer error warning:\") {\n      val lastLogLine = collectedLogs.last\n      lastLogLine should endWith(Messages.logWarningDeveloperError)\n    }\n  }\n\n  it should \"report an error if the logs contain an 'official' notice of such\" in {\n    val store = createStore()\n\n    val logs = List(\n      LogLine(Instant.now.toString, \"stdout\", \"this is a log\"),\n      LogLine(Instant.now.toString, \"stderr\", Messages.logFailure))\n    val container = new TestContainer(Source(toByteString(logs)))\n\n    val ex = the[LogCollectingException] thrownBy await(\n      store.collectLogs(tid, user, successfulActivation, container, action))\n    ex.partialLogs shouldBe ActivationLogs(logs.map(_.toFormattedString).toVector)\n  }\n\n  it should \"report an error if the logs contain an 'official' notice of such with developer error\" in {\n    val store = createStore()\n\n    val logs = List(\n      LogLine(Instant.now.toString, \"stdout\", \"this is a log\"),\n      LogLine(Instant.now.toString, \"stderr\", Messages.logFailure))\n    val container = new TestContainer(Source(toByteString(logs)))\n\n    val ex = the[LogCollectingException] thrownBy await(\n      store.collectLogs(tid, user, developerErrorActivation, container, action))\n    val collectedLogs = ex.partialLogs.logs\n\n    withClue(\"Collected logs should match provided logs:\") {\n      collectedLogs.dropRight(1) shouldBe logs.map(_.toFormattedString).toVector\n    }\n\n    withClue(\"Last line should end with developer error warning:\") {\n      val lastLogLine = collectedLogs.last\n      lastLogLine should endWith(Messages.logWarningDeveloperError)\n    }\n  }\n\n  it should \"report an error if logs have been truncated\" in {\n    val store = createStore()\n\n    val logs = List(\n      LogLine(Instant.now.toString, \"stdout\", \"this is a log\"),\n      LogLine(Instant.now.toString, \"stderr\", Messages.truncateLogs(action.limits.logs.asMegaBytes)))\n    val container = new TestContainer(Source(toByteString(logs)))\n\n    val ex = the[LogCollectingException] thrownBy await(\n      store.collectLogs(tid, user, successfulActivation, container, action))\n    ex.partialLogs shouldBe ActivationLogs(logs.map(_.toFormattedString).toVector)\n  }\n\n  class TestContainer(lines: Source[ByteString, Any],\n                      val id: ContainerId = ContainerId(\"test\"),\n                      val addr: ContainerAddress = ContainerAddress(\"test\", 1234))(implicit val ec: ExecutionContext,\n                                                                                   val logging: Logging)\n      extends Container {\n    override def suspend()(implicit transid: TransactionId): Future[Unit] = ???\n    override def resume()(implicit transid: TransactionId): Future[Unit] = ???\n\n    def logs(limit: ByteSize, waitForSentinel: Boolean)(implicit transid: TransactionId) = lines\n\n    override implicit protected val as: ActorSystem = actorSystem\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerArgsConfigTest.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.test\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.ContainerArgsConfig\n\n@RunWith(classOf[JUnitRunner])\nclass ContainerArgsConfigTest extends AnyFlatSpec with Matchers {\n\n  it should \"use defaults for container args map\" in {\n    val config = loadConfigOrThrow[ContainerArgsConfig](ConfigKeys.containerArgs)\n\n    //check defaults\n    config.network shouldBe \"bridge\"\n    config.dnsServers shouldBe Seq[String]()\n    config.dnsSearch shouldBe Seq[String]()\n    config.dnsOptions shouldBe Seq[String]()\n    config.extraArgs shouldBe Map[String, Set[String]]()\n  }\n\n  it should \"override defaults from system properties\" in {\n    System.setProperty(\"whisk.container-factory.container-args.extra-args.label.0\", \"l1\")\n    System.setProperty(\"whisk.container-factory.container-args.extra-args.label.1\", \"l2\")\n    System.setProperty(\"whisk.container-factory.container-args.extra-args.label.3\", \"l3\")\n    System.setProperty(\"whisk.container-factory.container-args.extra-args.env.0\", \"e1\")\n    System.setProperty(\"whisk.container-factory.container-args.extra-args.env.1\", \"e2\")\n\n    System.setProperty(\"whisk.container-factory.container-args.dns-servers.0\", \"google.com\")\n    System.setProperty(\"whisk.container-factory.container-args.dns-servers.1\", \"1.2.3.4\")\n\n    System.setProperty(\"whisk.container-factory.container-args.dns-search.0\", \"a.b.c\")\n    System.setProperty(\"whisk.container-factory.container-args.dns-search.1\", \"a.b\")\n\n    System.setProperty(\"whisk.container-factory.container-args.dns-options.0\", \"ndots:5\")\n\n    val config = loadConfigOrThrow[ContainerArgsConfig](ConfigKeys.containerArgs)\n    //check defaults\n    config.network shouldBe \"bridge\"\n    config.dnsServers shouldBe Seq[String](\"google.com\", \"1.2.3.4\")\n    config.dnsSearch shouldBe Seq[String](\"a.b.c\", \"a.b\")\n    config.dnsOptions shouldBe Seq[String](\"ndots:5\")\n    //check map parsing of extra-args config\n    config.extraArgs.get(\"label\") shouldBe Some(Set(\"l1\", \"l2\", \"l3\"))\n    config.extraArgs.get(\"env\") shouldBe Some(Set(\"e1\", \"e2\"))\n\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerPoolConfigTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.test\n\nimport org.apache.openwhisk.core.containerpool.ContainerPoolConfig\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration.DurationInt\n\n@RunWith(classOf[JUnitRunner])\nclass ContainerPoolConfigTests extends AnyFlatSpec with Matchers {\n\n  def createPoolConfig(userMemory: ByteSize, userCpus: Option[Double] = None): ContainerPoolConfig = {\n    ContainerPoolConfig(userMemory, 0.5, false, 2.second, 10.seconds, None, 1, 3, false, 1.second, 10, userCpus)\n  }\n\n  it should \"calculate container cpu shares\" in {\n    val (userMemory, memoryLimit) = (2.GB, 256.MB)\n    val poolConfig = createPoolConfig(userMemory)\n    poolConfig.cpuShare(memoryLimit) shouldBe 128\n  }\n\n  it should \"use min cpu shares when calculated container cpu shares is too low\" in {\n    val (userMemory, memoryLimit) = (1024.MB, 1.MB)\n    val poolConfig = createPoolConfig(userMemory)\n    poolConfig.cpuShare(memoryLimit) shouldBe 2 // calculated shares would be 1, but min is 2\n  }\n\n  it should \"calculate container cpu limit\" in {\n    val (userMemory, memoryLimit, userCpus) = (2.GB, 256.MB, 2.0)\n    val poolConfig = createPoolConfig(userMemory, Some(userCpus))\n    poolConfig.cpuLimit(memoryLimit) shouldBe Some(0.25)\n  }\n\n  it should \"correctly round container cpu limit\" in {\n    val (userMemory, memoryLimit, userCpus) = (768.MB, 256.MB, 2.0)\n    val poolConfig = createPoolConfig(userMemory, Some(userCpus))\n    poolConfig.cpuLimit(memoryLimit) shouldBe Some(0.66667) // calculated limit is 0.666..., rounded to 0.66667\n  }\n\n  it should \"use min container cpu limit when calculated limit is too low\" in {\n    val (userMemory, memoryLimit, userCpus) = (1024.MB, 1.MB, 1.0)\n    val poolConfig = createPoolConfig(userMemory, Some(userCpus))\n    poolConfig.cpuLimit(memoryLimit) shouldBe Some(0.01) // calculated limit is 0.001, but min is 0.01\n  }\n\n  it should \"return None for container cpu limit when userCpus is not set\" in {\n    val (userMemory, memoryLimit) = (2.GB, 256.MB)\n    val poolConfig = createPoolConfig(userMemory)\n    poolConfig.cpuLimit(memoryLimit) shouldBe None\n  }\n\n  it should \"require userCpus to be greater than 0\" in {\n    assertThrows[IllegalArgumentException] {\n      createPoolConfig(2.GB, Some(-1.0))\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerPoolTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.test\n\nimport java.io.{ByteArrayOutputStream, PrintStream}\nimport java.time.Instant\nimport java.util.concurrent.TimeUnit\n\nimport scala.collection.mutable\nimport scala.concurrent.duration._\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.actor.ActorRefFactory\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.ImplicitSender\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.pekko.testkit.TestProbe\nimport common.{StreamLogging, WhiskProperties}\nimport org.apache.openwhisk.common.{Logging, PrintStreamLogging, TransactionId}\nimport org.apache.openwhisk.core.connector.ActivationMessage\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, ReactivePrewarmingConfig, RuntimeManifest}\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.connector.MessageFeed\nimport org.scalatest.concurrent.Eventually\n\n/**\n * Behavior tests for the ContainerPool\n *\n * These tests test the runtime behavior of a ContainerPool actor.\n */\n@RunWith(classOf[JUnitRunner])\nclass ContainerPoolTests\n    extends TestKit(ActorSystem(\"ContainerPool\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with MockFactory\n    with Eventually\n    with StreamLogging {\n\n  override def afterAll = TestKit.shutdownActorSystem(system)\n\n  val timeout = 5.seconds\n\n  // Common entities to pass to the tests. We don't really care what's inside\n  // those for the behavior testing here, as none of the contents will really\n  // reach a container anyway. We merely assert that passing and extraction of\n  // the values is done properly.\n  val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n  val memoryLimit = 256.MB\n  val ttl = FiniteDuration(500, TimeUnit.MILLISECONDS)\n  val threshold = 1\n  val increment = 1\n\n  /** Creates a `Run` message */\n  def createRunMessage(action: ExecutableWhiskAction, invocationNamespace: EntityName) = {\n    val uuid = UUID()\n    val message = ActivationMessage(\n      TransactionId.testing,\n      action.fullyQualifiedName(true),\n      action.rev,\n      Identity(Subject(), Namespace(invocationNamespace, uuid), BasicAuthenticationAuthKey(uuid, Secret())),\n      ActivationId.generate(),\n      ControllerInstanceId(\"0\"),\n      blocking = false,\n      content = None,\n      initArgs = Set.empty,\n      lockedArgs = Map.empty)\n    Run(action, message)\n  }\n\n  val invocationNamespace = EntityName(\"invocationSpace\")\n  val differentInvocationNamespace = EntityName(\"invocationSpace2\")\n  val action = ExecutableWhiskAction(EntityPath(\"actionSpace\"), EntityName(\"actionName\"), exec)\n  val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean)\n  val concurrentAction = ExecutableWhiskAction(\n    EntityPath(\"actionSpace\"),\n    EntityName(\"actionName\"),\n    exec,\n    limits = ActionLimits(concurrency = IntraConcurrencyLimit(if (concurrencyEnabled) 3 else 1)))\n  val differentAction = action.copy(name = EntityName(\"actionName2\"))\n  val largeAction =\n    action.copy(\n      name = EntityName(\"largeAction\"),\n      limits = ActionLimits(memory = MemoryLimit(MemoryLimit.STD_MEMORY * 2)))\n\n  val runMessage = createRunMessage(action, invocationNamespace)\n  val runMessageLarge = createRunMessage(largeAction, invocationNamespace)\n  val runMessageDifferentAction = createRunMessage(differentAction, invocationNamespace)\n  val runMessageDifferentVersion = createRunMessage(action.copy().revision(DocRevision(\"v2\")), invocationNamespace)\n  val runMessageDifferentNamespace = createRunMessage(action, differentInvocationNamespace)\n  val runMessageDifferentEverything = createRunMessage(differentAction, differentInvocationNamespace)\n  val runMessageConcurrent = createRunMessage(concurrentAction, invocationNamespace)\n  val runMessageConcurrentDifferentNamespace = createRunMessage(concurrentAction, differentInvocationNamespace)\n\n  /** Helper to create PreWarmedData */\n  def preWarmedData(kind: String, memoryLimit: ByteSize = memoryLimit, expires: Option[Deadline] = None) =\n    PreWarmedData(stub[MockableContainer], kind, memoryLimit, expires = expires)\n\n  /** Helper to create WarmedData */\n  def warmedData(run: Run, lastUsed: Instant = Instant.now) = {\n    WarmedData(stub[MockableContainer], run.msg.user.namespace.name, run.action, lastUsed)\n  }\n\n  /** Creates a sequence of containers and a factory returning this sequence. */\n  def testContainers(n: Int) = {\n    val containers = (0 to n).map(_ => TestProbe())\n    val queue = mutable.Queue(containers: _*)\n    val factory = (fac: ActorRefFactory) => queue.dequeue().ref\n    (containers, factory)\n  }\n\n  def poolConfig(userMemory: ByteSize) =\n    ContainerPoolConfig(userMemory, 0.5, false, 2.second, 1.minute, None, 100, 3, false, 1.second, 10)\n\n  behavior of \"ContainerPool\"\n\n  /*\n   * CONTAINER SCHEDULING\n   *\n   * These tests only test the simplest approaches. Look below for full coverage tests\n   * of the respective scheduling methods.\n   */\n  it should \"reuse a warm container\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n    // Actions are created with default memory limit (MemoryLimit.stdMemory). This means 4 actions can be scheduled.\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 4), feed.ref))\n\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    containers(0).send(pool, NeedWork(warmedData(runMessage)))\n\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    containers(1).expectNoMessage(100.milliseconds)\n  }\n\n  it should \"reuse a warm container when action is the same even if revision changes\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n    // Actions are created with default memory limit (MemoryLimit.stdMemory). This means 4 actions can be scheduled.\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 4), feed.ref))\n\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    containers(0).send(pool, NeedWork(warmedData(runMessage)))\n\n    pool ! runMessageDifferentVersion\n    containers(0).expectMsg(runMessageDifferentVersion)\n    containers(1).expectNoMessage(100.milliseconds)\n  }\n\n  it should \"create a container if it cannot find a matching container\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    // Actions are created with default memory limit (MemoryLimit.stdMemory). This means 4 actions can be scheduled.\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 4), feed.ref))\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    // Note that the container doesn't respond, thus it's not free to take work\n    pool ! runMessage\n    containers(1).expectMsg(runMessage)\n  }\n\n  it should \"remove a container to make space in the pool if it is already full and a different action arrives\" in within(\n    timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    // a pool with only 1 slot\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY), feed.ref))\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    containers(0).send(pool, NeedWork(warmedData(runMessage)))\n    feed.expectMsg(MessageFeed.Processed)\n    pool ! runMessageDifferentEverything\n    containers(0).expectMsg(Remove)\n    containers(1).expectMsg(runMessageDifferentEverything)\n  }\n\n  it should \"remove several containers to make space in the pool if it is already full and a different large action arrives\" in within(\n    timeout) {\n    val (containers, factory) = testContainers(3)\n    val feed = TestProbe()\n\n    // a pool with slots for 2 actions with default memory limit.\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(512.MB), feed.ref))\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    pool ! runMessageDifferentAction // 2 * stdMemory taken -> full\n    containers(1).expectMsg(runMessageDifferentAction)\n\n    containers(0).send(pool, NeedWork(warmedData(runMessage))) // first action finished -> 1 * stdMemory taken\n    feed.expectMsg(MessageFeed.Processed)\n    containers(1)\n      .send(pool, NeedWork(warmedData(runMessageDifferentAction))) // second action finished -> 1 * stdMemory taken\n    feed.expectMsg(MessageFeed.Processed)\n\n    pool ! runMessageLarge // need to remove both action to make space for the large action (needs 2 * stdMemory)\n    containers(0).expectMsg(Remove)\n    containers(1).expectMsg(Remove)\n    containers(2).expectMsg(runMessageLarge)\n  }\n\n  it should \"cache a container if there is still space in the pool\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    // a pool with only 1 active slot but 2 slots in total\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 2), feed.ref))\n\n    // Run the first container\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    containers(0).send(pool, NeedWork(warmedData(runMessage, lastUsed = Instant.EPOCH)))\n    feed.expectMsg(MessageFeed.Processed)\n\n    // Run the second container, don't remove the first one\n    pool ! runMessageDifferentEverything\n    containers(1).expectMsg(runMessageDifferentEverything)\n    containers(1).send(pool, NeedWork(warmedData(runMessageDifferentEverything, lastUsed = Instant.now)))\n    feed.expectMsg(MessageFeed.Processed)\n    pool ! runMessageDifferentNamespace\n    containers(2).expectMsg(runMessageDifferentNamespace)\n\n    // 2 Slots exhausted, remove the first container to make space\n    containers(0).expectMsg(Remove)\n  }\n\n  it should \"remove a container to make space in the pool if it is already full and another action with different invocation namespace arrives\" in within(\n    timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    // a pool with only 1 slot\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY), feed.ref))\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    containers(0).send(pool, NeedWork(warmedData(runMessage)))\n    feed.expectMsg(MessageFeed.Processed)\n    pool ! runMessageDifferentNamespace\n    containers(0).expectMsg(Remove)\n    containers(1).expectMsg(runMessageDifferentNamespace)\n  }\n\n  it should \"reschedule job when container is removed prematurely without sending message to feed\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    // a pool with only 1 slot\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY), feed.ref))\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    containers(0).send(pool, RescheduleJob) // emulate container failure ...\n    containers(0).send(pool, runMessage) // ... causing job to be rescheduled\n    feed.expectNoMessage(100.millis)\n    containers(1).expectMsg(runMessage) // job resent to new actor\n  }\n\n  it should \"not start a new container if there is not enough space in the pool\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 2), feed.ref))\n\n    // Start first action\n    pool ! runMessage // 1 * stdMemory taken\n    containers(0).expectMsg(runMessage)\n\n    // Send second action to the pool\n    pool ! runMessageLarge // message is too large to be processed immediately.\n    containers(1).expectNoMessage(100.milliseconds)\n\n    // First action is finished\n    containers(0).send(pool, NeedWork(warmedData(runMessage))) // pool is empty again.\n    feed.expectMsg(MessageFeed.Processed)\n\n    // Second action should run now\n    containers(1).expectMsgPF() {\n      // The `Some` assures, that it has been retried while the first action was still blocking the invoker.\n      case Run(runMessageLarge.action, runMessageLarge.msg, Some(_)) => true\n    }\n\n    containers(1).send(pool, NeedWork(warmedData(runMessageLarge)))\n    feed.expectMsg(MessageFeed.Processed)\n  }\n\n  it should \"not create prewarm container when used memory reaches the limit\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val pool =\n      system.actorOf(ContainerPool\n        .props(factory, poolConfig(MemoryLimit.STD_MEMORY * 1), feed.ref, List(PrewarmingConfig(2, exec, memoryLimit))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(0).send(pool, NeedWork(preWarmedData(exec.kind)))\n\n    containers(1).expectNoMessage(100.milliseconds)\n  }\n\n  /*\n   * CONTAINER PREWARMING\n   */\n  it should \"create prewarmed containers on startup\" in within(timeout) {\n    val (containers, factory) = testContainers(1)\n    val feed = TestProbe()\n\n    val pool =\n      system.actorOf(\n        ContainerPool\n          .props(factory, poolConfig(MemoryLimit.STD_MEMORY), feed.ref, List(PrewarmingConfig(1, exec, memoryLimit))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n  }\n\n  it should \"use a prewarmed container and create a new one to fill its place\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val pool =\n      system.actorOf(ContainerPool\n        .props(factory, poolConfig(MemoryLimit.STD_MEMORY * 2), feed.ref, List(PrewarmingConfig(1, exec, memoryLimit))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(0).send(pool, NeedWork(preWarmedData(exec.kind)))\n    pool ! runMessage\n    containers(1).expectMsg(Start(exec, memoryLimit))\n  }\n\n  it should \"use a prewarmed container with ttl and create a new one to fill its place\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n    val ttl = 5.seconds //make sure replaced prewarm has ttl\n    val pool =\n      system.actorOf(\n        ContainerPool\n          .props(\n            factory,\n            poolConfig(MemoryLimit.STD_MEMORY * 2),\n            feed.ref,\n            List(PrewarmingConfig(1, exec, memoryLimit, Some(ReactivePrewarmingConfig(1, 1, ttl, 1, 1))))))\n    containers(0).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(0).send(pool, NeedWork(preWarmedData(exec.kind, expires = Some(ttl.fromNow))))\n    pool ! runMessage\n    containers(1).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n  }\n  it should \"not use a prewarmed container if it doesn't fit the kind\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val alternativeExec = CodeExecAsString(RuntimeManifest(\"anotherKind\", ImageName(\"testImage\")), \"testCode\", None)\n\n    val pool = system.actorOf(\n      ContainerPool\n        .props(\n          factory,\n          poolConfig(MemoryLimit.STD_MEMORY * 2),\n          feed.ref,\n          List(PrewarmingConfig(1, alternativeExec, memoryLimit))))\n    containers(0).expectMsg(Start(alternativeExec, memoryLimit)) // container0 was prewarmed\n    containers(0).send(pool, NeedWork(preWarmedData(alternativeExec.kind)))\n    pool ! runMessage\n    containers(1).expectMsg(runMessage) // but container1 is used\n  }\n\n  it should \"not use a prewarmed container if it doesn't fit memory wise\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val alternativeLimit = 128.MB\n\n    val pool =\n      system.actorOf(\n        ContainerPool\n          .props(\n            factory,\n            poolConfig(MemoryLimit.STD_MEMORY * 2),\n            feed.ref,\n            List(PrewarmingConfig(1, exec, alternativeLimit))))\n    containers(0).expectMsg(Start(exec, alternativeLimit)) // container0 was prewarmed\n    containers(0).send(pool, NeedWork(preWarmedData(exec.kind, alternativeLimit)))\n    pool ! runMessage\n    containers(1).expectMsg(runMessage) // but container1 is used\n  }\n\n  /*\n   * CONTAINER DELETION\n   */\n  it should \"not reuse a container which is scheduled for deletion\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 4), feed.ref))\n\n    // container0 is created and used\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    containers(0).send(pool, NeedWork(warmedData(runMessage)))\n\n    // container0 is reused\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    containers(0).send(pool, NeedWork(warmedData(runMessage)))\n\n    // container0 is deleted\n    containers(0).send(pool, ContainerRemoved(true))\n\n    // container1 is created and used\n    pool ! runMessage\n    containers(1).expectMsg(runMessage)\n  }\n\n  /*\n   * Run buffer\n   */\n  it should \"first put messages into the queue and retrying them and then put messages only into the queue\" in within(\n    timeout) {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    // Pool with 512 MB usermemory\n    val pool =\n      system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 2), feed.ref))\n\n    // Send action that blocks the pool\n    pool ! runMessageLarge\n    // Action 0 starts -> 0MB free\n    containers(0).expectMsg(runMessageLarge)\n\n    // Send action that should be written to the queue and retried in invoker\n    pool ! runMessage\n    containers(1).expectNoMessage(100.milliseconds)\n\n    // Send another message that should not be retried, but put into the queue as well\n    pool ! runMessageDifferentAction\n    containers(2).expectNoMessage(100.milliseconds)\n\n    // Action with 512 MB is finished\n    // Action 0 completes -> 512MB free\n    containers(0).send(pool, NeedWork(warmedData(runMessageLarge)))\n    // Action 1 should start immediately -> 256MB free\n    containers(1).expectMsgPF() {\n      // The `Some` assures, that it has been retried while the first action was still blocking the invoker.\n      case Run(runMessage.action, runMessage.msg, Some(_)) => true\n    }\n    // Action 2 should start immediately as well -> 0MB free (without any retries, as there is already enough space in the pool)\n    containers(2).expectMsg(runMessageDifferentAction)\n\n    // When buffer is emptied, process next feed message\n    feed.expectMsg(MessageFeed.Processed)\n\n    // Action 1 completes, process feed\n    containers(1).send(pool, NeedWork(warmedData(runMessage)))\n    feed.expectMsg(MessageFeed.Processed)\n\n    // Action 2 completes, process feed\n    containers(2).send(pool, NeedWork(warmedData(runMessageDifferentAction)))\n    feed.expectMsg(MessageFeed.Processed)\n\n  }\n\n  it should \"process activations in the order they are arriving\" in within(timeout) {\n    val (containers, factory) = testContainers(6)\n    val feed = TestProbe()\n\n    // Pool with 512 MB usermemory\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 2), feed.ref))\n\n    // Send 4 actions to the ContainerPool (Action 0, Action 2 and Action 3 with each 256 MB and Action 1 with 512 MB)\n    pool ! runMessage\n    containers(0).expectMsg(runMessage)\n    pool ! runMessageLarge\n    containers(1).expectNoMessage(100.milliseconds)\n    pool ! runMessageDifferentNamespace\n    containers(2).expectNoMessage(100.milliseconds)\n    pool ! runMessageDifferentAction\n    containers(3).expectNoMessage(100.milliseconds)\n\n    // Action 0 is finished -> 512 free; Large action should be executed now (2 more in queue)\n    containers(0).send(pool, NeedWork(warmedData(runMessage)))\n    // Buffer still has 2, so feed will not be used\n    feed.expectNoMessage(100.milliseconds)\n    containers(1).expectMsgPF() {\n      // The `Some` assures, that it has been retried while the first action was still blocking the invoker.\n      case Run(runMessageLarge.action, runMessageLarge.msg, Some(_)) => true\n    }\n\n    // Send another action to the container pool, that would fit memory-wise (3 in queue)\n    pool ! runMessageDifferentEverything\n    containers(4).expectNoMessage(100.milliseconds)\n\n    // Action 1 is finished -> 512 free; Action 2 and Action 3 should be executed now (1 more in queue)\n    containers(1).send(pool, NeedWork(warmedData(runMessageLarge)))\n    // Buffer still has 1, so feed will not be used\n    feed.expectNoMessage(100.milliseconds)\n\n    containers(2).expectMsg(runMessageDifferentNamespace)\n    // Assert retryLogline = false to check if this request has been stored in the queue instead of retrying in the system\n    containers(3).expectMsg(runMessageDifferentAction)\n\n    // Action 3 is finished -> 256 free; Action 4 should start (0 in queue)\n    containers(3).send(pool, NeedWork(warmedData(runMessageDifferentAction)))\n    // Buffer is empty, so go back to processing feed\n    feed.expectMsg(MessageFeed.Processed)\n    // Run the 5th message from the buffer\n    containers(4).expectMsg(runMessageDifferentEverything)\n\n    // Action 2 is finished -> 256 free\n    containers(2).send(pool, NeedWork(warmedData(runMessageDifferentNamespace)))\n    feed.expectMsg(MessageFeed.Processed)\n\n    pool ! runMessage\n    // Back to buffering\n    pool ! runMessageDifferentVersion\n    containers(5).expectMsg(runMessage)\n    // Action 6 won't start because it is buffered, waiting for Action 4 to complete\n    containers(6).expectNoMessage(100.milliseconds)\n\n    // Action 4 is finished -> 256 free\n    containers(4).send(pool, NeedWork(warmedData(runMessageDifferentEverything)))\n\n    // Run the 6th message from the buffer\n    containers(6).expectMsgPF() {\n      // The `Some` assures, that it has been retried while the first action was still blocking the invoker.\n      case Run(runMessageDifferentVersion.action, runMessageDifferentVersion.msg, Some(_)) => true\n    }\n\n    // When buffer is emptied, process next feed message\n    feed.expectMsg(MessageFeed.Processed)\n\n    // Action 5 is finished -> 256 free, process feed\n    containers(5).send(pool, NeedWork(warmedData(runMessage)))\n    feed.expectMsg(MessageFeed.Processed)\n\n    // Action 6 is finished -> 512 free, process feed\n    containers(6).send(pool, NeedWork(warmedData(runMessageDifferentVersion)))\n    feed.expectMsg(MessageFeed.Processed)\n  }\n\n  it should \"process runbuffer instead of requesting new messages\" in {\n\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 1), feed.ref))\n\n    val run1 = createRunMessage(action, invocationNamespace)\n    val run2 = createRunMessage(action, invocationNamespace)\n    val run3 = createRunMessage(action, invocationNamespace)\n\n    pool ! run1\n    pool ! run2 //will be buffered since the pool can only fit 1\n    pool ! run3 //will be buffered since the pool can only fit 1\n\n    //start first run\n    containers(0).expectMsg(run1)\n\n    //cannot launch more containers, so make sure additional containers are not created\n    containers(1).expectNoMessage(100.milliseconds)\n\n    //complete processing of first run\n    containers(0).send(pool, NeedWork(warmedData(run1)))\n\n    //don't feed till runBuffer is emptied\n    feed.expectNoMessage(100.milliseconds)\n\n    //start second run\n    containers(0).expectMsgPF() {\n      // The `Some` assures, that it has been retried while the first action was still blocking the invoker.\n      case Run(run2.action, run2.msg, Some(_)) => true\n    }\n\n    //complete processing of second run\n    containers(0).send(pool, NeedWork(warmedData(run2)))\n\n    //feed as part of last buffer item processing\n    feed.expectMsg(MessageFeed.Processed)\n\n    //start third run\n    containers(0).expectMsgPF() {\n      // The `Some` assures, that it has been retried while the first action was still blocking the invoker.\n      case Run(run3.action, run3.msg, None) => true\n    }\n\n    //complete processing of third run\n    containers(0).send(pool, NeedWork(warmedData(run3)))\n\n    //now we expect feed to send a new message (1 per completion = 2 new messages)\n    feed.expectMsg(MessageFeed.Processed)\n\n    //make sure only one though\n    feed.expectNoMessage(100.milliseconds)\n  }\n\n  it should \"process runbuffer when container is removed\" in {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val run1 = createRunMessage(action, invocationNamespace)\n    val run2 = createRunMessage(action, invocationNamespace)\n\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 1), feed.ref))\n\n    //these will get buffered since allowLaunch is false\n    pool ! run1\n    pool ! run2\n\n    //start first run\n    containers(0).expectMsg(run1)\n\n    //trigger removal of the container ref, but don't start processing\n    containers(0).send(pool, RescheduleJob)\n\n    //trigger buffer processing by ContainerRemoved message\n    pool ! ContainerRemoved(true)\n\n    //start second run\n    containers(1).expectMsgPF() {\n      // The `Some` assures, that it has been retried while the first action was still blocking the invoker.\n      case Run(run2.action, run2.msg, Some(_)) => true\n    }\n  }\n\n  it should \"process runbuffered items only once\" in {\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 1), feed.ref))\n\n    val run1 = createRunMessage(action, invocationNamespace)\n    val run2 = createRunMessage(action, invocationNamespace)\n    val run3 = createRunMessage(action, invocationNamespace)\n\n    pool ! run1\n    pool ! run2 //will be buffered since the pool can only fit 1\n    pool ! run3 //will be buffered since the pool can only fit 1\n\n    //start first run\n    containers(0).expectMsg(run1)\n\n    //cannot launch more containers, so make sure additional containers are not created\n    containers(1).expectNoMessage(100.milliseconds)\n\n    //ContainerRemoved triggers buffer processing - if we don't prevent duplicates, this will cause the buffer head to be resent!\n    pool ! ContainerRemoved(true)\n    pool ! ContainerRemoved(true)\n    pool ! ContainerRemoved(true)\n\n    //complete processing of first run\n    containers(0).send(pool, NeedWork(warmedData(run1)))\n\n    //don't feed till runBuffer is emptied\n    feed.expectNoMessage(100.milliseconds)\n\n    //start second run\n    containers(0).expectMsgPF() {\n      // The `Some` assures, that it has been retried while the first action was still blocking the invoker.\n      case Run(run2.action, run2.msg, Some(_)) => true\n    }\n\n    //complete processing of second run\n    containers(0).send(pool, NeedWork(warmedData(run2)))\n\n    //feed as part of last buffer item processing\n    feed.expectMsg(MessageFeed.Processed)\n\n    //start third run\n    containers(0).expectMsgPF() {\n      // The `Some` assures, that it has been retried while the first action was still blocking the invoker.\n      case Run(run3.action, run3.msg, None) => true\n    }\n\n    //complete processing of third run\n    containers(0).send(pool, NeedWork(warmedData(run3)))\n\n    //now we expect feed to send a new message (1 per completion = 2 new messages)\n    feed.expectMsg(MessageFeed.Processed)\n\n    //make sure only one though\n    feed.expectNoMessage(100.milliseconds)\n  }\n  it should \"increase activation counts when scheduling to containers whose actions support concurrency\" in {\n    assume(concurrencyEnabled)\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 4), feed.ref))\n\n    // container0 is created and used\n    pool ! runMessageConcurrent\n    containers(0).expectMsg(runMessageConcurrent)\n\n    // container0 is reused\n    pool ! runMessageConcurrent\n    containers(0).expectMsg(runMessageConcurrent)\n\n    // container0 is reused\n    pool ! runMessageConcurrent\n    containers(0).expectMsg(runMessageConcurrent)\n\n    // container1 is created and used (these concurrent containers are configured with max 3 concurrent activations)\n    pool ! runMessageConcurrent\n    containers(1).expectMsg(runMessageConcurrent)\n  }\n\n  it should \"schedule concurrent activations to different containers for different namespaces\" in {\n    assume(concurrencyEnabled)\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 4), feed.ref))\n\n    // container0 is created and used\n    pool ! runMessageConcurrent\n    containers(0).expectMsg(runMessageConcurrent)\n\n    // container1 is created and used\n    pool ! runMessageConcurrentDifferentNamespace\n    containers(1).expectMsg(runMessageConcurrentDifferentNamespace)\n  }\n\n  it should \"decrease activation counts when receiving NeedWork for actions that support concurrency\" in {\n    assume(concurrencyEnabled)\n    val (containers, factory) = testContainers(2)\n    val feed = TestProbe()\n\n    val pool = system.actorOf(ContainerPool.props(factory, poolConfig(MemoryLimit.STD_MEMORY * 4), feed.ref))\n\n    // container0 is created and used\n    pool ! runMessageConcurrent\n    containers(0).expectMsg(runMessageConcurrent)\n\n    // container0 is reused\n    pool ! runMessageConcurrent\n    containers(0).expectMsg(runMessageConcurrent)\n\n    // container0 is reused\n    pool ! runMessageConcurrent\n    containers(0).expectMsg(runMessageConcurrent)\n\n    // container1 is created and used (these concurrent containers are configured with max 3 concurrent activations)\n    pool ! runMessageConcurrent\n    containers(1).expectMsg(runMessageConcurrent)\n\n    // container1 is reused\n    pool ! runMessageConcurrent\n    containers(1).expectMsg(runMessageConcurrent)\n\n    // container1 is reused\n    pool ! runMessageConcurrent\n    containers(1).expectMsg(runMessageConcurrent)\n\n    containers(0).send(pool, NeedWork(warmedData(runMessageConcurrent)))\n\n    // container0 is reused (since active count decreased)\n    pool ! runMessageConcurrent\n    containers(0).expectMsg(runMessageConcurrent)\n  }\n\n  it should \"backfill prewarms when prewarm containers are removed\" in {\n    val (containers, factory) = testContainers(6)\n    val feed = TestProbe()\n\n    val pool =\n      system.actorOf(ContainerPool\n        .props(factory, poolConfig(MemoryLimit.STD_MEMORY * 5), feed.ref, List(PrewarmingConfig(2, exec, memoryLimit))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(1).expectMsg(Start(exec, memoryLimit))\n\n    //removing 2 prewarm containers will start 2 containers via backfill\n    containers(0).send(pool, ContainerRemoved(true))\n    containers(1).send(pool, ContainerRemoved(true))\n    containers(2).expectMsg(Start(exec, memoryLimit))\n    containers(3).expectMsg(Start(exec, memoryLimit))\n    //make sure extra prewarms are not started\n    containers(4).expectNoMessage(100.milliseconds)\n    containers(5).expectNoMessage(100.milliseconds)\n  }\n\n  it should \"adjust prewarm container run well without reactive config\" in {\n    val (containers, factory) = testContainers(4)\n    val feed = TestProbe()\n\n    stream.reset()\n    val prewarmExpirationCheckInitDelay = FiniteDuration(2, TimeUnit.SECONDS)\n    val prewarmExpirationCheckIntervel = FiniteDuration(2, TimeUnit.SECONDS)\n    val poolConfig =\n      ContainerPoolConfig(\n        MemoryLimit.STD_MEMORY * 4,\n        0.5,\n        false,\n        prewarmExpirationCheckInitDelay,\n        prewarmExpirationCheckIntervel,\n        None,\n        100,\n        3,\n        false,\n        1.second,\n        10)\n    val initialCount = 2\n    val pool =\n      system.actorOf(\n        ContainerPool\n          .props(factory, poolConfig, feed.ref, List(PrewarmingConfig(initialCount, exec, memoryLimit))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(1).expectMsg(Start(exec, memoryLimit))\n    containers(0).send(pool, NeedWork(preWarmedData(exec.kind)))\n    containers(1).send(pool, NeedWork(preWarmedData(exec.kind)))\n\n    // when invoker starts, include 0 prewarm container at the very beginning\n    stream.toString should include(s\"found 0 started\")\n\n    // the desiredCount should equal with initialCount when invoker starts\n    stream.toString should include(s\"desired count: ${initialCount}\")\n\n    stream.reset()\n\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n\n    // Because already supplemented the prewarmed container, so currentCount should equal with initialCount\n    eventually {\n      stream.toString should not include (\"started\")\n    }\n  }\n\n  it should \"adjust prewarm container run well with reactive config\" in {\n    val (containers, factory) = testContainers(15)\n    val feed = TestProbe()\n\n    stream.reset()\n    val prewarmExpirationCheckInitDelay = 2.seconds\n    val prewarmExpirationCheckIntervel = 2.seconds\n    val poolConfig =\n      ContainerPoolConfig(\n        MemoryLimit.STD_MEMORY * 12,\n        0.5,\n        false,\n        prewarmExpirationCheckInitDelay,\n        prewarmExpirationCheckIntervel,\n        None,\n        100,\n        3,\n        false,\n        1.second,\n        10)\n    val minCount = 0\n    val initialCount = 2\n    val maxCount = 4\n    val deadline: Option[Deadline] = Some(ttl.fromNow)\n    val reactive: Option[ReactivePrewarmingConfig] =\n      Some(ReactivePrewarmingConfig(minCount, maxCount, ttl, threshold, increment))\n    val pool =\n      system.actorOf(\n        ContainerPool\n          .props(factory, poolConfig, feed.ref, List(PrewarmingConfig(initialCount, exec, memoryLimit, reactive))))\n    //start 2 prewarms\n    containers(0).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(1).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(0).send(pool, NeedWork(preWarmedData(exec.kind, expires = deadline)))\n    containers(1).send(pool, NeedWork(preWarmedData(exec.kind, expires = deadline)))\n\n    // when invoker starts, include 0 prewarm container at the very beginning\n    stream.toString should include(s\"found 0 started\")\n\n    // the desiredCount should equal with initialCount when invoker starts\n    stream.toString should include(s\"desired count: ${initialCount}\")\n\n    stream.reset()\n\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n    //expire 2 prewarms\n    containers(0).expectMsg(Remove)\n    containers(1).expectMsg(Remove)\n    containers(0).send(pool, ContainerRemoved(false))\n    containers(1).send(pool, ContainerRemoved(false))\n\n    // currentCount should equal with 0 due to these 2 prewarmed containers are expired\n    stream.toString should not include (s\"found 0 started\")\n\n    // the desiredCount should equal with minCount because cold start didn't happen\n    stream.toString should not include (s\"desired count: ${minCount}\")\n    // Previously created prewarmed containers should be removed\n    stream.toString should not include (s\"removed ${initialCount} expired prewarmed container\")\n\n    stream.reset()\n    val action = ExecutableWhiskAction(\n      EntityPath(\"actionSpace\"),\n      EntityName(\"actionName\"),\n      exec,\n      limits = ActionLimits(memory = MemoryLimit(memoryLimit)))\n    val run = createRunMessage(action, invocationNamespace)\n    // 2 cold start happened\n    pool ! run\n    pool ! run\n    containers(2).expectMsg(run)\n    containers(3).expectMsg(run)\n\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n\n    eventually {\n      // Because already removed expired prewarmed containrs, so currentCount should equal with 0\n      stream.toString should include(s\"found 0 started\")\n      // the desiredCount should equal with 2 due to cold start happened\n      stream.toString should include(s\"desired count: 2\")\n    }\n    //add 2 prewarms due to increments\n    containers(4).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(5).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(4).send(pool, NeedWork(preWarmedData(exec.kind, expires = deadline)))\n    containers(5).send(pool, NeedWork(preWarmedData(exec.kind, expires = deadline)))\n\n    stream.reset()\n\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n\n    containers(4).expectMsg(Remove)\n    containers(5).expectMsg(Remove)\n    containers(4).send(pool, ContainerRemoved(false))\n    containers(5).send(pool, ContainerRemoved(false))\n\n    // removed previous 2 prewarmed container due to expired\n    stream.toString should include(s\"removing up to ${poolConfig.prewarmExpirationLimit} of 2 expired containers\")\n\n    stream.reset()\n    // 5 code start happened(5 > maxCount)\n    pool ! run\n    pool ! run\n    pool ! run\n    pool ! run\n    pool ! run\n\n    containers(6).expectMsg(run)\n    containers(7).expectMsg(run)\n    containers(8).expectMsg(run)\n    containers(9).expectMsg(run)\n    containers(10).expectMsg(run)\n\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n\n    eventually {\n      // Because already removed expired prewarmed containrs, so currentCount should equal with 0\n      stream.toString should include(s\"found 0 started\")\n      // in spite of the cold start number > maxCount, but the desiredCount can't be greater than maxCount\n      stream.toString should include(s\"desired count: ${maxCount}\")\n    }\n\n    containers(11).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(12).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(13).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(14).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(11).send(pool, NeedWork(preWarmedData(exec.kind, expires = deadline)))\n    containers(12).send(pool, NeedWork(preWarmedData(exec.kind, expires = deadline)))\n    containers(13).send(pool, NeedWork(preWarmedData(exec.kind, expires = deadline)))\n    containers(14).send(pool, NeedWork(preWarmedData(exec.kind, expires = deadline)))\n  }\n}\nabstract class MockableContainer extends Container {\n  protected[core] val addr: ContainerAddress = ContainerAddress(\"nohost\")\n}\n\n/**\n * Unit tests for the ContainerPool object.\n *\n * These tests test only the \"static\" methods \"schedule\" and \"remove\"\n * of the ContainerPool object.\n */\n@RunWith(classOf[JUnitRunner])\nclass ContainerPoolObjectTests extends AnyFlatSpec with Matchers with MockFactory {\n\n  val actionExec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n  val standardNamespace = EntityName(\"standardNamespace\")\n  val differentNamespace = EntityName(\"differentNamespace\")\n\n  /** Helper to create a new action from String representations */\n  def createAction(namespace: String = \"actionNS\", name: String = \"actionName\", limits: ActionLimits = ActionLimits()) =\n    ExecutableWhiskAction(EntityPath(namespace), EntityName(name), actionExec, limits = limits)\n\n  /** Helper to create WarmedData with sensible defaults */\n  def warmedData(action: ExecutableWhiskAction = createAction(),\n                 namespace: String = standardNamespace.asString,\n                 lastUsed: Instant = Instant.now,\n                 active: Int = 0) =\n    WarmedData(stub[MockableContainer], EntityName(namespace), action, lastUsed, active)\n\n  /** Helper to create WarmingData with sensible defaults */\n  def warmingData(action: ExecutableWhiskAction = createAction(),\n                  namespace: String = standardNamespace.asString,\n                  lastUsed: Instant = Instant.now,\n                  active: Int = 0) =\n    WarmingData(stub[MockableContainer], EntityName(namespace), action, lastUsed, active)\n\n  /** Helper to create WarmingData with sensible defaults */\n  def warmingColdData(action: ExecutableWhiskAction = createAction(),\n                      namespace: String = standardNamespace.asString,\n                      lastUsed: Instant = Instant.now,\n                      active: Int = 0) =\n    WarmingColdData(EntityName(namespace), action, lastUsed, active)\n\n  /** Helper to create PreWarmedData with sensible defaults */\n  def preWarmedData(kind: String = \"anyKind\", expires: Option[Deadline] = None) =\n    PreWarmedData(stub[MockableContainer], kind, 256.MB, expires = expires)\n\n  /** Helper to create NoData */\n  def noData() = NoData()\n\n  behavior of \"ContainerPool schedule()\"\n\n  it should \"not provide a container if idle pool is empty\" in {\n    ContainerPool.schedule(createAction(), standardNamespace, Map.empty) shouldBe None\n  }\n\n  it should \"reuse an applicable warm container from idle pool with one container\" in {\n    val data = warmedData()\n    val pool = Map('name -> data)\n\n    // copy to make sure, referencial equality doesn't suffice\n    ContainerPool.schedule(data.action.copy(), data.invocationNamespace, pool) shouldBe Some('name, data)\n  }\n\n  it should \"reuse an applicable warm container from idle pool with several applicable containers\" in {\n    val data = warmedData()\n    val pool = Map('first -> data, 'second -> data)\n\n    ContainerPool.schedule(data.action.copy(), data.invocationNamespace, pool) should (be(Some('first, data)) or be(\n      Some('second, data)))\n  }\n\n  it should \"reuse an applicable warm container from idle pool with several different containers\" in {\n    val matchingData = warmedData()\n    val pool = Map('none -> noData(), 'pre -> preWarmedData(), 'warm -> matchingData)\n\n    ContainerPool.schedule(matchingData.action.copy(), matchingData.invocationNamespace, pool) shouldBe Some(\n      'warm,\n      matchingData)\n  }\n\n  it should \"not reuse a container from idle pool with non-warm containers\" in {\n    val data = warmedData()\n    // data is **not** in the pool!\n    val pool = Map('none -> noData(), 'pre -> preWarmedData())\n\n    ContainerPool.schedule(data.action.copy(), data.invocationNamespace, pool) shouldBe None\n  }\n\n  it should \"not reuse a warm container with different invocation namespace\" in {\n    val data = warmedData()\n    val pool = Map('warm -> data)\n    val differentNamespace = EntityName(data.invocationNamespace.asString + \"butDifferent\")\n\n    data.invocationNamespace should not be differentNamespace\n    ContainerPool.schedule(data.action.copy(), differentNamespace, pool) shouldBe None\n  }\n\n  it should \"not reuse a warm container with different action name\" in {\n    val data = warmedData()\n    val differentAction = data.action.copy(name = EntityName(data.action.name.asString + \"butDifferent\"))\n    val pool = Map('warm -> data)\n\n    data.action.name should not be differentAction.name\n    ContainerPool.schedule(differentAction, data.invocationNamespace, pool) shouldBe None\n  }\n\n  it should \"not reuse a warm container with different action version\" in {\n    val data = warmedData()\n    val differentAction = data.action.copy(version = data.action.version.upMajor)\n    val pool = Map('warm -> data)\n\n    data.action.version should not be differentAction.version\n    ContainerPool.schedule(differentAction, data.invocationNamespace, pool) shouldBe None\n  }\n\n  it should \"not use a container when active activation count >= maxconcurrent\" in {\n    val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean)\n    val maxConcurrent = if (concurrencyEnabled) 25 else 1\n\n    val data = warmedData(\n      active = maxConcurrent,\n      action = createAction(limits = ActionLimits(concurrency = IntraConcurrencyLimit(maxConcurrent))))\n    val pool = Map('warm -> data)\n    ContainerPool.schedule(data.action, data.invocationNamespace, pool) shouldBe None\n\n    val data2 = warmedData(\n      active = maxConcurrent - 1,\n      action = createAction(limits = ActionLimits(concurrency = IntraConcurrencyLimit(maxConcurrent))))\n    val pool2 = Map('warm -> data2)\n\n    ContainerPool.schedule(data2.action, data2.invocationNamespace, pool2) shouldBe Some('warm, data2)\n\n  }\n\n  it should \"use a warming when active activation count < maxconcurrent\" in {\n    val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean)\n    val maxConcurrent = if (concurrencyEnabled) 25 else 1\n\n    val action = createAction(limits = ActionLimits(concurrency = IntraConcurrencyLimit(maxConcurrent)))\n    val data = warmingData(active = maxConcurrent - 1, action = action)\n    val pool = Map('warming -> data)\n    ContainerPool.schedule(data.action, data.invocationNamespace, pool) shouldBe Some('warming, data)\n\n    val data2 = warmedData(active = maxConcurrent - 1, action = action)\n    val pool2 = pool ++ Map('warm -> data2)\n\n    ContainerPool.schedule(data2.action, data2.invocationNamespace, pool2) shouldBe Some('warm, data2)\n  }\n\n  it should \"prefer warm to warming when active activation count < maxconcurrent\" in {\n    val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean)\n    val maxConcurrent = if (concurrencyEnabled) 25 else 1\n\n    val action = createAction(limits = ActionLimits(concurrency = IntraConcurrencyLimit(maxConcurrent)))\n    val data = warmingColdData(active = maxConcurrent - 1, action = action)\n    val data2 = warmedData(active = maxConcurrent - 1, action = action)\n    val pool = Map('warming -> data, 'warm -> data2)\n    ContainerPool.schedule(data.action, data.invocationNamespace, pool) shouldBe Some('warm, data2)\n  }\n\n  it should \"use a warmingCold when active activation count < maxconcurrent\" in {\n    val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean)\n    val maxConcurrent = if (concurrencyEnabled) 25 else 1\n\n    val action = createAction(limits = ActionLimits(concurrency = IntraConcurrencyLimit(maxConcurrent)))\n    val data = warmingColdData(active = maxConcurrent - 1, action = action)\n    val pool = Map('warmingCold -> data)\n    ContainerPool.schedule(data.action, data.invocationNamespace, pool) shouldBe Some('warmingCold, data)\n\n    //after scheduling, the pool will update with new data to set active = maxConcurrent\n    val data2 = warmingColdData(active = maxConcurrent, action = action)\n    val pool2 = Map('warmingCold -> data2)\n\n    ContainerPool.schedule(data2.action, data2.invocationNamespace, pool2) shouldBe None\n  }\n\n  it should \"prefer warm to warmingCold when active activation count < maxconcurrent\" in {\n    val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean)\n    val maxConcurrent = if (concurrencyEnabled) 25 else 1\n\n    val action = createAction(limits = ActionLimits(concurrency = IntraConcurrencyLimit(maxConcurrent)))\n    val data = warmingColdData(active = maxConcurrent - 1, action = action)\n    val data2 = warmedData(active = maxConcurrent - 1, action = action)\n    val pool = Map('warmingCold -> data, 'warm -> data2)\n    ContainerPool.schedule(data.action, data.invocationNamespace, pool) shouldBe Some('warm, data2)\n  }\n\n  it should \"prefer warming to warmingCold when active activation count < maxconcurrent\" in {\n    val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean)\n    val maxConcurrent = if (concurrencyEnabled) 25 else 1\n\n    val action = createAction(limits = ActionLimits(concurrency = IntraConcurrencyLimit(maxConcurrent)))\n    val data = warmingColdData(active = maxConcurrent - 1, action = action)\n    val data2 = warmingData(active = maxConcurrent - 1, action = action)\n    val pool = Map('warmingCold -> data, 'warming -> data2)\n    ContainerPool.schedule(data.action, data.invocationNamespace, pool) shouldBe Some('warming, data2)\n  }\n\n  behavior of \"ContainerPool remove()\"\n\n  it should \"not provide a container if pool is empty\" in {\n    ContainerPool.remove(Map.empty, MemoryLimit.STD_MEMORY) shouldBe List.empty\n  }\n\n  it should \"not provide a container from busy pool with non-warm containers\" in {\n    val pool = Map('none -> noData(), 'pre -> preWarmedData())\n    ContainerPool.remove(pool, MemoryLimit.STD_MEMORY) shouldBe List.empty\n  }\n\n  it should \"not provide a container from pool if there is not enough capacity\" in {\n    val pool = Map('first -> warmedData())\n\n    ContainerPool.remove(pool, MemoryLimit.STD_MEMORY * 2) shouldBe List.empty\n  }\n\n  it should \"provide a container from pool with one single free container\" in {\n    val data = warmedData()\n    val pool = Map('warm -> data)\n    ContainerPool.remove(pool, MemoryLimit.STD_MEMORY) shouldBe List('warm)\n  }\n\n  it should \"provide oldest container from busy pool with multiple containers\" in {\n    val commonNamespace = differentNamespace.asString\n    val first = warmedData(namespace = commonNamespace, lastUsed = Instant.ofEpochMilli(1))\n    val second = warmedData(namespace = commonNamespace, lastUsed = Instant.ofEpochMilli(2))\n    val oldest = warmedData(namespace = commonNamespace, lastUsed = Instant.ofEpochMilli(0))\n\n    val pool = Map('first -> first, 'second -> second, 'oldest -> oldest)\n\n    ContainerPool.remove(pool, MemoryLimit.STD_MEMORY) shouldBe List('oldest)\n  }\n\n  it should \"provide a list of the oldest containers from pool, if several containers have to be removed\" in {\n    val namespace = differentNamespace.asString\n    val first = warmedData(namespace = namespace, lastUsed = Instant.ofEpochMilli(1))\n    val second = warmedData(namespace = namespace, lastUsed = Instant.ofEpochMilli(2))\n    val third = warmedData(namespace = namespace, lastUsed = Instant.ofEpochMilli(3))\n    val oldest = warmedData(namespace = namespace, lastUsed = Instant.ofEpochMilli(0))\n\n    val pool = Map('first -> first, 'second -> second, 'third -> third, 'oldest -> oldest)\n\n    ContainerPool.remove(pool, MemoryLimit.STD_MEMORY * 2) shouldBe List('oldest, 'first)\n  }\n\n  it should \"provide oldest container (excluding concurrently busy) from busy pool with multiple containers\" in {\n    val commonNamespace = differentNamespace.asString\n    val first = warmedData(namespace = commonNamespace, lastUsed = Instant.ofEpochMilli(1), active = 0)\n    val second = warmedData(namespace = commonNamespace, lastUsed = Instant.ofEpochMilli(2), active = 0)\n    val oldest = warmedData(namespace = commonNamespace, lastUsed = Instant.ofEpochMilli(0), active = 3)\n\n    var pool = Map('first -> first, 'second -> second, 'oldest -> oldest)\n    ContainerPool.remove(pool, MemoryLimit.STD_MEMORY) shouldBe List('first)\n    pool = pool - 'first\n    ContainerPool.remove(pool, MemoryLimit.STD_MEMORY) shouldBe List('second)\n  }\n\n  it should \"remove expired in order of expiration\" in {\n    val poolConfig = ContainerPoolConfig(0.MB, 0.5, false, 2.second, 10.seconds, None, 1, 3, false, 1.second, 10)\n    val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n    //use a second kind so that we know sorting is not isolated to the expired of each kind\n    val exec2 = CodeExecAsString(RuntimeManifest(\"actionKind2\", ImageName(\"testImage\")), \"testCode\", None)\n    val memoryLimit = 256.MB\n    val prewarmConfig =\n      List(\n        PrewarmingConfig(1, exec, memoryLimit, Some(ReactivePrewarmingConfig(0, 10, 10.seconds, 1, 1))),\n        PrewarmingConfig(1, exec2, memoryLimit, Some(ReactivePrewarmingConfig(0, 10, 10.seconds, 1, 1))))\n    val oldestDeadline = Deadline.now - 1.seconds\n    val newerDeadline = Deadline.now\n    val newestDeadline = Deadline.now + 1.seconds\n    val prewarmedPool = Map(\n      'newest -> preWarmedData(\"actionKind\", Some(newestDeadline)),\n      'oldest -> preWarmedData(\"actionKind2\", Some(oldestDeadline)),\n      'newer -> preWarmedData(\"actionKind\", Some(newerDeadline)))\n    lazy val stream = new ByteArrayOutputStream\n    lazy val printstream = new PrintStream(stream)\n    lazy implicit val logging: Logging = new PrintStreamLogging(printstream)\n    ContainerPool.removeExpired(poolConfig, prewarmConfig, prewarmedPool) shouldBe (List('oldest))\n  }\n\n  it should \"remove only the prewarmExpirationLimit of expired prewarms\" in {\n    //limit prewarm removal to 2\n    val poolConfig = ContainerPoolConfig(0.MB, 0.5, false, 2.second, 10.seconds, None, 2, 3, false, 1.second, 10)\n    val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n    val memoryLimit = 256.MB\n    val prewarmConfig =\n      List(PrewarmingConfig(3, exec, memoryLimit, Some(ReactivePrewarmingConfig(0, 10, 10.seconds, 1, 1))))\n    //all are overdue, with different expiration times\n    val oldestDeadline = Deadline.now - 5.seconds\n    val newerDeadline = Deadline.now - 4.seconds\n    //the newest* ones are expired, but not the oldest, and not within the limit of 2 prewarms, so won't be removed\n    val newestDeadline = Deadline.now - 3.seconds\n    val newestDeadline2 = Deadline.now - 2.seconds\n    val newestDeadline3 = Deadline.now - 1.seconds\n    val prewarmedPool = Map(\n      'newest -> preWarmedData(\"actionKind\", Some(newestDeadline)),\n      'oldest -> preWarmedData(\"actionKind\", Some(oldestDeadline)),\n      'newest3 -> preWarmedData(\"actionKind\", Some(newestDeadline3)),\n      'newer -> preWarmedData(\"actionKind\", Some(newerDeadline)),\n      'newest2 -> preWarmedData(\"actionKind\", Some(newestDeadline2)))\n    lazy val stream = new ByteArrayOutputStream\n    lazy val printstream = new PrintStream(stream)\n    lazy implicit val logging: Logging = new PrintStreamLogging(printstream)\n    ContainerPool.removeExpired(poolConfig, prewarmConfig, prewarmedPool) shouldBe (List('oldest, 'newer))\n  }\n\n  it should \"remove only the expired prewarms regardless of minCount\" in {\n    //limit prewarm removal to 100\n    val poolConfig = ContainerPoolConfig(0.MB, 0.5, false, 2.second, 10.seconds, None, 100, 3, false, 1.second, 10)\n    val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n    val memoryLimit = 256.MB\n    //minCount is 2 - should leave at least 2 prewarms when removing expired\n    val prewarmConfig =\n      List(PrewarmingConfig(3, exec, memoryLimit, Some(ReactivePrewarmingConfig(2, 10, 10.seconds, 1, 1))))\n    //all are overdue, with different expiration times\n    val oldestDeadline = Deadline.now - 5.seconds\n    val newerDeadline = Deadline.now - 4.seconds\n    //the newest* ones are expired, but not the oldest, and not within the limit of 2 prewarms, so won't be removed\n    val newestDeadline = Deadline.now - 3.seconds\n    val newestDeadline2 = Deadline.now - 2.seconds\n    val newestDeadline3 = Deadline.now - 1.seconds\n    val prewarmedPool = Map(\n      'newest -> preWarmedData(\"actionKind\", Some(newestDeadline)),\n      'oldest -> preWarmedData(\"actionKind\", Some(oldestDeadline)),\n      'newest3 -> preWarmedData(\"actionKind\", Some(newestDeadline3)),\n      'newer -> preWarmedData(\"actionKind\", Some(newerDeadline)),\n      'newest2 -> preWarmedData(\"actionKind\", Some(newestDeadline2)))\n    lazy val stream = new ByteArrayOutputStream\n    lazy val printstream = new PrintStream(stream)\n    lazy implicit val logging: Logging = new PrintStreamLogging(printstream)\n    ContainerPool.removeExpired(poolConfig, prewarmConfig, prewarmedPool) shouldBe (List(\n      'oldest,\n      'newer,\n      'newest,\n      'newest2,\n      'newest3))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.test\n\nimport java.net.InetSocketAddress\nimport java.time.Instant\n\nimport org.apache.pekko.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}\nimport org.apache.pekko.actor.{ActorRef, ActorSystem, FSM}\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.testkit.{CallingThreadDispatcher, ImplicitSender, TestKit, TestProbe}\nimport org.apache.pekko.util.ByteString\nimport common.{LoggedFunction, StreamLogging, SynchronizedLoggedFunction, WhiskProperties}\nimport java.time.temporal.ChronoUnit\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.apache.pekko.io.Tcp.{Close, CommandFailed, Connect, Connected}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.ack.ActiveAck\nimport org.apache.openwhisk.core.connector.{\n  AcknowledgementMessage,\n  ActivationMessage,\n  CombinedCompletionAndResultMessage,\n  CompletionMessage,\n  ResultMessage\n}\nimport org.apache.openwhisk.core.containerpool.WarmingData\nimport org.apache.openwhisk.core.containerpool._\nimport org.apache.openwhisk.core.containerpool.logging.LogCollectingException\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.core.database.UserContext\nimport org.apache.openwhisk.core.entity.ActivationResponse.ContainerResponse\nimport org.apache.openwhisk.core.invoker.Invoker\n\nimport scala.collection.mutable\nimport scala.concurrent.Await\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future, Promise}\n\n@RunWith(classOf[JUnitRunner])\nclass ContainerProxyTests\n    extends TestKit(ActorSystem(\"ContainerProxys\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with StreamLogging {\n\n  override def afterAll = TestKit.shutdownActorSystem(system)\n\n  val timeout = 5.seconds\n  val pauseGrace = timeout + 1.minute\n  val log = logging\n  val defaultUserMemory: ByteSize = 1024.MB\n\n  // Common entities to pass to the tests. We don't really care what's inside\n  // those for the behavior testing here, as none of the contents will really\n  // reach a container anyway. We merely assert that passing and extraction of\n  // the values is done properly.\n  val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n  val memoryLimit = 256.MB\n\n  val invocationNamespace = EntityName(\"invocationSpace\")\n  val action = ExecutableWhiskAction(EntityPath(\"actionSpace\"), EntityName(\"actionName\"), exec)\n\n  val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\", \"false\")).exists(_.toBoolean)\n  val testConcurrencyLimit = if (concurrencyEnabled) IntraConcurrencyLimit(2) else IntraConcurrencyLimit(1)\n  val concurrentAction = ExecutableWhiskAction(\n    EntityPath(\"actionSpace\"),\n    EntityName(\"actionName\"),\n    exec,\n    limits = ActionLimits(concurrency = testConcurrencyLimit))\n\n  // create a transaction id to set the start time and control queue time\n  val messageTransId = TransactionId(TransactionId.testing.meta.id)\n\n  val initInterval = {\n    val now = messageTransId.meta.start.plusMillis(50) // this is the queue time for cold start\n    Interval(now, now.plusMillis(100))\n  }\n\n  val runInterval = {\n    val now = initInterval.end.plusMillis(75) // delay between init and run\n    Interval(now, now.plusMillis(200))\n  }\n\n  val errorInterval = {\n    val now = initInterval.end.plusMillis(75) // delay between init and run\n    Interval(now, now.plusMillis(150))\n  }\n\n  val uuid = UUID()\n\n  val activationArguments = JsObject(\"ENV_VAR\" -> \"env\".toJson, \"param\" -> \"param\".toJson)\n\n  val message = ActivationMessage(\n    messageTransId,\n    action.fullyQualifiedName(true),\n    action.rev,\n    Identity(Subject(), Namespace(invocationNamespace, uuid), BasicAuthenticationAuthKey(uuid, Secret())),\n    ActivationId.generate(),\n    ControllerInstanceId(\"0\"),\n    blocking = false,\n    content = Some(activationArguments),\n    initArgs = Set(\"ENV_VAR\"),\n    lockedArgs = Map.empty)\n\n  /*\n   * Helpers for assertions and actor lifecycles\n   */\n  /** Imitates a StateTimeout in the FSM */\n  def timeout(actor: ActorRef) = actor ! FSM.StateTimeout\n\n  /** Registers the transition callback and expects the first message */\n  def registerCallback(c: ActorRef) = {\n    c ! SubscribeTransitionCallBack(testActor)\n    expectMsg(CurrentState(c, Uninitialized))\n  }\n\n  /** Pre-warms the given state-machine, assumes good cases */\n  def preWarm(machine: ActorRef) = {\n    machine ! Start(exec, memoryLimit)\n    expectMsg(Transition(machine, Uninitialized, Starting))\n    expectPreWarmed(exec.kind)\n    expectMsg(Transition(machine, Starting, Started))\n  }\n\n  /** Run the common action on the state-machine, assumes good cases */\n  def run(machine: ActorRef, currentState: ContainerState) = {\n    machine ! Run(action, message)\n    expectMsg(Transition(machine, currentState, Running))\n    expectWarmed(invocationNamespace.name, action)\n    expectMsg(Transition(machine, Running, Ready))\n  }\n\n  /** Expect a NeedWork message with prewarmed data */\n  def expectPreWarmed(kind: String) = expectMsgPF() {\n    case NeedWork(PreWarmedData(_, kind, memoryLimit, _, _)) => true\n  }\n\n  /** Expect a NeedWork message with warmed data */\n  def expectWarmed(namespace: String, action: ExecutableWhiskAction) = {\n    val test = EntityName(namespace)\n    expectMsgPF() {\n      case a @ NeedWork(WarmedData(_, `test`, `action`, _, _, _)) => //matched, otherwise will fail\n    }\n  }\n\n  /** Expect the container to pause successfully */\n  def expectPause(machine: ActorRef) = {\n    expectMsg(Transition(machine, Ready, Pausing))\n    expectMsg(Transition(machine, Pausing, Paused))\n  }\n\n  trait LoggedAcker extends ActiveAck {\n    def calls =\n      mutable.Buffer[(TransactionId, WhiskActivation, Boolean, ControllerInstanceId, UUID, AcknowledgementMessage)]()\n\n    def verifyAnnotations(activation: WhiskActivation, a: ExecutableWhiskAction) = {\n      activation.annotations.get(\"limits\") shouldBe Some(a.limits.toJson)\n      activation.annotations.get(\"path\") shouldBe Some(a.fullyQualifiedName(false).toString.toJson)\n      activation.annotations.get(\"kind\") shouldBe Some(a.exec.kind.toJson)\n    }\n  }\n\n  /** Creates an inspectable version of the ack method, which records all calls in a buffer */\n  def createAcker(a: ExecutableWhiskAction = action) = new LoggedAcker {\n    val acker = LoggedFunction {\n      (_: TransactionId,\n       activation: WhiskActivation,\n       _: Boolean,\n       _: ControllerInstanceId,\n       _: UUID,\n       _: AcknowledgementMessage) =>\n        Future.successful(())\n    }\n\n    override def calls = acker.calls\n\n    override def apply(tid: TransactionId,\n                       activation: WhiskActivation,\n                       blockingInvoke: Boolean,\n                       controllerInstance: ControllerInstanceId,\n                       userId: UUID,\n                       acknowledgement: AcknowledgementMessage): Future[Any] = {\n      verifyAnnotations(activation, a)\n      acker(tid, activation, blockingInvoke, controllerInstance, userId, acknowledgement)\n    }\n  }\n\n  /** Creates an synchronized inspectable version of the ack method, which records all calls in a buffer */\n  def createSyncAcker(a: ExecutableWhiskAction = action) = new LoggedAcker {\n    val acker = SynchronizedLoggedFunction {\n      (_: TransactionId,\n       activation: WhiskActivation,\n       _: Boolean,\n       _: ControllerInstanceId,\n       _: UUID,\n       _: AcknowledgementMessage) =>\n        Future.successful(())\n    }\n\n    override def calls = acker.calls\n\n    override def apply(tid: TransactionId,\n                       activation: WhiskActivation,\n                       blockingInvoke: Boolean,\n                       controllerInstance: ControllerInstanceId,\n                       userId: UUID,\n                       acknowledgement: AcknowledgementMessage): Future[Any] = {\n      verifyAnnotations(activation, a)\n      acker(tid, activation, blockingInvoke, controllerInstance, userId, acknowledgement)\n    }\n  }\n\n  /** Creates an inspectable factory */\n  def createFactory(response: Future[Container]) = LoggedFunction {\n    (_: TransactionId,\n     _: String,\n     _: ImageName,\n     _: Boolean,\n     _: ByteSize,\n     _: Int,\n     _: Option[Double],\n     _: Option[ExecutableWhiskAction]) =>\n      response\n  }\n\n  class LoggedCollector(response: Future[ActivationLogs], invokeCallback: () => Unit) extends Invoker.LogsCollector {\n    val collector = LoggedFunction {\n      (transid: TransactionId,\n       user: Identity,\n       activation: WhiskActivation,\n       container: Container,\n       action: ExecutableWhiskAction) =>\n        response\n    }\n\n    def calls = collector.calls\n\n    override def apply(transid: TransactionId,\n                       user: Identity,\n                       activation: WhiskActivation,\n                       container: Container,\n                       action: ExecutableWhiskAction) = {\n      invokeCallback()\n      collector(transid, user, activation, container, action)\n    }\n  }\n\n  def createCollector(response: Future[ActivationLogs] = Future.successful(ActivationLogs()),\n                      invokeCallback: () => Unit = () => ()) =\n    new LoggedCollector(response, invokeCallback)\n\n  def createStore = LoggedFunction {\n    (transid: TransactionId, activation: WhiskActivation, isBlockingActivation: Boolean, context: UserContext) =>\n      Future.successful(())\n  }\n  def createSyncStore = SynchronizedLoggedFunction {\n    (transid: TransactionId, activation: WhiskActivation, isBlockingActivation: Boolean, context: UserContext) =>\n      Future.successful(())\n  }\n  val poolConfig = ContainerPoolConfig(2.MB, 0.5, false, 2.second, 1.minute, None, 100, 3, false, 1.second, 10)\n  def healthchecksConfig(enabled: Boolean = false) = ContainerProxyHealthCheckConfig(enabled, 100.milliseconds, 2)\n  val filterEnvVar = (k: String) => Character.isUpperCase(k.charAt(0))\n\n  behavior of \"ContainerProxy\"\n\n  it should \"partition activation arguments into environment variables and main arguments\" in {\n    ContainerProxy.partitionArguments(None, Set.empty) should be(Map.empty, JsObject.empty)\n    ContainerProxy.partitionArguments(Some(JsObject.empty), Set(\"a\")) should be(Map.empty, JsObject.empty)\n\n    val content = JsObject(\"a\" -> \"A\".toJson, \"b\" -> \"B\".toJson, \"C\" -> \"c\".toJson, \"D\" -> \"d\".toJson)\n    val (env, args) = ContainerProxy.partitionArguments(Some(content), Set(\"C\", \"D\"))\n    env should be {\n      content.fields.filter(k => filterEnvVar(k._1))\n    }\n\n    args should be {\n      JsObject(content.fields.filterNot(k => filterEnvVar(k._1)))\n    }\n  }\n\n  it should \"unlock arguments\" in {\n    val k128 = \"ra1V6AfOYAv0jCzEdufIFA==\"\n    val coder = ParameterEncryption(ParameterStorageConfig(\"aes-128\", aes128 = Some(k128)))\n    val locker = Some(coder.encryptor(\"aes-128\"))\n\n    val param = Parameters(\"a\", \"abc\").lock(locker).merge(Some(JsObject(\"b\" -> JsString(\"xyz\"))))\n    param.get.compactPrint should not include \"abc\"\n    ContainerProxy.unlockArguments(param, Map(\"a\" -> \"aes-128\"), coder) shouldBe Some {\n      JsObject(\"a\" -> JsString(\"abc\"), \"b\" -> JsString(\"xyz\"))\n    }\n  }\n\n  /*\n   * SUCCESSFUL CASES\n   */\n  it should \"create a container given a Start message\" in within(timeout) {\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val store = createStore\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            createAcker(),\n            store,\n            createCollector(),\n            InvokerInstanceId(0, Some(\"myname\"), userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    preWarm(machine)\n\n    factory.calls should have size 1\n    val (tid, name, _, _, memory, cpuShares, _, _) = factory.calls(0)\n    tid shouldBe TransactionId.invokerWarmup\n    name should fullyMatch regex \"\"\"wskmyname\\d+_\\d+_prewarm_actionKind\"\"\"\n    memory shouldBe memoryLimit\n  }\n\n  it should \"run a container which has been started before, write an active ack, write to the store, pause and remove the container\" in within(\n    timeout) {\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n\n    preWarm(machine)\n    run(machine, Started)\n\n    // Timeout causes the container to pause\n    timeout(machine)\n    expectPause(machine)\n\n    // Another pause causes the container to be removed\n    timeout(machine)\n    expectMsg(RescheduleJob)\n    expectMsg(Transition(machine, Paused, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls should have size 1\n      container.suspendCount shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls should have size 1\n      store.calls should have size 1\n    }\n  }\n\n  it should \"run an action and continue with a next run without pausing the container\" in within(timeout) {\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    preWarm(machine)\n\n    run(machine, Started)\n    // Note that there are no intermediate state changes\n    run(machine, Ready)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 2\n      collector.calls should have size 2\n      container.suspendCount shouldBe 0\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      // As the active acks are sent asynchronously, it is possible, that the activation with the init time is not the\n      // first one in the buffer.\n      val (initRunActivation, runOnlyActivation) = {\n        // false is sorted before true\n        val sorted = acker.calls.sortBy(_._2.annotations.get(WhiskActivation.initTimeAnnotation).isEmpty)\n        (sorted.head._2, sorted(1)._2)\n      }\n\n      initRunActivation.annotations.get(WhiskActivation.initTimeAnnotation) should not be empty\n      initRunActivation.duration shouldBe Some((initInterval.duration + runInterval.duration).toMillis)\n      initRunActivation.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n      initRunActivation.annotations\n        .get(WhiskActivation.waitTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe\n        Interval(message.transid.meta.start, initInterval.start).duration.toMillis\n\n      runOnlyActivation.duration shouldBe Some(runInterval.duration.toMillis)\n      runOnlyActivation.annotations.get(WhiskActivation.initTimeAnnotation) shouldBe empty\n      runOnlyActivation.annotations.get(WhiskActivation.waitTimeAnnotation).get.convertTo[Int] shouldBe {\n        Interval(message.transid.meta.start, runInterval.start).duration.toMillis\n      }\n    }\n  }\n\n  it should \"run an action after pausing the container\" in within(timeout) {\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    preWarm(machine)\n\n    run(machine, Started)\n    timeout(machine)\n    expectPause(machine)\n    run(machine, Paused)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 2\n      collector.calls should have size 2\n      container.suspendCount shouldBe 1\n      container.resumeCount shouldBe 1\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      // As the active acks are sent asynchronously, it is possible, that the activation with the init time is not the\n      // first one in the buffer.\n      val initializedActivations =\n        acker.calls.filter(_._2.annotations.get(WhiskActivation.initTimeAnnotation).isDefined)\n      initializedActivations should have size 1\n\n      initializedActivations.head._2.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n    }\n  }\n\n  it should \"successfully run on an uninitialized container\" in within(timeout) {\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    run(machine, Uninitialized)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls should have size 1\n      acker.calls should have size 1\n      store.calls should have size 1\n      acker\n        .calls(0)\n        ._2\n        .annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n    }\n  }\n\n  it should \"not collect logs if the log-limit is set to 0\" in within(timeout) {\n    val noLogsAction = action.copy(limits = ActionLimits(logs = LogLimit(0.MB)))\n\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker(noLogsAction)\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n\n    machine ! Run(noLogsAction, message)\n    expectMsg(Transition(machine, Uninitialized, Running))\n    expectWarmed(invocationNamespace.name, noLogsAction)\n    expectMsg(Transition(machine, Running, Ready))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls should have size 0\n      acker.calls should have size 1\n      store.calls should have size 1\n      acker.calls.head._6 shouldBe a[CompletionMessage]\n    }\n  }\n\n  it should \"resend a failed Run when it is first Run after Ready state\" in within(timeout) {\n    val noLogsAction = action.copy(limits = ActionLimits(logs = LogLimit(0.MB)))\n    val runPromises = Seq(Promise[(Interval, ActivationResponse)](), Promise[(Interval, ActivationResponse)]())\n    val container = new TestContainer(runPromises = runPromises)\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker(noLogsAction)\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n\n    machine ! Run(noLogsAction, message)\n    expectMsg(Transition(machine, Uninitialized, Running))\n    //run the first successfully\n    runPromises(0).success(runInterval, ActivationResponse.success())\n    expectWarmed(invocationNamespace.name, noLogsAction)\n    expectMsg(Transition(machine, Running, Ready))\n\n    val failingRun = Run(noLogsAction, message)\n    val runAfterFail = Run(noLogsAction, message)\n    //should fail and retry\n    machine ! failingRun\n    machine ! runAfterFail //will be buffered first, and then retried\n    expectMsg(Transition(machine, Ready, Running))\n    //run the second as failure\n    runPromises(1).failure(ContainerHealthError(messageTransId, \"intentional failure\"))\n    //on failure, buffered are resent first\n    expectMsg(runAfterFail)\n    //resend the first run to parent, and start removal process\n    expectMsg(RescheduleJob)\n    expectMsg(Transition(machine, Running, Removing))\n    expectMsg(failingRun)\n    expectNoMessage(100.milliseconds)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 2\n      collector.calls should have size 0\n      acker.calls should have size 1\n      store.calls should have size 1\n      acker.calls.head._6 shouldBe a[CompletionMessage]\n    }\n  }\n\n  it should \"start tcp ping to containers when action healthcheck enabled\" in within(timeout) {\n    val noLogsAction = action.copy(limits = ActionLimits(logs = LogLimit(0.MB)))\n    val container = new TestContainer()\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker(noLogsAction)\n    val store = createStore\n    val collector = createCollector()\n    val tcpProbe = TestProbe()\n    val healthchecks = healthchecksConfig(true)\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecks,\n            pauseGrace = pauseGrace,\n            tcp = Some(tcpProbe.ref)))\n    registerCallback(machine)\n    preWarm(machine)\n\n    tcpProbe.expectMsg(Connect(new InetSocketAddress(\"0.0.0.0\", 8080)))\n    tcpProbe.expectMsg(Connect(new InetSocketAddress(\"0.0.0.0\", 8080)))\n    tcpProbe.expectMsg(Connect(new InetSocketAddress(\"0.0.0.0\", 8080)))\n    //pings should repeat till the container goes into Running state\n    run(machine, Started)\n    tcpProbe.expectNoMessage(healthchecks.checkPeriod + 100.milliseconds)\n\n    awaitAssert {\n      factory.calls should have size 1\n    }\n  }\n\n  it should \"respond with CombinedCompletionAndResultMessage for blocking invocation with no logs\" in within(timeout) {\n    val noLogsAction = action.copy(limits = ActionLimits(logs = LogLimit(0.MB)))\n    val blockingMessage = message.copy(blocking = true)\n    val (factory, container, acker, store, collector, machine) = createServices(noLogsAction)\n\n    sendActivationMessage(machine, blockingMessage, noLogsAction)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n\n      //For no log case log collector call should be zero\n      collector.calls should have size 0\n\n      //There would be only 1 call\n      // First with CombinedCompletionAndResultMessage\n      acker.calls should have size 1\n      store.calls should have size 1\n      acker.calls.head._6 shouldBe a[CombinedCompletionAndResultMessage]\n    }\n  }\n\n  it should \"respond with ResultMessage and CompletionMessage for blocking invocation with logs\" in within(timeout) {\n    val blockingMessage = message.copy(blocking = true)\n    val (factory, container, acker, store, collector, machine) = createServices(action)\n\n    sendActivationMessage(machine, blockingMessage, action)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n\n      //State related checks\n      collector.calls should have size 1\n\n      //There would be 2 calls\n      // First with ResultMessage\n      // Second with CompletionMessage.\n      acker.calls should have size 2\n      store.calls should have size 1\n      acker.calls.head._6 shouldBe a[ResultMessage]\n      acker.calls.last._6 shouldBe a[CompletionMessage]\n    }\n  }\n\n  it should \"respond with only CompletionMessage for non blocking invocation with logs\" in within(timeout) {\n    val nonBlockingMessage = message.copy(blocking = false)\n    val (factory, container, acker, store, collector, machine) = createServices(action)\n\n    sendActivationMessage(machine, nonBlockingMessage, action)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n\n      //For log case log collector call should be one\n      collector.calls should have size 1\n\n      //There would only be 1 call\n      // First with CompletionMessage\n      acker.calls should have size 1\n      store.calls should have size 1\n      acker.calls.head._6 shouldBe a[CompletionMessage]\n    }\n  }\n\n  it should \"respond with only CompletionMessage for non blocking invocation with no logs\" in within(timeout) {\n    val noLogsAction = action.copy(limits = ActionLimits(logs = LogLimit(0.MB)))\n    val nonBlockingMessage = message.copy(blocking = false)\n    val (factory, container, acker, store, collector, machine) = createServices(noLogsAction)\n\n    sendActivationMessage(machine, nonBlockingMessage, noLogsAction)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n\n      //For no log case log collector call should be zero\n      collector.calls should have size 0\n\n      //There would only be 1 call\n      // First with CompletionMessage\n      acker.calls should have size 1\n      store.calls should have size 1\n      acker.calls.head._6 shouldBe a[CompletionMessage]\n    }\n  }\n\n  private def createServices(action: ExecutableWhiskAction) = {\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker(action)\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    (factory, container, acker, store, collector, machine)\n  }\n\n  private def sendActivationMessage(machine: ActorRef, message: ActivationMessage, action: ExecutableWhiskAction) = {\n    machine ! Run(action, message)\n    expectMsg(Transition(machine, Uninitialized, Running))\n    expectWarmed(invocationNamespace.name, action)\n    expectMsg(Transition(machine, Running, Ready))\n  }\n\n  //This tests concurrency from the ContainerPool perspective - where multiple Run messages may be sent to ContainerProxy\n  //without waiting for the completion of the previous Run message (signaled by NeedWork message)\n  //Multiple messages can only be handled after Warming.\n  it should \"stay in Running state if others are still running\" in within(timeout) {\n    assume(concurrencyEnabled)\n\n    val initPromise = Promise[Interval]()\n    val runPromises = Seq(\n      Promise[(Interval, ActivationResponse)](),\n      Promise[(Interval, ActivationResponse)](),\n      Promise[(Interval, ActivationResponse)](),\n      Promise[(Interval, ActivationResponse)](),\n      Promise[(Interval, ActivationResponse)](),\n      Promise[(Interval, ActivationResponse)]())\n    val container = new TestContainer(Some(initPromise), runPromises)\n    val factory = createFactory(Future.successful(container))\n    val acker = createSyncAcker(concurrentAction)\n    val store = createSyncStore\n    val collector =\n      createCollector(Future.successful(ActivationLogs()), () => container.logs(0.MB, false)(TransactionId.testing))\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace)\n          .withDispatcher(CallingThreadDispatcher.Id))\n    registerCallback(machine)\n    preWarm(machine) //ends in Started state\n\n    machine ! Run(concurrentAction, message) //first in Started state\n    machine ! Run(concurrentAction, message) //second in Started or Running state\n\n    //first message go from Started -> Running -> Ready, with 2 NeedWork messages (1 for init, 1 for run)\n    //second message will be delayed until we get to Running state with WarmedData\n    //   (and will produce 1 NeedWork message after run)\n    expectMsg(Transition(machine, Started, Running))\n\n    //complete the init\n    initPromise.success(initInterval)\n\n    //complete the first run\n    runPromises(0).success(runInterval, ActivationResponse.success())\n\n    //room for 1 more, so expect NeedWork msg\n    expectWarmed(invocationNamespace.name, concurrentAction) //when first completes\n\n    //complete the second run\n    runPromises(1).success(runInterval, ActivationResponse.success())\n\n    //room for 1 more, so expect NeedWork msg\n    expectWarmed(invocationNamespace.name, concurrentAction) //when second completes\n\n    //go back to ready after first and second runs are complete\n    expectMsg(Transition(machine, Running, Ready))\n\n    machine ! Run(concurrentAction, message) //third in Ready state\n    machine ! Run(concurrentAction, message) //fourth in Ready state\n    machine ! Run(concurrentAction, message) //fifth in Ready state - will be queued\n    machine ! Run(concurrentAction, message) //sixth in Ready state - will be queued\n\n    //third message will go from Ready -> Running -> Ready (after fourth run)\n    expectMsg(Transition(machine, Ready, Running))\n    //expect no NeedWork since there are still running and queued messages\n    expectNoMessage(500.milliseconds)\n\n    //complete the third run (do not request new work yet)\n    runPromises(2).success(runInterval, ActivationResponse.success())\n    //expect no NeedWork since there are still queued messages\n    expectNoMessage(500.milliseconds)\n\n    //complete the fourth run -> dequeue the fifth run (do not request new work yet)\n    runPromises(3).success(runInterval, ActivationResponse.success())\n\n    //complete the fifth run (request new work, 1 active remain)\n    runPromises(4).success(runInterval, ActivationResponse.success())\n\n    //request new work since buffer is now empty AND activationCount < concurrent max\n    expectWarmed(invocationNamespace.name, concurrentAction) //when fifth completes\n\n    //complete the sixth run (request new work 0 active remain)\n    runPromises(5).success(runInterval, ActivationResponse.success())\n\n    // back to ready\n    expectWarmed(invocationNamespace.name, concurrentAction) //when sixth completes\n    expectMsg(Transition(machine, Running, Ready))\n\n    //timeout + pause after getting back to Ready\n    timeout(machine)\n    expectMsg(Transition(machine, Ready, Pausing))\n    expectMsg(Transition(machine, Pausing, Paused))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 6\n      container.atomicLogsCount.get() shouldBe 6\n      container.suspendCount shouldBe 1\n      container.resumeCount shouldBe 0\n      acker.calls should have size 6\n\n      store.calls should have size 6\n\n      // As the active acks are sent asynchronously, it is possible, that the activation with the init time is not the\n      // first one in the buffer.\n      val initializedActivations =\n        acker.calls.filter(_._2.annotations.get(WhiskActivation.initTimeAnnotation).isDefined)\n      initializedActivations should have size 1\n\n      initializedActivations.head._2.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n    }\n\n  }\n\n  it should \"not destroy on failure during Removing state when concurrent activations are in flight\" in {\n    assume(concurrencyEnabled)\n\n    val initPromise = Promise[Interval]()\n    val runPromises = Seq(Promise[(Interval, ActivationResponse)](), Promise[(Interval, ActivationResponse)]())\n    val container = new TestContainer(Some(initPromise), runPromises)\n    val factory = createFactory(Future.successful(container))\n    val acker = createSyncAcker(concurrentAction)\n    val store = createSyncStore\n    val collector =\n      createCollector(Future.successful(ActivationLogs()), () => container.logs(0.MB, false)(TransactionId.testing))\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace)\n          .withDispatcher(CallingThreadDispatcher.Id))\n    registerCallback(machine)\n    preWarm(machine) //ends in Started state\n\n    machine ! Run(concurrentAction, message) //first in Started state\n    machine ! Run(concurrentAction, message) //second in Started or Running state\n\n    //first message go from Started -> Running -> Ready, with 2 NeedWork messages (1 for init, 1 for run)\n    //second message will be delayed until we get to Running state with WarmedData\n    //   (and will produce 1 NeedWork message after run)\n    expectMsg(Transition(machine, Started, Running))\n\n    //complete the init\n    initPromise.success(initInterval)\n\n    //fail the first run\n    runPromises(0).success(runInterval, ActivationResponse.whiskError(\"intentional failure in test\"))\n    //fail the second run\n    runPromises(1).success(runInterval, ActivationResponse.whiskError(\"intentional failure in test\"))\n    //go to Removing state when a failure happens while others are in flight\n    expectMsg(Transition(machine, Running, Removing))\n    expectMsg(RescheduleJob)\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 2\n      container.atomicLogsCount.get() shouldBe 2\n      container.suspendCount shouldBe 0\n      container.resumeCount shouldBe 0\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      // As the active acks are sent asynchronously, it is possible, that the activation with the init time is not the\n      // first one in the buffer.\n      val initializedActivations =\n        acker.calls.filter(_._2.annotations.get(WhiskActivation.initTimeAnnotation).isDefined)\n      initializedActivations should have size 1\n\n      initializedActivations.head._2.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n    }\n  }\n\n  it should \"not destroy on failure during Running state when concurrent activations are in flight\" in {\n    assume(concurrencyEnabled)\n\n    val initPromise = Promise[Interval]()\n    val runPromises = Seq(Promise[(Interval, ActivationResponse)](), Promise[(Interval, ActivationResponse)]())\n    val container = new TestContainer(Some(initPromise), runPromises)\n    val factory = createFactory(Future.successful(container))\n    val acker = createSyncAcker(concurrentAction)\n    val store = createSyncStore\n    val collector =\n      createCollector(Future.successful(ActivationLogs()), () => container.logs(0.MB, false)(TransactionId.testing))\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace)\n          .withDispatcher(CallingThreadDispatcher.Id))\n    registerCallback(machine)\n    preWarm(machine) //ends in Started state\n\n    machine ! Run(concurrentAction, message) //first in Started state\n    machine ! Run(concurrentAction, message) //second in Started or Running state\n\n    //first message go from Started -> Running -> Ready, with 2 NeedWork messages (1 for init, 1 for run)\n    //second message will be delayed until we get to Running state with WarmedData\n    //   (and will produce 1 NeedWork message after run)\n    expectMsg(Transition(machine, Started, Running))\n\n    //complete the init\n    initPromise.success(initInterval)\n\n    //fail the first run\n    runPromises(0).success(runInterval, ActivationResponse.whiskError(\"intentional failure in test\"))\n    //succeed the second run\n    runPromises(1).success(runInterval, ActivationResponse.success())\n    //go to Removing state when a failure happens while others are in flight\n    expectMsg(Transition(machine, Running, Removing))\n    expectMsg(RescheduleJob)\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 2\n      container.atomicLogsCount.get() shouldBe 2\n      container.suspendCount shouldBe 0\n      container.resumeCount shouldBe 0\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      // As the active acks are sent asynchronously, it is possible, that the activation with the init time is not the\n      // first one in the buffer.\n      val initializedActivations =\n        acker.calls.filter(_._2.annotations.get(WhiskActivation.initTimeAnnotation).isDefined)\n      initializedActivations should have size 1\n\n      initializedActivations.head._2.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n    }\n  }\n  it should \"terminate buffered concurrent activations when prewarm init fails with an error\" in {\n    assume(Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean))\n\n    val initPromise = Promise[Interval]()\n    val container = new TestContainer(Some(initPromise))\n    val factory = createFactory(Future.successful(container))\n    val acker = createSyncAcker(concurrentAction)\n    val store = createSyncStore\n    val collector =\n      createCollector(Future.successful(ActivationLogs()), () => container.logs(0.MB, false)(TransactionId.testing))\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace)\n          .withDispatcher(CallingThreadDispatcher.Id))\n    registerCallback(machine)\n    preWarm(machine) //ends in Started state\n\n    machine ! Run(concurrentAction, message) //first in Started state\n    machine ! Run(concurrentAction, message) //second in Started or Running state\n\n    //first message go from Started -> Running -> Ready, with 2 NeedWork messages (1 for init, 1 for run)\n    //second message will be delayed until we get to Running state with WarmedData\n    //   (and will produce 1 NeedWork message after run)\n    expectMsg(Transition(machine, Started, Running))\n\n    //complete the init\n    initPromise.failure(\n      InitializationError(\n        initInterval,\n        ActivationResponse\n          .processInitResponseContent(Right(ContainerResponse(false, \"some bad init response...\")), logging)))\n\n    expectMsg(ContainerRemoved(true))\n    //go to Removing state when a failure happens while others are in flight\n    expectMsg(Transition(machine, Running, Removing))\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 0\n      container.atomicLogsCount.get() shouldBe 1\n      container.suspendCount shouldBe 0\n      container.resumeCount shouldBe 0\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      //we should have 2 activations that are container error\n      acker.calls.filter(_._2.response.isContainerError) should have size 2\n    }\n  }\n\n  it should \"terminate buffered concurrent activations when prewarm init fails unexpectedly\" in {\n    assume(Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean))\n\n    val initPromise = Promise[Interval]()\n    val container = new TestContainer(Some(initPromise))\n    val factory = createFactory(Future.successful(container))\n    val acker = createSyncAcker(concurrentAction)\n    val store = createSyncStore\n    val collector =\n      createCollector(Future.successful(ActivationLogs()), () => container.logs(0.MB, false)(TransactionId.testing))\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace)\n          .withDispatcher(CallingThreadDispatcher.Id))\n    registerCallback(machine)\n    preWarm(machine) //ends in Started state\n\n    machine ! Run(concurrentAction, message) //first in Started state\n    machine ! Run(concurrentAction, message) //second in Started or Running state\n\n    //first message go from Started -> Running -> Ready, with 2 NeedWork messages (1 for init, 1 for run)\n    //second message will be delayed until we get to Running state with WarmedData\n    //   (and will produce 1 NeedWork message after run)\n    expectMsg(Transition(machine, Started, Running))\n\n    //complete the init\n    initPromise.failure(new IllegalStateException(\"intentional failure during init test\"))\n\n    expectMsg(ContainerRemoved(true))\n    //go to Removing state when a failure happens while others are in flight\n    expectMsg(Transition(machine, Running, Removing))\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 0\n      container.atomicLogsCount.get() shouldBe 1\n      container.suspendCount shouldBe 0\n      container.resumeCount shouldBe 0\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      //we should have 2 activations that are whisk error\n      acker.calls.filter(_._2.response.isWhiskError) should have size 2\n    }\n  }\n\n  it should \"terminate buffered concurrent activations when cold init fails with an error\" in {\n    assume(Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean))\n\n    val initPromise = Promise[Interval]()\n    val container = new TestContainer(Some(initPromise))\n    val factory = createFactory(Future.successful(container))\n    val acker = createSyncAcker(concurrentAction)\n    val store = createSyncStore\n    val collector =\n      createCollector(Future.successful(ActivationLogs()), () => container.logs(0.MB, false)(TransactionId.testing))\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace)\n          .withDispatcher(CallingThreadDispatcher.Id))\n    registerCallback(machine)\n    //no prewarming\n\n    machine ! Run(concurrentAction, message) //first in Uninitialized state\n    machine ! Run(concurrentAction, message) //second in Uninitialized or Running state\n\n    expectMsg(Transition(machine, Uninitialized, Running))\n\n    //complete the init\n    initPromise.failure(\n      InitializationError(\n        initInterval,\n        ActivationResponse\n          .processInitResponseContent(Right(ContainerResponse(false, \"some bad init response...\")), logging)))\n\n    expectMsg(ContainerRemoved(true))\n    //go to Removing state when a failure happens while others are in flight\n    expectMsg(Transition(machine, Running, Removing))\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 0\n      container.atomicLogsCount.get() shouldBe 1\n      container.suspendCount shouldBe 0\n      container.resumeCount shouldBe 0\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      //we should have 2 activations that are container error\n      acker.calls.filter(_._2.response.isContainerError) should have size 2\n    }\n  }\n\n  it should \"terminate buffered concurrent activations when cold init fails unexpectedly\" in {\n    assume(Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean))\n\n    val initPromise = Promise[Interval]()\n    val container = new TestContainer(Some(initPromise))\n    val factory = createFactory(Future.successful(container))\n    val acker = createSyncAcker(concurrentAction)\n    val store = createSyncStore\n    val collector =\n      createCollector(Future.successful(ActivationLogs()), () => container.logs(0.MB, false)(TransactionId.testing))\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace)\n          .withDispatcher(CallingThreadDispatcher.Id))\n    registerCallback(machine)\n    //no prewarming\n\n    machine ! Run(concurrentAction, message) //first in Uninitialized state\n    machine ! Run(concurrentAction, message) //second in Uninitialized or Running state\n\n    expectMsg(Transition(machine, Uninitialized, Running))\n\n    //complete the init\n    initPromise.failure(new IllegalStateException(\"intentional failure during init test\"))\n\n    expectMsg(ContainerRemoved(true))\n    //go to Removing state when a failure happens while others are in flight\n    expectMsg(Transition(machine, Running, Removing))\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 0\n      container.atomicLogsCount.get() shouldBe 1\n      container.suspendCount shouldBe 0\n      container.resumeCount shouldBe 0\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      //we should have 2 activations that are whisk error\n      acker.calls.filter(_._2.response.isWhiskError) should have size 2\n    }\n  }\n\n  it should \"terminate buffered concurrent activations when cold init fails to launch container\" in {\n    assume(Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean))\n\n    val containerPromise = Promise[Container]\n    val factory = createFactory(containerPromise.future)\n    val acker = createSyncAcker(concurrentAction)\n    val store = createSyncStore\n    val collector =\n      createCollector(Future.successful(ActivationLogs()), () => ())\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace)\n          .withDispatcher(CallingThreadDispatcher.Id))\n    registerCallback(machine)\n    //no prewarming\n\n    machine ! Run(concurrentAction, message) //first in Uninitialized state\n    machine ! Run(concurrentAction, message) //second in Uninitialized or Running state\n\n    //wait for buffering before failing the container\n    containerPromise.failure(new Exception(\"simulating a container creation failure\"))\n\n    expectMsg(Transition(machine, Uninitialized, Running))\n\n    expectMsg(ContainerRemoved(true))\n    //go to Removing state when a failure happens while others are in flight\n    expectMsg(Transition(machine, Running, Removing))\n    awaitAssert {\n      factory.calls should have size 1\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      //we should have 2 activations that are whisk error\n      acker.calls.filter(_._2.response.isWhiskError) should have size 2\n    }\n  }\n\n  it should \"complete the transaction and reuse the container on a failed run IFF failure was applicationError\" in within(\n    timeout) {\n    val container = new TestContainer {\n      override def run(\n        parameters: JsValue,\n        environment: JsObject,\n        timeout: FiniteDuration,\n        concurrent: Int,\n        maxResponse: ByteSize,\n        truncation: ByteSize,\n        reschedule: Boolean = false)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n        atomicRunCount.incrementAndGet()\n        //every other run fails\n        if (runCount % 2 == 0) {\n          Future.successful((runInterval, ActivationResponse.success()))\n        } else {\n          Future.successful((errorInterval, ActivationResponse.applicationError((\"boom\"))))\n        }\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = timeout))\n    registerCallback(machine)\n    preWarm(machine)\n\n    //first one will fail\n    run(machine, Started)\n\n    // Note that there are no intermediate state changes\n    //second one will succeed\n    run(machine, Ready)\n\n    timeout(machine) // times out Ready state so container suspends\n    expectMsg(Transition(machine, Ready, Pausing))\n    expectMsg(Transition(machine, Pausing, Paused))\n    //With exception of the error on first run, the assertions should be the same as in\n    //         `run an action and continue with a next run without pausing the container`\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 2\n      collector.calls should have size 2\n      container.suspendCount shouldBe 1\n      container.destroyCount shouldBe 0\n      acker.calls should have size 2\n\n      store.calls should have size 2\n\n      // As the active acks are sent asynchronously, it is possible, that the activation with the init time is not the\n      // first one in the buffer.\n      val (initErrorActivation, runOnlyActivation) = {\n        // false is sorted before true\n        val sorted = acker.calls.sortBy(_._2.annotations.get(WhiskActivation.initTimeAnnotation).isEmpty)\n        (sorted.head._2, sorted(1)._2)\n      }\n\n      initErrorActivation.annotations.get(WhiskActivation.initTimeAnnotation) should not be empty\n      initErrorActivation.duration shouldBe Some((initInterval.duration + errorInterval.duration).toMillis)\n      initErrorActivation.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n      initErrorActivation.annotations\n        .get(WhiskActivation.waitTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe\n        Interval(message.transid.meta.start, initInterval.start).duration.toMillis\n\n      runOnlyActivation.duration shouldBe Some(runInterval.duration.toMillis)\n      runOnlyActivation.annotations.get(WhiskActivation.initTimeAnnotation) shouldBe empty\n      runOnlyActivation.annotations.get(WhiskActivation.waitTimeAnnotation).get.convertTo[Int] shouldBe {\n        Interval(message.transid.meta.start, runInterval.start).duration.toMillis\n      }\n    }\n\n  }\n\n  /*\n   * ERROR CASES\n   */\n  it should \"complete the transaction and abort if container creation fails\" in within(timeout) {\n    val container = new TestContainer\n    val factory = createFactory(Future.failed(new Exception()))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    machine ! Run(action, message)\n    expectMsg(Transition(machine, Uninitialized, Running))\n    expectMsg(ContainerRemoved(true))\n    expectMsg(Transition(machine, Running, Removing))\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls should have size 0 // gather no logs\n      container.destroyCount shouldBe 0 // no destroying possible as no container could be obtained\n      acker.calls should have size 1\n      val activation = acker.calls(0)._2\n      activation.response should be a 'whiskError\n      activation.annotations.get(WhiskActivation.initTimeAnnotation) shouldBe empty\n      store.calls should have size 1\n    }\n  }\n\n  it should \"complete the transaction and destroy the container on a failed init\" in within(timeout) {\n    val container = new TestContainer {\n      override def initialize(initializer: JsObject,\n                              timeout: FiniteDuration,\n                              concurrent: Int,\n                              entity: Option[WhiskAction] = None)(implicit transid: TransactionId): Future[Interval] = {\n        initializeCount += 1\n        Future.failed(InitializationError(initInterval, ActivationResponse.developerError(\"boom\")))\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    machine ! Run(action, message)\n    expectMsg(Transition(machine, Uninitialized, Running))\n    expectMsg(ContainerRemoved(true)) // The message is sent as soon as the container decides to destroy itself\n    expectMsg(Transition(machine, Running, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 0 // should not run the action\n      collector.calls should have size 1\n      container.destroyCount shouldBe 1\n      val activation = acker.calls(0)._2\n      activation.response shouldBe ActivationResponse.developerError(\"boom\")\n      activation.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n\n      store.calls should have size 1\n    }\n  }\n\n  it should \"complete the transaction and destroy the container on a failed run IFF failure was containerError\" in within(\n    timeout) {\n    val container = new TestContainer {\n      override def run(\n        parameters: JsValue,\n        environment: JsObject,\n        timeout: FiniteDuration,\n        concurrent: Int,\n        maxResponse: ByteSize,\n        truncation: ByteSize,\n        reschedule: Boolean = false)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n        atomicRunCount.incrementAndGet()\n        Future.successful((initInterval, ActivationResponse.developerError((\"boom\"))))\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    machine ! Run(action, message)\n    expectMsg(Transition(machine, Uninitialized, Running))\n    expectMsg(ContainerRemoved(true)) // The message is sent as soon as the container decides to destroy itself\n    expectMsg(Transition(machine, Running, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls should have size 1\n      container.destroyCount shouldBe 1\n      acker.calls(0)._2.response shouldBe ActivationResponse.developerError(\"boom\")\n      store.calls should have size 1\n    }\n  }\n\n  it should \"complete the transaction and destroy the container if log reading failed\" in {\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n\n    val partialLogs = Vector(\"this log line made it\", Messages.logFailure)\n    val collector =\n      createCollector(Future.failed(LogCollectingException(ActivationLogs(partialLogs))))\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    machine ! Run(action, message)\n    expectMsg(Transition(machine, Uninitialized, Running))\n    expectMsg(ContainerRemoved(true)) // The message is sent as soon as the container decides to destroy itself\n    expectMsg(Transition(machine, Running, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls should have size 1\n      container.destroyCount shouldBe 1\n      acker.calls should have size 1\n      acker.calls(0)._2.response shouldBe ActivationResponse.success()\n      store.calls should have size 1\n      store.calls(0)._2.logs shouldBe ActivationLogs(partialLogs)\n    }\n  }\n\n  it should \"complete the transaction and destroy the container if log reading failed terminally\" in {\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector(Future.failed(new Exception))\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    machine ! Run(action, message)\n    expectMsg(Transition(machine, Uninitialized, Running))\n    expectMsg(ContainerRemoved(true)) // The message is sent as soon as the container decides to destroy itself\n    expectMsg(Transition(machine, Running, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls should have size 1\n      container.destroyCount shouldBe 1\n      acker.calls should have size 1\n      acker.calls(0)._2.response shouldBe ActivationResponse.success()\n      store.calls should have size 1\n      store.calls(0)._2.logs shouldBe ActivationLogs(Vector(Messages.logFailure))\n    }\n  }\n\n  it should \"resend the job to the parent if resuming a container fails\" in within(timeout) {\n    val container = new TestContainer {\n      override def resume()(implicit transid: TransactionId) = {\n        resumeCount += 1\n        Future.failed(new RuntimeException())\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            createCollector(),\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    run(machine, Uninitialized) // first run an activation\n    timeout(machine) // times out Ready state so container suspends\n    expectPause(machine)\n\n    val runMessage = Run(action, message)\n    machine ! runMessage\n    expectMsg(Transition(machine, Paused, Running))\n    expectMsg(RescheduleJob)\n    expectMsg(Transition(machine, Running, Removing))\n    expectMsg(runMessage)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.runCount shouldBe 1\n      container.suspendCount shouldBe 1\n      container.resumeCount shouldBe 1\n      container.destroyCount shouldBe 1\n    }\n  }\n\n  it should \"resend the job to the parent if /run fails connection after Paused -> Running\" in within(timeout) {\n    val container = new TestContainer {\n      override def run(\n        parameters: JsValue,\n        environment: JsObject,\n        timeout: FiniteDuration,\n        concurrent: Int,\n        maxResponse: ByteSize,\n        truncation: ByteSize,\n        reschedule: Boolean = false)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n\n        if (reschedule) {\n          throw ContainerHealthError(transid, \"reconnect failed to xyz\")\n        }\n        super.run(parameters, environment, timeout, concurrent, maxResponse, truncation, reschedule)\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            createCollector(),\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    run(machine, Uninitialized) // first run an activation\n    timeout(machine) // times out Ready state so container suspends\n    expectPause(machine)\n\n    val runMessage = Run(action, message)\n    machine ! runMessage\n    expectMsg(Transition(machine, Paused, Running))\n    expectMsg(RescheduleJob)\n    expectMsg(Transition(machine, Running, Removing))\n    expectMsg(runMessage)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.runCount shouldBe 1\n      container.suspendCount shouldBe 1\n      container.resumeCount shouldBe 1\n      container.destroyCount shouldBe 1\n    }\n  }\n\n  it should \"resend the job to the parent if /run fails connection after Ready -> Running\" in within(timeout) {\n    val container = new TestContainer {\n      override def run(\n        parameters: JsValue,\n        environment: JsObject,\n        timeout: FiniteDuration,\n        concurrent: Int,\n        maxResponse: ByteSize,\n        truncation: ByteSize,\n        reschedule: Boolean = false)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n\n        if (reschedule) {\n          throw ContainerHealthError(transid, \"reconnect failed to xyz\")\n        }\n        super.run(parameters, environment, timeout, concurrent, maxResponse, truncation, reschedule)\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            createCollector(),\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    run(machine, Uninitialized) // first run an activation\n    //will be in Ready state now\n\n    val runMessage = Run(action, message)\n    machine ! runMessage\n    expectMsg(Transition(machine, Ready, Running))\n    expectMsg(RescheduleJob)\n    expectMsg(Transition(machine, Running, Removing))\n    expectMsg(runMessage)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.runCount shouldBe 1\n      container.suspendCount shouldBe 0\n      container.resumeCount shouldBe 0\n      container.destroyCount shouldBe 1\n    }\n  }\n\n  it should \"remove and replace a prewarm container if it fails healthcheck after startup\" in within(timeout) {\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(true),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    preWarm(machine)\n\n    //expect failure after healthchecks fail\n    expectMsg(ContainerRemoved(true))\n    expectMsg(Transition(machine, Started, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls should have size 0\n      container.suspendCount shouldBe 0\n      container.resumeCount shouldBe 0\n      acker.calls should have size 0\n    }\n  }\n  it should \"remove the container if suspend fails\" in within(timeout) {\n    val container = new TestContainer {\n      override def suspend()(implicit transid: TransactionId) = {\n        suspendCount += 1\n        Future.failed(new RuntimeException())\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            createCollector(),\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    run(machine, Uninitialized)\n    timeout(machine) // times out Ready state so container suspends\n    expectMsg(Transition(machine, Ready, Pausing))\n    expectMsg(ContainerRemoved(true)) // The message is sent as soon as the container decides to destroy itself\n    expectMsg(Transition(machine, Pausing, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.suspendCount shouldBe 1\n      container.destroyCount shouldBe 1\n    }\n  }\n\n  /*\n   * DELAYED DELETION CASES\n   */\n  // this test represents a Remove message whenever you are in the \"Running\" state. Therefore, testing\n  // a Remove while /init should suffice to guarantee test coverage here.\n  it should \"delay a deletion message until the transaction is completed successfully\" in within(timeout) {\n    val initPromise = Promise[Interval]\n    val container = new TestContainer {\n      override def initialize(initializer: JsObject,\n                              timeout: FiniteDuration,\n                              concurrent: Int,\n                              entity: Option[WhiskAction] = None)(implicit transid: TransactionId): Future[Interval] = {\n        initializeCount += 1\n        initPromise.future\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n\n    // Start running the action\n    machine ! Run(action, message)\n    expectMsg(Transition(machine, Uninitialized, Running))\n\n    // Schedule the container to be removed\n    machine ! Remove\n\n    // Finish /init, note that /run and log-collecting happens nonetheless\n    initPromise.success(Interval.zero)\n    expectWarmed(invocationNamespace.name, action)\n    expectMsg(Transition(machine, Running, Ready))\n\n    // Remove the container after the transaction finished\n    expectMsg(ContainerRemoved(true))\n    expectMsg(Transition(machine, Ready, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls should have size 1\n      container.suspendCount shouldBe 0 // skips pausing the container\n      container.destroyCount shouldBe 1\n      acker.calls should have size 1\n      store.calls should have size 1\n    }\n  }\n\n  // this tests a Run message in the \"Removing\" state. The contract between the pool and state-machine\n  // is, that only one Run is to be sent until a \"NeedWork\" comes back. If we sent a NeedWork but no work is\n  // there, we might run into the final timeout which will schedule a removal of the container. There is a\n  // time window though, in which the pool doesn't know of that decision yet. We handle the collision by\n  // sending the Run back to the pool so it can reschedule.\n  it should \"send back a Run message which got sent before the container decided to remove itself\" in within(timeout) {\n    val destroyPromise = Promise[Unit]\n    val container = new TestContainer {\n      override def destroy()(implicit transid: TransactionId): Future[Unit] = {\n        destroyCount += 1\n        destroyPromise.future\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n    run(machine, Uninitialized)\n    timeout(machine)\n    expectPause(machine)\n    timeout(machine)\n\n    // We don't know of this timeout, so we schedule a run.\n    machine ! Run(action, message)\n\n    // State-machine shuts down nonetheless.\n    expectMsg(RescheduleJob)\n    expectMsg(Transition(machine, Paused, Removing))\n\n    // Pool gets the message again.\n    expectMsg(Run(action, message))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls should have size 1\n      container.suspendCount shouldBe 1\n      container.resumeCount shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls should have size 1\n      store.calls should have size 1\n    }\n  }\n\n  // This tests ensures the user api key is not present in the action context if not requested\n  it should \"omit api key from action run context\" in within(timeout) {\n    val container = new TestContainer(apiKeyMustBePresent = false)\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n\n    val machine =\n      childActorOf(\n        ContainerProxy\n          .props(\n            factory,\n            acker,\n            store,\n            collector,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            poolConfig,\n            healthchecksConfig(),\n            pauseGrace = pauseGrace))\n    registerCallback(machine)\n\n    preWarm(machine)\n\n    val keyFalsyAnnotation = Parameters(Annotations.ProvideApiKeyAnnotationName, JsFalse)\n    val actionWithFalsyKeyAnnotation =\n      ExecutableWhiskAction(EntityPath(\"actionSpace\"), EntityName(\"actionName\"), exec, annotations = keyFalsyAnnotation)\n\n    machine ! Run(actionWithFalsyKeyAnnotation, message)\n    expectMsg(Transition(machine, Started, Running))\n    expectWarmed(invocationNamespace.name, actionWithFalsyKeyAnnotation)\n    expectMsg(Transition(machine, Running, Ready))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls should have size 1\n      acker.calls should have size 1\n      store.calls should have size 1\n    }\n  }\n  it should \"reset the lastUse and increment the activationCount on nextRun()\" in {\n    //NoData/MemoryData/PrewarmedData always reset activation count to 1, and reset lastUse\n    val noData = NoData()\n    noData.nextRun(Run(action, message)) should matchPattern {\n      case WarmingColdData(message.user.namespace.name, action, _, 1) =>\n    }\n\n    val memData = MemoryData(action.limits.memory.megabytes.MB)\n    memData.nextRun(Run(action, message)) should matchPattern {\n      case WarmingColdData(message.user.namespace.name, action, _, 1) =>\n    }\n    val pwData = PreWarmedData(new TestContainer(), action.exec.kind, action.limits.memory.megabytes.MB)\n    pwData.nextRun(Run(action, message)) should matchPattern {\n      case WarmingData(pwData.container, message.user.namespace.name, action, _, 1) =>\n    }\n\n    //WarmingData, WarmingColdData, and WarmedData increment counts and reset lastUse\n    val timeDiffSeconds = 20\n    val initialCount = 10\n    //WarmingData\n    val warmingData = WarmingData(\n      pwData.container,\n      message.user.namespace.name,\n      action,\n      Instant.now.minusSeconds(timeDiffSeconds),\n      initialCount)\n    val nextWarmingData = warmingData.nextRun(Run(action, message))\n    val nextCount = warmingData.activeActivationCount + 1\n    nextWarmingData should matchPattern {\n      case WarmingData(pwData.container, message.user.namespace.name, action, _, nextCount) =>\n    }\n    warmingData.lastUsed.until(nextWarmingData.lastUsed, ChronoUnit.SECONDS) should be >= timeDiffSeconds.toLong\n\n    //WarmingColdData\n    val warmingColdData =\n      WarmingColdData(message.user.namespace.name, action, Instant.now.minusSeconds(timeDiffSeconds), initialCount)\n    val nextWarmingColdData = warmingColdData.nextRun(Run(action, message))\n    nextWarmingColdData should matchPattern {\n      case WarmingColdData(message.user.namespace.name, action, _, newCount) =>\n    }\n    warmingColdData.lastUsed.until(nextWarmingColdData.lastUsed, ChronoUnit.SECONDS) should be >= timeDiffSeconds.toLong\n\n    //WarmedData\n    val warmedData = WarmedData(\n      pwData.container,\n      message.user.namespace.name,\n      action,\n      Instant.now.minusSeconds(timeDiffSeconds),\n      initialCount)\n    val nextWarmedData = warmedData.nextRun(Run(action, message))\n    nextWarmedData should matchPattern {\n      case WarmedData(pwData.container, message.user.namespace.name, action, _, newCount, _) =>\n    }\n    warmedData.lastUsed.until(nextWarmedData.lastUsed, ChronoUnit.SECONDS) should be >= timeDiffSeconds.toLong\n  }\n\n  /**\n   * Implements all the good cases of a perfect run to facilitate error case overriding.\n   */\n  class TestContainer(initPromise: Option[Promise[Interval]] = None,\n                      runPromises: Seq[Promise[(Interval, ActivationResponse)]] = Seq.empty,\n                      apiKeyMustBePresent: Boolean = true)\n      extends Container {\n    protected[core] val id = ContainerId(\"testcontainer\")\n    protected[core] val addr = ContainerAddress(\"0.0.0.0\")\n    protected implicit val logging: Logging = log\n    protected implicit val ec: ExecutionContext = system.dispatcher\n    override implicit protected val as: ActorSystem = system\n    var suspendCount = 0\n    var resumeCount = 0\n    var destroyCount = 0\n    var initializeCount = 0\n    val atomicRunCount = new AtomicInteger(0) //need atomic tracking since we will test concurrent runs\n    var atomicLogsCount = new AtomicInteger(0)\n\n    def runCount = atomicRunCount.get()\n    override def suspend()(implicit transid: TransactionId): Future[Unit] = {\n      suspendCount += 1\n      val s = super.suspend()\n      Await.result(s, 5.seconds)\n      //verify that httpconn is closed\n      httpConnection should be(None)\n      s\n    }\n    override def resume()(implicit transid: TransactionId): Future[Unit] = {\n      resumeCount += 1\n      val r = super.resume()\n      Await.result(r, 5.seconds)\n      //verify that httpconn is recreated\n      httpConnection should be('defined)\n      r\n    }\n    override def destroy()(implicit transid: TransactionId): Future[Unit] = {\n      destroyCount += 1\n      super.destroy()\n    }\n    override def initialize(initializer: JsObject,\n                            timeout: FiniteDuration,\n                            concurrent: Int,\n                            entity: Option[WhiskAction] = None)(implicit transid: TransactionId): Future[Interval] = {\n      initializeCount += 1\n\n      val envField = \"env\"\n\n      (initializer.fields - envField) shouldBe (action.containerInitializer {\n        activationArguments.fields.filter(k => filterEnvVar(k._1))\n      }.fields - envField)\n      timeout shouldBe action.limits.timeout.duration\n\n      val initializeEnv = initializer.fields(envField).asJsObject\n\n      initializeEnv.fields(\"__OW_NAMESPACE\") shouldBe invocationNamespace.name.toJson\n      initializeEnv.fields(\"__OW_ACTION_NAME\") shouldBe message.action.qualifiedNameWithLeadingSlash.toJson\n      initializeEnv.fields(\"__OW_ACTION_VERSION\") shouldBe message.action.version.toJson\n      initializeEnv.fields(\"__OW_ACTIVATION_ID\") shouldBe message.activationId.toJson\n      initializeEnv.fields(\"__OW_TRANSACTION_ID\") shouldBe transid.id.toJson\n\n      val convertedAuthKey = message.user.authkey.toEnvironment.fields.map(f => (\"__OW_\" + f._1.toUpperCase(), f._2))\n      val authEnvironment = initializeEnv.fields.filterKeys(convertedAuthKey.contains).toMap\n      if (apiKeyMustBePresent) {\n        convertedAuthKey shouldBe authEnvironment\n      } else {\n        authEnvironment shouldBe empty\n      }\n\n      val deadline = Instant.ofEpochMilli(initializeEnv.fields(\"__OW_DEADLINE\").convertTo[String].toLong)\n      val maxDeadline = Instant.now.plusMillis(timeout.toMillis)\n\n      // The deadline should be in the future but must be smaller than or equal\n      // a freshly computed deadline, as they get computed slightly after each other\n      deadline should (be <= maxDeadline and be >= Instant.now)\n\n      initPromise.map(_.future).getOrElse(Future.successful(initInterval))\n    }\n    override def run(\n      parameters: JsValue,\n      environment: JsObject,\n      timeout: FiniteDuration,\n      concurrent: Int,\n      maxResponse: ByteSize,\n      truncation: ByteSize,\n      reschedule: Boolean = false)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n\n      // the \"init\" arguments are not passed on run\n      parameters shouldBe JsObject(activationArguments.fields.filter(k => !filterEnvVar(k._1)))\n\n      val runCount = atomicRunCount.incrementAndGet()\n      environment.fields(\"namespace\") shouldBe invocationNamespace.name.toJson\n      environment.fields(\"action_name\") shouldBe message.action.qualifiedNameWithLeadingSlash.toJson\n      environment.fields(\"action_version\") shouldBe message.action.version.toJson\n      environment.fields(\"activation_id\") shouldBe message.activationId.toJson\n      environment.fields(\"transaction_id\") shouldBe transid.id.toJson\n      val authEnvironment = environment.fields.filterKeys(message.user.authkey.toEnvironment.fields.contains).toMap\n      if (apiKeyMustBePresent) {\n        message.user.authkey.toEnvironment shouldBe authEnvironment.toJson.asJsObject\n      } else {\n        authEnvironment shouldBe empty\n      }\n\n      val deadline = Instant.ofEpochMilli(environment.fields(\"deadline\").convertTo[String].toLong)\n      val maxDeadline = Instant.now.plusMillis(timeout.toMillis)\n\n      // The deadline should be in the future but must be smaller than or equal\n      // a freshly computed deadline, as they get computed slightly after each other\n      deadline should (be <= maxDeadline and be >= Instant.now)\n\n      //return the future for this run (if runPromises no empty), or a default response\n      runPromises\n        .lift(runCount - 1)\n        .map(_.future)\n        .getOrElse(Future.successful((runInterval, ActivationResponse.success())))\n    }\n    def logs(limit: ByteSize, waitForSentinel: Boolean)(implicit transid: TransactionId): Source[ByteString, Any] = {\n      atomicLogsCount.incrementAndGet()\n      Source.empty\n    }\n  }\n}\n@RunWith(classOf[JUnitRunner])\nclass TCPPingClientTests extends TestKit(ActorSystem(\"TCPPingClient\")) with Matchers with AnyFlatSpecLike {\n  val config = ContainerProxyHealthCheckConfig(true, 200.milliseconds, 2)\n  val addr = new InetSocketAddress(\"1.2.3.4\", 12345)\n  val localAddr = new InetSocketAddress(\"localhost\", 5432)\n\n  behavior of \"TCPPingClient\"\n  it should \"start the ping on HealthPingEnabled(true) and stop on HealthPingEnabled(false)\" in {\n    val tcpProbe = TestProbe()\n    val pingClient = system.actorOf(TCPPingClient.props(tcpProbe.ref, \"1234\", config, addr))\n    pingClient ! HealthPingEnabled(true)\n    tcpProbe.expectMsg(Connect(addr))\n    //measure the delay between connections\n    val start = System.currentTimeMillis()\n    tcpProbe.expectMsg(Connect(addr))\n    val delay = System.currentTimeMillis() - start\n    delay should be > config.checkPeriod.toMillis - 25 //allow 25ms slop\n    tcpProbe.expectMsg(Connect(addr))\n    //make sure disable works\n    pingClient ! HealthPingEnabled(false)\n    //make sure no Connect msg for at least the check period\n    tcpProbe.expectNoMessage(config.checkPeriod)\n  }\n  it should \"send FailureMessage and cancel the ping on CommandFailed\" in {\n    val tcpProbe = TestProbe()\n    val pingClient = system.actorOf(TCPPingClient.props(tcpProbe.ref, \"1234\", config, addr))\n    val clientProbe = TestProbe()\n    clientProbe watch pingClient\n    pingClient ! HealthPingEnabled(true)\n    val c = Connect(addr)\n    //send config.maxFails CommandFailed messages\n    (1 to config.maxFails).foreach { _ =>\n      tcpProbe.expectMsg(c)\n      pingClient ! CommandFailed(c)\n    }\n    //now we expect termination\n    clientProbe.expectTerminated(pingClient)\n  }\n  it should \"reset failedCount on Connected\" in {\n    val tcpProbe = TestProbe()\n    val pingClient = system.actorOf(TCPPingClient.props(tcpProbe.ref, \"1234\", config, addr))\n    val clientProbe = TestProbe()\n    clientProbe watch pingClient\n    pingClient ! HealthPingEnabled(true)\n    val c = Connect(addr)\n    //send maxFails-1 (should not fail)\n    (1 to config.maxFails - 1).foreach { _ =>\n      tcpProbe.expectMsg(c)\n      pingClient ! CommandFailed(c)\n    }\n    tcpProbe.expectMsg(c)\n    tcpProbe.send(pingClient, Connected(addr, localAddr))\n    //counter should be reset\n    tcpProbe.expectMsg(Close)\n    //send maxFails (will fail, but counter is reset so we get maxFails tries)\n    (1 to config.maxFails).foreach { _ =>\n      tcpProbe.expectMsg(c)\n      pingClient ! CommandFailed(c)\n    }\n    //now we expect termination\n    clientProbe.expectTerminated(pingClient)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/v2/test/ActivationClientProxyTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.v2.test\n\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}\nimport org.apache.pekko.actor.Status.Failure\nimport org.apache.pekko.actor.{ActorRef, ActorSystem}\nimport org.apache.pekko.grpc.internal.ClientClosedException\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit, TestProbe}\nimport common.StreamLogging\nimport io.grpc.StatusRuntimeException\nimport org.apache.openwhisk.common.{GracefulShutdown, TransactionId}\nimport org.apache.openwhisk.core.connector.ActivationMessage\nimport org.apache.openwhisk.core.containerpool.ContainerId\nimport org.apache.openwhisk.core.containerpool.v2._\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.scheduler.SchedulerEndpoints\nimport org.apache.openwhisk.core.scheduler.grpc.{ActivationResponse => AResponse}\nimport org.apache.openwhisk.core.scheduler.queue.{ActionMismatch, NoActivationMessage, NoMemoryQueue}\nimport org.apache.openwhisk.grpc\nimport org.apache.openwhisk.grpc.{ActivationServiceClient, FetchRequest, RescheduleRequest, RescheduleResponse}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.collection.mutable.ArrayBuffer\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass ActivationClientProxyTests\n    extends TestKit(ActorSystem(\"ActivationClientProxy\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with StreamLogging\n    with ScalaFutures {\n\n  override def afterAll: Unit = TestKit.shutdownActorSystem(system)\n\n  implicit val ec = system.dispatcher\n\n  val timeout = 20.seconds\n\n  val log = logging\n\n  val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n  val action = ExecutableWhiskAction(EntityPath(\"actionSpace\"), EntityName(\"actionName\"), exec)\n  val fqn = action.fullyQualifiedName(true)\n  val rev = action.rev\n  val schedulerHost = \"127.17.0.1\"\n  val rpcPort = 13001\n  val containerId = ContainerId(\"fakeContainerId\")\n  val messageTransId = TransactionId(TransactionId.testing.meta.id)\n  val invocationNamespace = EntityName(\"invocationSpace\")\n  val uuid = UUID()\n\n  val message = ActivationMessage(\n    messageTransId,\n    action.fullyQualifiedName(true),\n    action.rev,\n    Identity(Subject(), Namespace(invocationNamespace, uuid), BasicAuthenticationAuthKey(uuid, Secret()), Set.empty),\n    ActivationId.generate(),\n    ControllerInstanceId(\"0\"),\n    blocking = false,\n    content = None)\n\n  val entityStore = WhiskEntityStore.datastore()\n\n  behavior of \"ActivationClientProxy\"\n\n  it should \"create a grpc client successfully\" in within(timeout) {\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Right(message)).serialize))\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) =>\n      Future(MockActivationServiceClient(fetch))\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n\n    machine ! StartClient\n\n    probe.expectMsg(ClientCreationCompleted)\n    probe.expectMsg(Transition(machine, ClientProxyUninitialized, ClientProxyReady))\n  }\n\n  it should \"be closed when failed to create grpc client\" in within(timeout) {\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Right(message)).serialize))\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) =>\n      Future {\n        throw new RuntimeException(\"failed to create client\")\n        MockActivationServiceClient(fetch)\n    }\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n\n    machine ! StartClient\n\n    probe.expectMsgPF() {\n      case Failure(t) => t.getMessage shouldBe \"The number of client creation retries has been exceeded.\"\n    }\n    probe.expectMsg(Transition(machine, ClientProxyUninitialized, ClientProxyRemoving))\n    probe.expectMsg(ClientClosed)\n\n    probe expectTerminated machine\n  }\n\n  it should \"fetch activation message successfully\" in within(timeout) {\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Right(message)).serialize))\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) =>\n      Future(MockActivationServiceClient(fetch))\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! RequestActivation()\n    probe.expectMsg(message)\n  }\n\n  it should \"be recreated when scheduler is changed\" in within(timeout) {\n    var creationCount = 0\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Left(NoMemoryQueue())).serialize))\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) => {\n      creationCount += 1\n      Future(MockActivationServiceClient(fetch))\n    }\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    // new scheduler is reached\n    machine ! RequestActivation(newScheduler = Some(SchedulerEndpoints(\"0.0.0.0\", 10, 11)))\n\n    awaitAssert {\n      creationCount should be > 1\n    }\n  }\n\n  it should \"be recreated when the queue does not exist\" in within(timeout) {\n    var creationCount = 0\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Left(NoMemoryQueue())).serialize))\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) => {\n      creationCount += 1\n      Future(MockActivationServiceClient(fetch))\n    }\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! RequestActivation()\n\n    awaitAssert {\n      creationCount should be > 1\n    }\n  }\n\n  it should \"be closed when the action version does not match\" in within(timeout) {\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Left(ActionMismatch())).serialize))\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) =>\n      Future(MockActivationServiceClient(fetch))\n\n    val parentProbe = TestProbe()\n    val selfProbe = TestProbe()\n    val machine =\n      parentProbe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n\n    // set up watch of client proxy fsm\n    machine ! SubscribeTransitionCallBack(selfProbe.ref)\n    selfProbe.expectMsg(CurrentState(machine, ClientProxyUninitialized))\n    selfProbe watch machine\n\n    // wait for client proxy to be ready\n    machine ! StartClient\n    parentProbe.expectMsg(ClientCreationCompleted)\n    selfProbe.expectMsg(Transition(machine, ClientProxyUninitialized, ClientProxyReady))\n\n    machine ! RequestActivation()\n\n    // next two events can happen in any order\n    selfProbe.expectMsg(Transition(machine, ClientProxyReady, ClientProxyRemoving))\n    parentProbe.expectMsgPF() {\n      case Failure(t) => t.getMessage.contains(s\"action version does not match\") shouldBe true\n    }\n\n    parentProbe.expectMsg(ClientClosed)\n\n    selfProbe expectTerminated machine\n  }\n\n  it should \"retry to request activation message when scheduler response no activation message\" in within(timeout) {\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Left(NoActivationMessage())).serialize))\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) =>\n      Future(MockActivationServiceClient(fetch))\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! RequestActivation()\n    probe.expectMsg(RetryRequestActivation)\n  }\n\n  it should \"create activation client on other scheduler when the queue does not exist\" in within(timeout) {\n    val createClientOnOtherScheduler = new ArrayBuffer[Boolean]()\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Left(NoMemoryQueue())).serialize))\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, tryOtherScheduler: Boolean) => {\n      createClientOnOtherScheduler += tryOtherScheduler\n      Future(MockActivationServiceClient(fetch))\n    }\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! RequestActivation()\n\n    awaitAssert {\n      // Create activation client using original scheduler endpoint firstly\n      createClientOnOtherScheduler(0) shouldBe false\n      // Create activation client using latest scheduler endpoint(try other scheduler) when no memoryQueue\n      createClientOnOtherScheduler(1) shouldBe true\n    }\n  }\n\n  it should \"request activation message when the message can't deserialize\" in within(timeout) {\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(\"aaaaaa\"))\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) =>\n      Future(MockActivationServiceClient(fetch))\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! RequestActivation()\n    probe.expectMsg(RetryRequestActivation)\n  }\n\n  it should \"be recreated when pekko grpc server connection failed\" in within(timeout) {\n    var creationCount = 0\n    val fetch = (_: FetchRequest) =>\n      Future {\n        throw new StatusRuntimeException(io.grpc.Status.UNAVAILABLE)\n        grpc.FetchResponse(AResponse(Right(message)).serialize)\n    }\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) => {\n      creationCount += 1\n      Future(MockActivationServiceClient(fetch))\n    }\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! RequestActivation()\n\n    awaitAssert {\n      creationCount should be > 1\n    }\n  }\n\n  it should \"be closed when grpc client is already closed\" in within(timeout) {\n    val fetch = (_: FetchRequest) =>\n      Future {\n        throw new ClientClosedException()\n        grpc.FetchResponse(AResponse(Right(message)).serialize)\n    }\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) =>\n      Future(MockActivationServiceClient(fetch))\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! RequestActivation()\n    probe.expectMsgPF() {\n      case Failure(t) => t.isInstanceOf[ClientClosedException] shouldBe true\n    }\n    probe.expectMsg(Transition(machine, ClientProxyReady, ClientProxyRemoving))\n\n    probe.expectMsg(ClientClosed)\n\n    probe expectTerminated machine\n  }\n\n  it should \"be closed when it failed to getting activation from scheduler\" in within(timeout) {\n    val fetch = (_: FetchRequest) =>\n      Future {\n        throw new Exception(\"Unknown exception\")\n        grpc.FetchResponse(AResponse(Right(message)).serialize)\n    }\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) =>\n      Future(MockActivationServiceClient(fetch))\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! RequestActivation()\n    probe.expectMsgPF() {\n      case Failure(t) => t.getMessage.contains(\"Unknown exception\") shouldBe true\n    }\n    probe.expectMsg(Transition(machine, ClientProxyReady, ClientProxyRemoving))\n    probe.expectMsg(ClientClosed)\n\n    probe expectTerminated machine\n  }\n\n  it should \"be closed when it receives a GracefulShutdown message for a normal timeout case\" in within(timeout) {\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Right(message)).serialize))\n    val activationClient = MockActivationServiceClient(fetch)\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) => Future(activationClient)\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! GracefulShutdown\n\n    probe.expectMsg(Transition(machine, ClientProxyReady, ClientProxyRemoving))\n\n    machine ! RequestActivation()\n\n    probe expectMsg ClientClosed\n    awaitAssert(activationClient.isClosed shouldBe true)\n    probe expectTerminated machine\n  }\n\n  it should \"be closed when it receives a StopClientProxy message for the case of graceful shutdown\" in within(timeout) {\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Right(message)).serialize))\n    val activationClient = MockActivationServiceClient(fetch)\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) => Future(activationClient)\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    machine ! StopClientProxy\n    awaitAssert(activationClient.isClosed shouldBe true)\n\n    probe expectMsg ClientClosed\n    probe expectTerminated machine\n  }\n\n  it should \"be safely closed when the client is already closed\" in within(timeout) {\n    val fetch = (_: FetchRequest) => Future(grpc.FetchResponse(AResponse(Right(message)).serialize))\n    val activationClient = MockActivationServiceClient(fetch)\n    val client = (_: String, _: FullyQualifiedEntityName, _: String, _: Int, _: Boolean) => Future(activationClient)\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        ActivationClientProxy\n          .props(invocationNamespace.asString, fqn, rev, schedulerHost, rpcPort, containerId, client))\n    registerCallback(machine, probe)\n    ready(machine, probe)\n\n    // close client\n    activationClient.close().futureValue\n    awaitAssert(activationClient.isClosed shouldBe true)\n\n    // close client again\n    machine ! StopClientProxy\n\n    probe expectMsg ClientClosed\n    probe expectTerminated machine\n  }\n\n  /** Registers the transition callback and expects the first message */\n  def registerCallback(c: ActorRef, probe: TestProbe) = {\n    c ! SubscribeTransitionCallBack(probe.ref)\n    probe.expectMsg(CurrentState(c, ClientProxyUninitialized))\n    probe watch c\n  }\n\n  def ready(machine: ActorRef, probe: TestProbe) = {\n    machine ! StartClient\n    probe.expectMsg(ClientCreationCompleted)\n    probe.expectMsg(Transition(machine, ClientProxyUninitialized, ClientProxyReady))\n  }\n\n  case class MockActivationServiceClient(customFetchActivation: FetchRequest => Future[grpc.FetchResponse])\n      extends ActivationServiceClient {\n\n    var isClosed = false\n\n    override def close(): Future[Done] = {\n      isClosed = true\n      Future.successful(Done)\n    }\n\n    override def closed(): Future[Done] = close()\n\n    override def rescheduleActivation(in: RescheduleRequest): Future[RescheduleResponse] = {\n      Future.successful(RescheduleResponse())\n    }\n\n    override def fetchActivation(in: FetchRequest): Future[grpc.FetchResponse] = {\n      if (!isClosed) {\n        customFetchActivation(in)\n      } else {\n        throw new ClientClosedException()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/v2/test/FunctionPullingContainerPoolTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.v2.test\n\nimport java.time.Instant\nimport java.util.concurrent.TimeUnit\nimport org.apache.pekko.actor.{ActorRef, ActorRefFactory, ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestActor, TestActorRef, TestKit, TestProbe}\nimport common.StreamLogging\nimport org.apache.openwhisk.common.{Enable, GracefulShutdown, TransactionId}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.ContainerCreationError._\nimport org.apache.openwhisk.core.connector.test.TestConnector\nimport org.apache.openwhisk.core.connector.{\n  ContainerCreationAckMessage,\n  ContainerCreationError,\n  ContainerCreationMessage,\n  MessageProducer,\n  ResultMetadata\n}\nimport org.apache.openwhisk.core.containerpool.v2._\nimport org.apache.openwhisk.core.containerpool.{\n  Container,\n  ContainerAddress,\n  ContainerId,\n  ContainerPoolConfig,\n  ContainerRemoved,\n  PrewarmContainerCreationConfig,\n  PrewarmingConfig\n}\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, ReactivePrewarmingConfig, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.utils.{retry => utilRetry}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.Eventually\n\nimport scala.collection.mutable\nimport scala.collection.mutable.ListBuffer\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.language.postfixOps\n\n/**\n * Behavior tests for the ContainerPool\n *\n * These tests test the runtime behavior of a ContainerPool actor.\n */\n@RunWith(classOf[JUnitRunner])\nclass FunctionPullingContainerPoolTests\n    extends TestKit(ActorSystem(\"FunctionPullingContainerPool\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with MockFactory\n    with StreamLogging\n    with Eventually\n    with DbUtils {\n\n  override def afterAll = {\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n  override def afterEach = {\n    cleanup()\n    super.afterEach()\n  }\n\n  private val config = new WhiskConfig(ExecManifest.requiredProperties)\n  ExecManifest.initialize(config) should be a 'success\n\n  val timeout = 5.seconds\n\n  private implicit val transId = TransactionId.testing\n  private implicit val creationId = CreationId.generate()\n\n  // Common entities to pass to the tests. We don't really care what's inside\n  // those for the behavior testing here, as none of the contents will really\n  // reach a container anyway. We merely assert that passing and extraction of\n  // the values is done properly.\n  private val actionKind = \"nodejs:20\"\n  private val exec = CodeExecAsString(RuntimeManifest(actionKind, ImageName(\"testImage\")), \"testCode\", None)\n  private val memoryLimit = MemoryLimit.STD_MEMORY.toMB.MB\n  private val whiskAction = WhiskAction(EntityPath(\"actionSpace\"), EntityName(\"actionName\"), exec)\n  private val invocationNamespace = EntityName(\"invocationSpace\")\n  private val schedulerHost = \"127.17.0.1\"\n  private val rpcPort = 13001\n  private val isBlockboxInvocation = false\n  private val bigWhiskAction = WhiskAction(\n    EntityPath(\"actionSpace\"),\n    EntityName(\"bigActionName\"),\n    exec,\n    limits = ActionLimits(memory = MemoryLimit(memoryLimit * 2)))\n  private val execMetadata = CodeExecMetaDataAsString(exec.manifest, entryPoint = exec.entryPoint)\n  private val actionMetaData =\n    WhiskActionMetaData(\n      whiskAction.namespace,\n      whiskAction.name,\n      execMetadata,\n      whiskAction.parameters,\n      whiskAction.limits,\n      whiskAction.version,\n      whiskAction.publish,\n      whiskAction.annotations)\n  private val bigActionMetaData =\n    WhiskActionMetaData(\n      bigWhiskAction.namespace,\n      bigWhiskAction.name,\n      execMetadata,\n      bigWhiskAction.parameters,\n      bigWhiskAction.limits,\n      bigWhiskAction.version,\n      bigWhiskAction.publish,\n      bigWhiskAction.annotations)\n  private val invokerHealthService = TestProbe()\n  private val schedulerInstanceId = SchedulerInstanceId(\"0\")\n  private val producer = stub[MessageProducer]\n  private val prewarmedData = PreWarmData(mock[MockableV2Container], actionKind, memoryLimit)\n  private val mockContainer = mock[MockableV2Container]\n  (mockContainer.containerId _: () => ContainerId)\n    .expects()\n    .returning(ContainerId(\"test-container-id\"))\n    .anyNumberOfTimes()\n\n  private val initializedData =\n    InitializedData(\n      mockContainer,\n      invocationNamespace.asString,\n      whiskAction.toExecutableWhiskAction.get,\n      TestProbe().ref)\n\n  private val entityStore = WhiskEntityStore.datastore()\n  private val invokerInstance = InvokerInstanceId(0, userMemory = 0 B)\n  private val creationMessage =\n    ContainerCreationMessage(\n      transId,\n      invocationNamespace.asString,\n      whiskAction.fullyQualifiedName(true),\n      DocRevision.empty,\n      actionMetaData,\n      schedulerInstanceId,\n      schedulerHost,\n      rpcPort,\n      creationId = creationId)\n  private val creationMessageLarge =\n    ContainerCreationMessage(\n      transId,\n      invocationNamespace.asString,\n      bigWhiskAction.fullyQualifiedName(true),\n      DocRevision.empty,\n      bigActionMetaData,\n      schedulerInstanceId,\n      schedulerHost,\n      rpcPort,\n      creationId = creationId)\n\n  /** Creates a sequence of containers and a factory returning this sequence. */\n  def testContainers(n: Int) = {\n    val containers = (0 to n).map(_ => TestProbe())\n    val queue = mutable.Queue(containers: _*)\n    val factory = (fac: ActorRefFactory) => queue.dequeue().ref\n    (containers, factory)\n  }\n\n  def poolConfig(userMemory: ByteSize,\n                 memorySyncInterval: FiniteDuration = FiniteDuration(1, TimeUnit.SECONDS),\n                 prewarmMaxRetryLimit: Int = 3,\n                 prewarmPromotion: Boolean = false,\n                 batchDeletionSize: Int = 10,\n                 prewarmContainerCreationConfig: Option[PrewarmContainerCreationConfig] = None) =\n    ContainerPoolConfig(\n      userMemory,\n      0.5,\n      false,\n      FiniteDuration(2, TimeUnit.SECONDS),\n      FiniteDuration(1, TimeUnit.MINUTES),\n      None,\n      100,\n      prewarmMaxRetryLimit,\n      prewarmPromotion,\n      memorySyncInterval,\n      batchDeletionSize,\n      Some(2),\n      prewarmContainerCreationConfig)\n\n  def sendAckToScheduler(producer: MessageProducer)(schedulerInstanceId: SchedulerInstanceId,\n                                                    ackMessage: ContainerCreationAckMessage): Future[ResultMetadata] = {\n    val topic = s\"creationAck${schedulerInstanceId.asString}\"\n    producer.send(topic, ackMessage)\n  }\n\n  behavior of \"ContainerPool\"\n\n  /*\n   * CONTAINER SCHEDULING\n   *\n   * These tests only test the simplest approaches. Look below for full coverage tests\n   * of the respective scheduling methods.\n   */\n\n  it should \"create a container if it cannot find a matching prewarmed container\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val doc = put(entityStore, whiskAction)\n    // Actions are created with default memory limit (MemoryLimit.stdMemory). This means 4 actions can be scheduled.\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig(MemoryLimit.STD_MEMORY * 4),\n          invokerInstance,\n          List.empty,\n          sendAckToScheduler(producer))))\n\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(1).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n  }\n\n  it should \"not start a new container if there is not enough space in the pool\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val doc = put(entityStore, whiskAction)\n    val bigDoc = put(entityStore, bigWhiskAction)\n    // use a fake producer here so sendAckToScheduler won't failed\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig(MemoryLimit.STD_MEMORY * 2),\n          invokerInstance,\n          List.empty,\n          sendAckToScheduler(producer))))\n\n    // Start first action\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction) // 1 * stdMemory taken\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    // Send second action to the pool\n    pool ! CreationContainer(creationMessageLarge.copy(revision = bigDoc.rev), bigWhiskAction) // message is too large to be processed immediately.\n    containers(1).expectNoMessage(100.milliseconds)\n\n    // First container is removed\n    containers(0).send(pool, ContainerRemoved(true)) // pool is empty again.\n\n    pool ! CreationContainer(creationMessageLarge.copy(revision = bigDoc.rev), bigWhiskAction)\n    // Second container should run now\n    containers(1).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, bigExecuteAction, schedulerHost, rpcPort, _) => true\n    }\n  }\n\n  it should \"not start a new container if it is shut down\" in within(timeout) {\n    val (containers, factory) = testContainers(1)\n    val doc = put(entityStore, bigWhiskAction)\n    val topic = s\"creationAck${schedulerInstanceId.asString}\"\n    val consumer = new TestConnector(topic, 4, true)\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig(MemoryLimit.STD_MEMORY),\n          invokerInstance,\n          List.empty,\n          sendAckToScheduler(consumer.getProducer()))))\n\n    pool ! GracefulShutdown\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction) // 1 * stdMemory taken\n\n    containers(0).expectNoMessage()\n\n    val error =\n      s\"creationId: ${creationMessage.creationId}, invoker is shutting down, reschedule ${creationMessage.action.copy(version = None)}\"\n    val ackMessage =\n      createAckMsg(creationMessage.copy(revision = doc.rev), Some(ShuttingDownError), Some(error))\n\n    utilRetry({\n      val buffer = consumer.peek(50.millisecond)\n      buffer.size shouldBe 1\n      buffer.head._1 shouldBe topic\n      buffer.head._4 shouldBe ackMessage.serialize.getBytes\n    }, 10, Some(500.millisecond))\n\n    // pool should be back to work after enabled again\n    pool ! Enable\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction) // 1 * stdMemory taken\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n  }\n\n  private def retry[T](fn: => T) = org.apache.openwhisk.utils.retry(fn, 10, Some(1.second))\n\n  it should \"stop containers gradually when shut down\" in within(timeout * 20) {\n    (mockContainer.containerId _: () => ContainerId)\n      .expects()\n      .returning(ContainerId(\"test-container-id\"))\n      .anyNumberOfTimes()\n\n    val (containers, factory) = testContainers(10)\n    val disablingContainers = ListBuffer[ActorRef]()\n\n    for (container <- containers) {\n      container.setAutoPilot((_: ActorRef, msg: Any) =>\n        msg match {\n          case GracefulShutdown =>\n            disablingContainers += container.ref\n            TestActor.KeepRunning\n\n          case _ =>\n            TestActor.KeepRunning\n      })\n    }\n\n    val doc = put(entityStore, bigWhiskAction)\n    val topic = s\"creationAck${schedulerInstanceId.asString}\"\n    val consumer = new TestConnector(topic, 4, true)\n    val pool = TestActorRef(\n      new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService.ref,\n        poolConfig(MemoryLimit.STD_MEMORY * 20, batchDeletionSize = 3),\n        invokerInstance,\n        List.empty,\n        sendAckToScheduler(consumer.getProducer())))\n\n    (0 to 10).foreach(_ => pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)) // 11 * stdMemory taken)\n\n    (0 to 10).foreach(i => {\n      containers(i).expectMsgPF() {\n        case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n      }\n      // create 5 container in busy pool, and 6 in warmed pool\n      if (i < 5)\n        containers(i).send(pool, Initialized(initializedData)) // container is initialized\n      else\n        containers(i).send(\n          pool,\n          ContainerIsPaused(\n            WarmData(\n              mockContainer,\n              invocationNamespace.asString,\n              whiskAction.toExecutableWhiskAction.get,\n              doc.rev,\n              Instant.now,\n              TestProbe().ref)))\n    })\n\n    retry {\n      pool.underlyingActor.warmedPool.size shouldBe 6\n      pool.underlyingActor.busyPool.size shouldBe 5\n    }\n\n    // disable\n    pool ! GracefulShutdown\n\n    // at first, 3 containers will be removed from busy pool, and left containers will not\n    retry {\n      disablingContainers.size shouldBe 3\n    }\n\n    // all 3 containers finish termination\n    disablingContainers.foreach(pool.tell(ContainerRemoved(false), _))\n\n    retry {\n      pool.underlyingActor.warmedPool.size + pool.underlyingActor.busyPool.size shouldBe 8\n    }\n\n    // it will disable 3 more containers.\n    retry {\n      disablingContainers.size shouldBe 6\n    }\n\n    // only one container of them finishes termination\n    pool.tell(ContainerRemoved(false), disablingContainers.last)\n\n    // there should be only one more container going to shut down as more than 3 containers are shutting down.\n    retry {\n      disablingContainers.size shouldBe 7\n    }\n\n    // all 3 containers finish termination\n    disablingContainers.foreach(pool.tell(ContainerRemoved(false), _))\n\n    retry {\n      disablingContainers.size shouldBe 10\n    }\n\n    // all disabling containers finish termination\n    disablingContainers.foreach(pool.tell(ContainerRemoved(false), _))\n\n    // the last container is shutting down.\n    retry {\n      disablingContainers.size shouldBe 11\n    }\n    disablingContainers.foreach(pool.tell(ContainerRemoved(false), _))\n  }\n\n  it should \"create prewarmed containers on startup\" in within(timeout) {\n    stream.reset()\n    val (containers, factory) = testContainers(1)\n\n    val pool = system.actorOf(\n      Props(new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService.ref,\n        poolConfig(MemoryLimit.STD_MEMORY * 2),\n        invokerInstance,\n        List(PrewarmingConfig(1, exec, memoryLimit)),\n        sendAckToScheduler(producer))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    stream.toString should include(\"initing 1 pre-warms to desired count: 1\")\n    stream.toString should not include (\"prewarm container creation is starting with creation delay configuration\")\n  }\n\n  it should \"create prewarmed containers on startup with creation delay configuration\" in within(7.seconds) {\n    stream.reset()\n    val (containers, factory) = testContainers(3)\n    val prewarmContainerCreationConfig: Option[PrewarmContainerCreationConfig] =\n      Some(PrewarmContainerCreationConfig(1, 3.seconds))\n\n    val poolConfig = ContainerPoolConfig(\n      MemoryLimit.STD_MEMORY * 3,\n      0.5,\n      false,\n      FiniteDuration(10, TimeUnit.SECONDS),\n      FiniteDuration(10, TimeUnit.SECONDS),\n      None,\n      100,\n      3,\n      false,\n      FiniteDuration(10, TimeUnit.SECONDS),\n      10,\n      Some(2),\n      prewarmContainerCreationConfig)\n\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig,\n          invokerInstance,\n          List(PrewarmingConfig(3, exec, memoryLimit)),\n          sendAckToScheduler(producer))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(1).expectNoMessage(2.seconds)\n    containers(1).expectMsg(4.seconds, Start(exec, memoryLimit))\n    containers(2).expectNoMessage(2.seconds)\n    containers(2).expectMsg(4.seconds, Start(exec, memoryLimit))\n    stream.toString should include(\"prewarm container creation is starting with creation delay configuration\")\n  }\n\n  it should \"backfill prewarms when prewarm containers are removed\" in within(timeout) {\n    val (containers, factory) = testContainers(6)\n\n    val pool = system.actorOf(\n      Props(new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService.ref,\n        poolConfig(MemoryLimit.STD_MEMORY * 2),\n        invokerInstance,\n        List(PrewarmingConfig(2, exec, memoryLimit)),\n        sendAckToScheduler(producer))))\n\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(1).expectMsg(Start(exec, memoryLimit))\n\n    //removing 2 prewarm containers will start 2 containers via backfill\n    containers(0).send(pool, ContainerRemoved(true))\n    containers(1).send(pool, ContainerRemoved(true))\n    containers(2).expectMsg(Start(exec, memoryLimit))\n    containers(3).expectMsg(Start(exec, memoryLimit))\n    //make sure extra prewarms are not started\n    containers(4).expectNoMessage(100.milliseconds)\n    containers(5).expectNoMessage(100.milliseconds)\n  }\n\n  it should \"use a prewarmed container when kind and memory are both match and create a new one to fill its place when prewarmPromotion is false\" in within(\n    timeout) {\n    val (containers, factory) = testContainers(4)\n    val doc = put(entityStore, whiskAction)\n    val biggerMemory = memoryLimit * 2\n\n    val pool = system.actorOf(\n      Props(new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService.ref,\n        poolConfig(MemoryLimit.STD_MEMORY * 6, prewarmPromotion = false),\n        invokerInstance,\n        List(PrewarmingConfig(1, exec, memoryLimit), PrewarmingConfig(1, exec, biggerMemory)),\n        sendAckToScheduler(producer))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(1).expectMsg(Start(exec, biggerMemory))\n\n    // prewarm container is started\n    containers(0).send(pool, ReadyToWork(prewarmedData))\n    containers(1).send(pool, ReadyToWork(prewarmedData.copy(memoryLimit = biggerMemory)))\n\n    // the prewarm container with matched memory should be chose\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    // prewarm a new container\n    containers(2).expectMsgPF() {\n      case Start(exec, memoryLimit, _) => true\n    }\n\n    // the prewarm container with bigger memory should not be chose\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(3).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n  }\n\n  it should \"use a prewarmed container when kind is matched and create a new one to fill its place when prewarmPromotion is true\" in within(\n    timeout) {\n    val (containers, factory) = testContainers(6)\n    val doc = put(entityStore, whiskAction)\n    val biggerMemory = memoryLimit * 2\n    val biggestMemory = memoryLimit * 3\n\n    val pool = system.actorOf(\n      Props(new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService.ref,\n        poolConfig(MemoryLimit.STD_MEMORY * 6, prewarmPromotion = true),\n        invokerInstance,\n        List(PrewarmingConfig(1, exec, memoryLimit), PrewarmingConfig(1, exec, biggestMemory)),\n        sendAckToScheduler(producer))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(1).expectMsg(Start(exec, biggestMemory))\n\n    // two prewarm containers are started\n    containers(0).send(pool, ReadyToWork(prewarmedData))\n    containers(1).send(pool, ReadyToWork(prewarmedData.copy(memoryLimit = biggestMemory)))\n\n    // the prewarm container with smallest memory should be chose\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    // prewarm a new container\n    containers(2).expectMsgPF() {\n      case Start(exec, memoryLimit, _) => true\n    }\n\n    // the prewarm container with bigger memory should be chose\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(1).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    // prewarm a new container\n    containers(3).expectMsgPF() {\n      case Start(exec, biggestMemory, _) => true\n    }\n\n    // now free memory is (6 - 3 - 1) * stdMemory, and required 2 * stdMemory, so both two prewarmed containers are not suitable\n    // a new container should be created\n    pool ! CreationContainer(creationMessageLarge.copy(revision = doc.rev), bigWhiskAction)\n    containers(4).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    // no new prewarmed container should be created\n    containers(6).expectNoMessage(500.milliseconds)\n  }\n\n  it should \"not use a prewarmed container if it doesn't fit the kind\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val doc = put(entityStore, whiskAction)\n\n    val alternativeExec = CodeExecAsString(RuntimeManifest(\"anotherKind\", ImageName(\"testImage\")), \"testCode\", None)\n\n    val pool = system.actorOf(\n      Props(new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService.ref,\n        poolConfig(MemoryLimit.STD_MEMORY),\n        invokerInstance,\n        List(PrewarmingConfig(1, alternativeExec, memoryLimit)),\n        sendAckToScheduler(producer))))\n\n    containers(0).expectMsg(Start(alternativeExec, memoryLimit)) // container0 was prewarmed\n    containers(0).send(pool, ReadyToWork(prewarmedData.copy(kind = alternativeExec.kind))) // container0 was started\n\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(1).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n  }\n\n  it should \"not use a prewarmed container if it doesn't fit memory wise\" in within(timeout) {\n    val (containers, factory) = testContainers(2)\n    val doc = put(entityStore, whiskAction)\n\n    val alternativeLimit = 128.MB\n\n    val pool = system.actorOf(\n      Props(new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService.ref,\n        poolConfig(MemoryLimit.STD_MEMORY),\n        invokerInstance,\n        List(PrewarmingConfig(1, exec, alternativeLimit)),\n        sendAckToScheduler(producer))))\n\n    containers(0).expectMsg(Start(exec, alternativeLimit)) // container0 was prewarmed\n    containers(0).send(pool, ReadyToWork(prewarmedData.copy(memoryLimit = alternativeLimit))) // container0 was started\n\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(1).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n  }\n\n  it should \"use a warmed container when invocationNamespace, action and revision matched\" in within(timeout) {\n    (mockContainer.containerId _: () => ContainerId)\n      .expects()\n      .returning(ContainerId(\"test-container-id\"))\n      .anyNumberOfTimes()\n    val (containers, factory) = testContainers(3)\n    val doc = put(entityStore, whiskAction)\n\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig(MemoryLimit.STD_MEMORY * 4),\n          invokerInstance,\n          List.empty,\n          sendAckToScheduler(producer))))\n\n    // register a fake warmed container\n    val container = TestProbe()\n    pool.tell(\n      ContainerIsPaused(\n        WarmData(\n          mockContainer,\n          invocationNamespace.asString,\n          whiskAction.toExecutableWhiskAction.get,\n          doc.rev,\n          Instant.now,\n          TestProbe().ref)),\n      container.ref)\n\n    // the revision doesn't match, create 1 container\n    pool ! CreationContainer(creationMessage, whiskAction)\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    // the invocation namespace doesn't match, create 1 container\n    pool ! CreationContainer(creationMessage.copy(invocationNamespace = \"otherNamespace\"), whiskAction)\n    containers(1).expectMsgPF() {\n      case Initialize(\"otherNamespace\", fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    container.expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    // warmed container is occupied, create 1 more container\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(2).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n  }\n\n  it should \"retry when chosen warmed container is failed to resume\" in within(timeout) {\n    (mockContainer.containerId _: () => ContainerId)\n      .expects()\n      .returning(ContainerId(\"test-container-id\"))\n      .anyNumberOfTimes()\n\n    val (containers, factory) = testContainers(2)\n    val doc = put(entityStore, whiskAction)\n\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig(MemoryLimit.STD_MEMORY * 2),\n          invokerInstance,\n          List.empty,\n          sendAckToScheduler(producer))))\n\n    // register a fake warmed container\n    val container = TestProbe()\n    pool.tell(\n      ContainerIsPaused(\n        WarmData(\n          mockContainer,\n          invocationNamespace.asString,\n          whiskAction.toExecutableWhiskAction.get,\n          doc.rev,\n          Instant.now,\n          TestProbe().ref)),\n      container.ref)\n\n    // choose the warmed container\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    container.expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    // warmed container is failed to resume\n    pool.tell(\n      ResumeFailed(\n        WarmData(\n          mockContainer,\n          invocationNamespace.asString,\n          whiskAction.toExecutableWhiskAction.get,\n          doc.rev,\n          Instant.now,\n          TestProbe().ref)),\n      container.ref)\n\n    // then a new container will be created\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n  }\n\n  it should \"remove oldest previously used container to make space for the job passed to run\" in within(timeout) {\n    (mockContainer.containerId _: () => ContainerId)\n      .expects()\n      .returning(ContainerId(\"test-container-id\"))\n      .anyNumberOfTimes()\n    val (containers, factory) = testContainers(2)\n    val doc = put(entityStore, whiskAction)\n\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig(MemoryLimit.STD_MEMORY * 3),\n          invokerInstance,\n          List.empty,\n          sendAckToScheduler(producer))))\n\n    // register three fake warmed containers, so now pool has no space for new container\n    val container1 = TestProbe()\n    pool.tell(\n      ContainerIsPaused(\n        WarmData(\n          mockContainer,\n          invocationNamespace.asString,\n          whiskAction.toExecutableWhiskAction.get,\n          doc.rev,\n          Instant.now,\n          TestProbe().ref)),\n      container1.ref)\n\n    val container2 = TestProbe()\n    pool.tell(\n      ContainerIsPaused(\n        WarmData(\n          mockContainer,\n          invocationNamespace.asString,\n          whiskAction.toExecutableWhiskAction.get,\n          doc.rev,\n          Instant.now,\n          TestProbe().ref)),\n      container2.ref)\n\n    val container3 = TestProbe()\n    pool.tell(\n      ContainerIsPaused(\n        WarmData(\n          mockContainer,\n          invocationNamespace.asString,\n          whiskAction.toExecutableWhiskAction.get,\n          doc.rev,\n          Instant.now,\n          TestProbe().ref)),\n      container3.ref)\n\n    // now the pool has no free memory, and new job needs 2*stdMemory, so it needs to remove two warmed containers\n    pool ! CreationContainer(creationMessage, bigWhiskAction)\n    container1.expectMsg(Remove)\n    container2.expectMsg(Remove)\n    container3.expectNoMessage()\n\n    // a new container will be created\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n  }\n\n  private def createAckMsg(creationMessage: ContainerCreationMessage,\n                           error: Option[ContainerCreationError],\n                           reason: Option[String]) = {\n    ContainerCreationAckMessage(\n      creationMessage.transid,\n      creationMessage.creationId,\n      invocationNamespace.asString,\n      creationMessage.action,\n      creationMessage.revision,\n      creationMessage.whiskActionMetaData,\n      invokerInstance,\n      creationMessage.schedulerHost,\n      creationMessage.rpcPort,\n      creationMessage.retryCount,\n      error,\n      reason)\n  }\n\n  it should \"send ack(success) to scheduler when container creation is finished\" in within(timeout) {\n    val (containers, factory) = testContainers(1)\n    val doc = put(entityStore, whiskAction)\n    // Actions are created with default memory limit (MemoryLimit.stdMemory). This means 4 actions can be scheduled.\n    val topic = s\"creationAck${creationMessage.rootSchedulerIndex.asString}\"\n\n    val consumer = new TestConnector(topic, 4, true)\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig(MemoryLimit.STD_MEMORY),\n          invokerInstance,\n          List.empty,\n          sendAckToScheduler(consumer.getProducer()))))\n\n    val actualCreationMessage = creationMessage.copy(revision = doc.rev)\n    val ackMessage = createAckMsg(actualCreationMessage, None, None)\n\n    pool ! CreationContainer(actualCreationMessage, whiskAction)\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n    containers(0).send(pool, Initialized(initializedData)) // container is initialized\n\n    utilRetry({\n      val buffer = consumer.peek(50.millisecond)\n      buffer.size shouldBe 1\n      buffer.head._1 shouldBe topic\n      buffer.head._4 shouldBe ackMessage.serialize.getBytes\n    }, 10, Some(500.millisecond))\n  }\n\n  it should \"send ack(success) to scheduler when chosen warmed container is resumed\" in within(timeout) {\n    (mockContainer.containerId _: () => ContainerId)\n      .expects()\n      .returning(ContainerId(\"test-container-id\"))\n      .anyNumberOfTimes()\n    val (containers, factory) = testContainers(1)\n    val doc = put(entityStore, whiskAction)\n    // Actions are created with default memory limit (MemoryLimit.stdMemory). This means 4 actions can be scheduled.\n    val topic = s\"creationAck${creationMessage.rootSchedulerIndex.asString}\"\n\n    val consumer = new TestConnector(topic, 4, true)\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig(MemoryLimit.STD_MEMORY),\n          invokerInstance,\n          List.empty,\n          sendAckToScheduler(consumer.getProducer()))))\n\n    val actualCreationMessage = creationMessage.copy(revision = doc.rev)\n    val ackMessage = createAckMsg(actualCreationMessage, None, None)\n\n    // register a fake warmed container\n    val container = TestProbe()\n    pool.tell(\n      ContainerIsPaused(\n        WarmData(\n          mockContainer,\n          invocationNamespace.asString,\n          whiskAction.toExecutableWhiskAction.get,\n          doc.rev,\n          Instant.now,\n          TestProbe().ref)),\n      container.ref)\n\n    // choose the warmed container\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    container.expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n    pool.tell(\n      Resumed(\n        WarmData(\n          mockContainer,\n          invocationNamespace.asString,\n          whiskAction.toExecutableWhiskAction.get,\n          doc.rev,\n          Instant.now,\n          TestProbe().ref)),\n      container.ref)\n\n    utilRetry({\n      val buffer = consumer.peek(50.millisecond)\n      buffer.size shouldBe 1\n      buffer.head._1 shouldBe topic\n      buffer.head._4 shouldBe ackMessage.serialize.getBytes\n    }, 10, Some(500.millisecond))\n  }\n\n  it should \"send ack(reschedule) to scheduler when container creation is failed or resource is not enough\" in within(\n    timeout) {\n    val (containers, factory) = testContainers(1)\n    val doc = put(entityStore, bigWhiskAction)\n    val doc2 = put(entityStore, whiskAction)\n    val topic = s\"creationAck${schedulerInstanceId.asString}\"\n    val consumer = new TestConnector(topic, 4, true)\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig(MemoryLimit.STD_MEMORY),\n          invokerInstance,\n          List.empty,\n          sendAckToScheduler(consumer.getProducer()))))\n\n    val actualCreationMessageLarge = creationMessageLarge.copy(revision = doc.rev)\n    val error =\n      s\"creationId: ${creationMessageLarge.creationId}, invoker[$invokerInstance] doesn't have enough resource for container: ${creationMessageLarge.action}\"\n    val ackMessage =\n      createAckMsg(actualCreationMessageLarge, Some(ResourceNotEnoughError), Some(error))\n\n    pool ! CreationContainer(actualCreationMessageLarge, bigWhiskAction)\n\n    utilRetry({\n      val buffer = consumer.peek(50.millisecond)\n      buffer.size shouldBe 1\n      buffer.head._1 shouldBe topic\n      buffer.head._4 shouldBe ackMessage.serialize.getBytes\n    }, 10, Some(500.millisecond))\n\n    val actualCreationMessage = creationMessage.copy(revision = doc2.rev)\n    val rescheduleAckMsg = createAckMsg(actualCreationMessage, Some(UnknownError), Some(\"ContainerProxy init failed.\"))\n\n    pool ! CreationContainer(actualCreationMessage, whiskAction)\n    containers(0).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n    containers(0).send(pool, ContainerRemoved(true)) // the container0 init failed or create container failed\n\n    utilRetry({\n      val buffer2 = consumer.peek(50.millisecond)\n      buffer2.size shouldBe 1\n      buffer2.head._1 shouldBe topic\n      buffer2.head._4 shouldBe rescheduleAckMsg.serialize.getBytes\n    }, 10, Some(500.millisecond))\n  }\n\n  it should \"send memory info to invokerHealthManager immediately when doesn't have enough resource happens\" in within(\n    timeout) {\n    val (containers, factory) = testContainers(1)\n    val doc = put(entityStore, whiskAction)\n\n    val invokerHealthService = TestProbe()\n    var count = 0\n    invokerHealthService.setAutoPilot((_: ActorRef, msg: Any) =>\n      msg match {\n        case _: MemoryInfo =>\n          count += 1\n          TestActor.KeepRunning\n\n        case _ =>\n          TestActor.KeepRunning\n    })\n\n    val pool = system.actorOf(\n      Props(new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService.ref,\n        poolConfig(MemoryLimit.STD_MEMORY * 1, memorySyncInterval = 1.minute),\n        invokerInstance,\n        List(PrewarmingConfig(1, exec, memoryLimit)),\n        sendAckToScheduler(producer))))\n    containers(0).expectMsg(Start(exec, memoryLimit))\n\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n\n    awaitAssert {\n      count shouldBe 3\n    }\n  }\n\n  it should \"adjust prewarm container run well without reactive config\" in {\n    stream.reset()\n    val (containers, factory) = testContainers(4)\n\n    val prewarmExpirationCheckInitDelay = FiniteDuration(2, TimeUnit.SECONDS)\n    val prewarmExpirationCheckIntervel = FiniteDuration(2, TimeUnit.SECONDS)\n    val poolConfig =\n      ContainerPoolConfig(\n        MemoryLimit.STD_MEMORY * 4,\n        0.5,\n        false,\n        prewarmExpirationCheckInitDelay,\n        prewarmExpirationCheckIntervel,\n        None,\n        100,\n        3,\n        false,\n        1.second,\n        10)\n    val initialCount = 2\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig,\n          invokerInstance,\n          List(PrewarmingConfig(initialCount, exec, memoryLimit)),\n          sendAckToScheduler(producer))))\n\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(1).expectMsg(Start(exec, memoryLimit))\n    containers(0).send(pool, ReadyToWork(prewarmedData))\n    containers(1).send(pool, ReadyToWork(prewarmedData))\n\n    // when invoker starts, include 0 prewarm container at the very beginning\n    stream.toString should include(s\"found 0 started\")\n\n    // the desiredCount should equal with initialCount when invoker starts\n    stream.toString should include(s\"desired count: ${initialCount}\")\n\n    stream.reset()\n\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n\n    // Because already supplemented the prewarmed container, so currentCount should equal with initialCount\n    eventually {\n      stream.toString should not include (\"started\")\n    }\n  }\n\n  it should \"adjust prewarm container run well with reactive config\" in {\n    stream.reset()\n    val (containers, factory) = testContainers(15)\n    val doc = put(entityStore, whiskAction)\n\n    val prewarmExpirationCheckInitDelay = FiniteDuration(2, TimeUnit.SECONDS)\n    val prewarmExpirationCheckIntervel = FiniteDuration(2, TimeUnit.SECONDS)\n    val poolConfig =\n      ContainerPoolConfig(\n        MemoryLimit.STD_MEMORY * 8,\n        0.5,\n        false,\n        prewarmExpirationCheckInitDelay,\n        prewarmExpirationCheckIntervel,\n        None,\n        100,\n        3,\n        false,\n        1.second,\n        10)\n    val minCount = 0\n    val initialCount = 2\n    val maxCount = 4\n    val ttl = FiniteDuration(500, TimeUnit.MILLISECONDS)\n    val threshold = 1\n    val increment = 1\n    val deadline: Option[Deadline] = Some(ttl.fromNow)\n    val reactive: Option[ReactivePrewarmingConfig] =\n      Some(ReactivePrewarmingConfig(minCount, maxCount, ttl, threshold, increment))\n    val prewarmedData = PreWarmData(mock[MockableV2Container], actionKind, memoryLimit, deadline)\n    val pool = system.actorOf(\n      Props(new FunctionPullingContainerPool(\n        factory,\n        invokerHealthService.ref,\n        poolConfig,\n        invokerInstance,\n        List(PrewarmingConfig(initialCount, exec, memoryLimit, reactive)),\n        sendAckToScheduler(producer))))\n\n    containers(0).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(1).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(0).send(pool, ReadyToWork(prewarmedData))\n    containers(1).send(pool, ReadyToWork(prewarmedData))\n\n    // when invoker starts, include 0 prewarm container at the very beginning\n    stream.toString should include(s\"found 0 started\")\n\n    // the desiredCount should equal with initialCount when invoker starts\n    stream.toString should include(s\"desired count: ${initialCount}\")\n\n    stream.reset()\n\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n    //expire 2 prewarms\n    containers(0).expectMsg(Remove)\n    containers(1).expectMsg(Remove)\n    containers(0).send(pool, ContainerRemoved(true))\n    containers(1).send(pool, ContainerRemoved(true))\n\n    // currentCount should equal with 0 due to these 2 prewarmed containers are expired\n    stream.toString should not include (s\"found 0 started\")\n\n    // the desiredCount should equal with minCount because cold start didn't happen\n    stream.toString should not include (s\"desired count: ${minCount}\")\n    // Previously created prewarmed containers should be removed\n    stream.toString should not include (s\"removed ${initialCount} expired prewarmed container\")\n\n    // 2 cold start happened\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(2).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(3).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n\n    eventually {\n      // Because already removed expired prewarmed containrs, so currentCount should equal with 0\n      stream.toString should include(s\"found 0 started\")\n      // the desiredCount should equal with 2 due to cold start happened\n      stream.toString should include(s\"desired count: 2\")\n    }\n    containers(4).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(5).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(4).send(pool, ReadyToWork(prewarmedData))\n    containers(5).send(pool, ReadyToWork(prewarmedData))\n\n    stream.reset()\n\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n\n    containers(4).expectMsg(Remove)\n    containers(5).expectMsg(Remove)\n    containers(4).send(pool, ContainerRemoved(true))\n    containers(5).send(pool, ContainerRemoved(true))\n\n    // removed previous 2 prewarmed container due to expired\n    stream.toString should include(s\"removing up to ${poolConfig.prewarmExpirationLimit} of 2 expired containers\")\n\n    stream.reset()\n\n    // 5 code start happened(5 > maxCount)\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(6).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(7).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(8).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(9).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n    pool ! CreationContainer(creationMessage.copy(revision = doc.rev), whiskAction)\n    containers(10).expectMsgPF() {\n      case Initialize(invocationNamespace, fqn, executeAction, schedulerHost, rpcPort, _) => true\n    }\n\n    // Make sure AdjustPrewarmedContainer is sent by ContainerPool's scheduler after prewarmExpirationCheckIntervel time\n    Thread.sleep(prewarmExpirationCheckIntervel.toMillis)\n\n    eventually {\n      // Because already removed expired prewarmed containrs, so currentCount should equal with 0\n      stream.toString should include(s\"found 0 started\")\n      // in spite of the cold start number > maxCount, but the desiredCount can't be greater than maxCount\n      stream.toString should include(s\"desired count: ${maxCount}\")\n    }\n\n    containers(11).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(12).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(13).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(14).expectMsg(Start(exec, memoryLimit, Some(ttl)))\n    containers(11).send(pool, ReadyToWork(prewarmedData))\n    containers(12).send(pool, ReadyToWork(prewarmedData))\n    containers(13).send(pool, ReadyToWork(prewarmedData))\n    containers(14).send(pool, ReadyToWork(prewarmedData))\n  }\n\n  it should \"prewarm failed creation count cannot exceed max retry limit\" in {\n    stream.reset()\n    val (containers, factory) = testContainers(4)\n\n    val prewarmExpirationCheckInitDelay = FiniteDuration(1, TimeUnit.MINUTES)\n    val prewarmExpirationCheckIntervel = FiniteDuration(1, TimeUnit.MINUTES)\n    val maxRetryLimit = 3\n    val poolConfig =\n      ContainerPoolConfig(\n        MemoryLimit.STD_MEMORY * 4,\n        0.5,\n        false,\n        prewarmExpirationCheckInitDelay,\n        prewarmExpirationCheckIntervel,\n        None,\n        100,\n        maxRetryLimit,\n        false,\n        1.second,\n        10)\n    val initialCount = 1\n    val pool = system.actorOf(\n      Props(\n        new FunctionPullingContainerPool(\n          factory,\n          invokerHealthService.ref,\n          poolConfig,\n          invokerInstance,\n          List(PrewarmingConfig(initialCount, exec, memoryLimit)),\n          sendAckToScheduler(producer))))\n\n    // create the prewarm initially\n    containers(0).expectMsg(Start(exec, memoryLimit))\n    containers(0).send(pool, ContainerRemoved(true))\n    stream.toString should not include (s\"prewarm create failed count exceeds max retry limit\")\n\n    // the first retry\n    containers(1).expectMsg(Start(exec, memoryLimit))\n    containers(1).send(pool, ContainerRemoved(true))\n    stream.toString should not include (s\"prewarm create failed count exceeds max retry limit\")\n\n    // the second retry\n    containers(2).expectMsg(Start(exec, memoryLimit))\n    containers(2).send(pool, ContainerRemoved(true))\n    stream.toString should not include (s\"prewarm create failed count exceeds max retry limit\")\n\n    // the third retry\n    containers(3).expectMsg(Start(exec, memoryLimit))\n    containers(3).send(pool, ContainerRemoved(true))\n\n    // the forth retry but failed retry count exceeds max retry limit\n    eventually {\n      stream.toString should include(s\"prewarm create failed count exceeds max retry limit\")\n    }\n  }\n\n}\n\nabstract class MockableV2Container extends Container {\n  protected[core] val addr: ContainerAddress = ContainerAddress(\"nohost\")\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/v2/test/FunctionPullingContainerProxyTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.v2.test\n\nimport org.apache.pekko.actor.FSM.{CurrentState, StateTimeout, SubscribeTransitionCallBack, Transition}\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, ActorSystem, Props}\nimport org.apache.pekko.http.scaladsl.model\nimport org.apache.pekko.io.Tcp.Connect\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.pekko.testkit.{ImplicitSender, TestFSMRef, TestKit, TestProbe}\nimport org.apache.pekko.util.ByteString\nimport com.ibm.etcd.api.{DeleteRangeResponse, KeyValue, PutResponse}\nimport com.ibm.etcd.client.{EtcdClient => Client}\nimport common.{LoggedFunction, StreamLogging, SynchronizedLoggedFunction}\nimport org.apache.openwhisk.common.{GracefulShutdown, Logging, TransactionId}\nimport org.apache.openwhisk.core.ack.ActiveAck\nimport org.apache.openwhisk.core.connector.{AcknowledgementMessage, ActivationMessage}\nimport org.apache.openwhisk.core.containerpool.logging.LogCollectingException\nimport org.apache.openwhisk.core.containerpool.v2._\nimport org.apache.openwhisk.core.containerpool.{\n  ContainerRemoved,\n  Paused => _,\n  Pausing => _,\n  Removing => _,\n  Running => _,\n  Start => _,\n  Uninitialized => _,\n  _\n}\nimport org.apache.openwhisk.core.database.{ArtifactStore, StaleParameter, UserContext}\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.types.AuthStore\nimport org.apache.openwhisk.core.etcd.EtcdClient\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys\nimport org.apache.openwhisk.core.etcd.EtcdType._\nimport org.apache.openwhisk.core.invoker.{Invoker, NamespaceBlacklist}\nimport org.apache.openwhisk.core.service.{GetLease, Lease, RegisterData, UnregisterData}\nimport org.apache.openwhisk.http.Messages\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{Assertion, BeforeAndAfterAll}\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.DefaultJsonProtocol._\nimport spray.json.{JsObject, _}\n\nimport java.net.InetSocketAddress\nimport java.time.Instant\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.atomic.AtomicInteger\nimport scala.collection.mutable\nimport scala.collection.mutable.{Map => MutableMap}\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future, Promise}\n@RunWith(classOf[JUnitRunner])\nclass FunctionPullingContainerProxyTests\n    extends TestKit(ActorSystem(\"FunctionPullingContainerProxy\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with StreamLogging {\n\n  override def afterAll: Unit = {\n    client.close()\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  implicit val ece: ExecutionContextExecutor = system.dispatcher\n\n  val timeout = 20.seconds\n  val longTimeout = timeout * 2\n\n  val log = logging\n\n  val defaultUserMemory: ByteSize = 1024.MB\n\n  val invocationNamespace = EntityName(\"invocationSpace\")\n\n  val schedulerHost = \"127.17.0.1\"\n\n  val rpcPort = 13001\n\n  val neverMatchNamespace = EntityName(\"neverMatchNamespace\")\n\n  val uuid = UUID()\n\n  val poolConfig =\n    ContainerPoolConfig(\n      2.MB,\n      0.5,\n      false,\n      FiniteDuration(2, TimeUnit.SECONDS),\n      FiniteDuration(1, TimeUnit.MINUTES),\n      None,\n      100,\n      3,\n      false,\n      1.second,\n      10)\n\n  val timeoutConfig = ContainerProxyTimeoutConfig(5.seconds, 5.seconds, 5.seconds)\n\n  val messageTransId = TransactionId(TransactionId.testing.meta.id)\n\n  val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n\n  val memoryLimit = 256.MB\n\n  val action = ExecutableWhiskAction(EntityPath(\"actionSpace\"), EntityName(\"actionName\"), exec)\n\n  val fqn = FullyQualifiedEntityName(action.namespace, action.name, Some(action.version))\n\n  val message = ActivationMessage(\n    messageTransId,\n    action.fullyQualifiedName(true),\n    action.rev,\n    Identity(Subject(), Namespace(invocationNamespace, uuid), BasicAuthenticationAuthKey(uuid, Secret()), Set.empty),\n    ActivationId.generate(),\n    ControllerInstanceId(\"0\"),\n    blocking = false,\n    content = None)\n\n  val entityStore = WhiskEntityStore.datastore()\n\n  val invokerHealthManager = TestProbe()\n\n  val testContainerId = ContainerId(\"testcontainerId\")\n\n  val testLease = Lease(60, 10)\n\n  def keepAliveService: ActorRef =\n    system.actorOf(Props(new Actor {\n      override def receive: Receive = {\n        case GetLease =>\n          Thread.sleep(1000)\n          sender() ! testLease\n      }\n    }))\n\n  def healthchecksConfig(enabled: Boolean = false) = ContainerProxyHealthCheckConfig(enabled, 100.milliseconds, 2)\n\n  val client: Client = {\n    val hostAndPorts = \"172.17.0.1:2379\"\n    Client.forEndpoints(hostAndPorts).withPlainText().build()\n  }\n\n  class MockEtcdClient() extends EtcdClient(client)(ece) {\n    val etcdStore = MutableMap[String, String]()\n    var putFlag = false\n    override def put(key: String, value: String): Future[PutResponse] = {\n      etcdStore.update(key, value)\n      putFlag = true\n      Future.successful(\n        PutResponse.newBuilder().setPrevKv(KeyValue.newBuilder().setKey(key).setValue(value).build()).build())\n    }\n\n    override def del(key: String): Future[DeleteRangeResponse] = {\n      etcdStore.remove(key)\n      val res = DeleteRangeResponse.getDefaultInstance\n      Future.successful(\n        DeleteRangeResponse.newBuilder().setPrevKvs(0, KeyValue.newBuilder().setKey(key).build()).build())\n    }\n  }\n\n  val initInterval = {\n    val now = messageTransId.meta.start.plusMillis(50) // this is the queue time for cold start\n    Interval(now, now.plusMillis(100))\n  }\n\n  val runInterval = {\n    val now = initInterval.end.plusMillis(75) // delay between init and run\n    Interval(now, now.plusMillis(200))\n  }\n\n  val errorInterval = {\n    val now = initInterval.end.plusMillis(75) // delay between init and run\n    Interval(now, now.plusMillis(150))\n  }\n\n  /** Creates a client and a factory returning this ref of the client. */\n  def testClient\n    : (TestProbe,\n       (ActorRefFactory, String, FullyQualifiedEntityName, DocRevision, String, Int, ContainerId) => ActorRef) = {\n    val client = TestProbe()\n    val factory =\n      (_: ActorRefFactory, _: String, _: FullyQualifiedEntityName, _: DocRevision, _: String, _: Int, _: ContainerId) =>\n        client.ref\n    (client, factory)\n  }\n\n  /** get WhiskAction*/\n  def getWhiskAction(response: Future[WhiskAction]) = LoggedFunction {\n    (_: ArtifactStore[WhiskEntity], _: DocId, _: DocRevision, _: Boolean, _: Boolean) =>\n      response\n  }\n\n  /** Creates an inspectable factory */\n  def createFactory(response: Future[Container]) = LoggedFunction {\n    (_: TransactionId,\n     _: String,\n     _: ImageName,\n     _: Boolean,\n     _: ByteSize,\n     _: Int,\n     _: Option[Double],\n     _: Option[ExecutableWhiskAction]) =>\n      response\n  }\n\n  trait LoggedAcker extends ActiveAck {\n    def calls =\n      mutable.Buffer[(TransactionId, WhiskActivation, Boolean, ControllerInstanceId, UUID, AcknowledgementMessage)]()\n\n    def verifyAnnotations(activation: WhiskActivation, a: ExecutableWhiskAction) = {\n      activation.annotations.get(\"limits\") shouldBe Some(a.limits.toJson)\n      activation.annotations.get(\"path\") shouldBe Some(a.fullyQualifiedName(false).toString.toJson)\n      activation.annotations.get(\"kind\") shouldBe Some(a.exec.kind.toJson)\n    }\n  }\n\n  /** Creates an inspectable version of the ack method, which records all calls in a buffer */\n  def createAcker(a: ExecutableWhiskAction = action) = new LoggedAcker {\n    val acker = LoggedFunction {\n      (_: TransactionId, _: WhiskActivation, _: Boolean, _: ControllerInstanceId, _: UUID, _: AcknowledgementMessage) =>\n        Future.successful(())\n    }\n\n    override def calls = acker.calls\n\n    override def apply(tid: TransactionId,\n                       activation: WhiskActivation,\n                       blockingInvoke: Boolean,\n                       controllerInstance: ControllerInstanceId,\n                       userId: UUID,\n                       acknowledgement: AcknowledgementMessage): Future[Any] = {\n      verifyAnnotations(activation, a)\n      acker(tid, activation, blockingInvoke, controllerInstance, userId, acknowledgement)\n    }\n  }\n\n  /** Creates an synchronized inspectable version of the ack method, which records all calls in a buffer */\n  def createAckerForNamespaceBlacklist(a: ExecutableWhiskAction = action,\n                                       mockNamespaceBlacklist: MockNamespaceBlacklist) = new LoggedAcker {\n    val acker = SynchronizedLoggedFunction {\n      (_: TransactionId, _: WhiskActivation, _: Boolean, _: ControllerInstanceId, _: UUID, _: AcknowledgementMessage) =>\n        Future.successful(())\n    }\n\n    override def calls = acker.calls\n\n    override def verifyAnnotations(activation: WhiskActivation, a: ExecutableWhiskAction): Assertion = {\n      activation.annotations.get(\"path\") shouldBe Some(a.fullyQualifiedName(false).toString.toJson)\n    }\n\n    override def apply(tid: TransactionId,\n                       activation: WhiskActivation,\n                       blockingInvoke: Boolean,\n                       controllerInstance: ControllerInstanceId,\n                       userId: UUID,\n                       acknowledgement: AcknowledgementMessage): Future[Any] = {\n      verifyAnnotations(activation, a)\n      acker(tid, activation, blockingInvoke, controllerInstance, userId, acknowledgement)\n    }\n  }\n\n  /** Registers the transition callback and expects the first message */\n  def registerCallback(c: ActorRef, probe: TestProbe) = {\n    c ! SubscribeTransitionCallBack(probe.ref)\n    probe.expectMsg(CurrentState(c, Uninitialized))\n  }\n\n  def createStore = LoggedFunction {\n    (transid: TransactionId, activation: WhiskActivation, isBlocking: Boolean, context: UserContext) =>\n      Future.successful(())\n  }\n\n  def getLiveContainerCount(count: Long) = LoggedFunction { (_: String, _: FullyQualifiedEntityName, _: DocRevision) =>\n    Future.successful(count)\n  }\n\n  def getLiveContainerCountFail(count: Long) = LoggedFunction {\n    (_: String, _: FullyQualifiedEntityName, _: DocRevision) =>\n      Future.failed(new Exception(\"failure\"))\n  }\n\n  def getLiveContainerCountFailFirstCall(count: Long) = {\n    var firstCall = true\n    LoggedFunction { (_: String, _: FullyQualifiedEntityName, _: DocRevision) =>\n      if (firstCall) {\n        firstCall = false\n        Future.failed(new Exception(\"failure\"))\n      } else {\n        Future.successful(count)\n      }\n    }\n  }\n\n  def getWarmedContainerLimit(limit: Future[(Int, FiniteDuration)]) = LoggedFunction { (_: String) =>\n    limit\n  }\n\n  class LoggedCollector(response: Future[ActivationLogs], invokeCallback: () => Unit) extends Invoker.LogsCollector {\n    val collector = LoggedFunction {\n      (transid: TransactionId,\n       user: Identity,\n       activation: WhiskActivation,\n       container: Container,\n       action: ExecutableWhiskAction) =>\n        response\n    }\n\n    def calls = collector.calls\n\n    override def apply(transid: TransactionId,\n                       user: Identity,\n                       activation: WhiskActivation,\n                       container: Container,\n                       action: ExecutableWhiskAction) = {\n      invokeCallback()\n      collector(transid, user, activation, container, action)\n    }\n  }\n\n  def createCollector(response: Future[ActivationLogs] = Future.successful(ActivationLogs()),\n                      invokeCallback: () => Unit = () => ()) =\n    new LoggedCollector(response, invokeCallback)\n\n  /** Expect a NeedWork message with prewarmed data */\n  def expectPreWarmed(kind: String, probe: TestProbe) = probe.expectMsgPF() {\n    case ReadyToWork(PreWarmData(_, kind, memoryLimit, _)) => true\n  }\n\n  /** Expect a Initialized message with prewarmed data */\n  def expectInitialized(probe: TestProbe) = probe.expectMsgPF() {\n    case Initialized(InitializedData(_, _, _, _)) => true\n  }\n\n  /** Pre-warms the given state-machine, assumes good cases */\n  def preWarm(machine: ActorRef, probe: TestProbe) = {\n    machine ! Start(exec, memoryLimit)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingContainer))\n    expectPreWarmed(exec.kind, probe)\n    probe.expectMsg(Transition(machine, CreatingContainer, ContainerCreated))\n  }\n\n  behavior of \"FunctionPullingContainerProxy\"\n\n  it should \"create a prewarm container\" in within(timeout) {\n    implicit val transid: TransactionId = messageTransId\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val probe = TestProbe()\n\n    val (_, clientFactory) = testClient\n\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            createAcker(),\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, Some(\"myname\"), userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    factory.calls should have size 1\n    val (tid, name, _, _, memory, _, _, _) = factory.calls(0)\n    tid shouldBe TransactionId.invokerWarmup\n    name should fullyMatch regex \"\"\"wskmyname\\d+_\\d+_prewarm_actionKind\"\"\"\n    memory shouldBe memoryLimit\n  }\n\n  it should \"run actions to a started prewarm container with get activationMessage successfully\" in within(timeout) {\n    implicit val transid: TransactionId = messageTransId\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, transid)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 0\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"run actions to a started prewarm container with get no activationMessage\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, RetryRequestActivation)\n    client.expectMsg(RequestActivation())\n    client.send(machine, StateTimeout)\n    client.send(machine, RetryRequestActivation)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, ClientCreated, Removing))\n    client.expectMsg(StopClientProxy)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls.length shouldBe 0\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 0\n      store.calls.length shouldBe 0\n    }\n  }\n\n  it should \"run actions to a cold start container with get activationMessage successfully\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, Some(\"myname\"), userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    val (tid, name, _, _, memory, _, _, _) = factory.calls(0)\n    tid shouldBe TransactionId.invokerColdstart\n    name should fullyMatch regex \"\"\"wskmyname\\d+_\\d+_actionSpace_actionName\"\"\"\n    memory shouldBe memoryLimit\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 0\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"run actions to a cold start container with get no activationMessage\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, RetryRequestActivation)\n    client.expectMsg(RequestActivation())\n    client.send(machine, StateTimeout)\n    client.send(machine, RetryRequestActivation)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, ClientCreated, Removing))\n    client.expectMsg(StopClientProxy)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls.length shouldBe 0\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 0\n      store.calls.length shouldBe 0\n    }\n  }\n\n  it should \"not run activations and destroy the prewarm container when get client failed\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n\n    def clientFactory(f: ActorRefFactory,\n                      invocationNamespace: String,\n                      fqn: FullyQualifiedEntityName,\n                      d: DocRevision,\n                      schedulerHost: String,\n                      rpcPort: Int,\n                      c: ContainerId): ActorRef = {\n      throw new Exception(\"failed to create activation client\")\n    }\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    probe.expectMsg(ContainerRemoved(true))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls.length shouldBe 0\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 0\n      store.calls.length shouldBe 0\n    }\n  }\n\n  it should \"not run activations and don't create cold start container when get client failed\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n\n    def clientFactory(f: ActorRefFactory,\n                      invocationNamespace: String,\n                      fqn: FullyQualifiedEntityName,\n                      d: DocRevision,\n                      schedulerHost: String,\n                      rpcPort: Int,\n                      c: ContainerId): ActorRef = {\n      throw new Exception(\"failed to create activation client\")\n    }\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    probe.expectMsg(ContainerRemoved(true))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls.length shouldBe 0\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 0\n      store.calls.length shouldBe 0\n    }\n  }\n\n  it should \"destroy container proxy when the client closed in CreatingClient\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientClosed)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, CreatingClient, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls.length shouldBe 0\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 0\n      store.calls.length shouldBe 0\n    }\n  }\n\n  it should \"destroy container proxy when the client closed in ContainerCreated\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, ClientClosed)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, ClientCreated, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls.length shouldBe 0\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 0\n      store.calls.length shouldBe 0\n    }\n  }\n\n  it should \"destroy container proxy when the client closed in Running\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, message)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, ClientClosed)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Running, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 2\n      collector.calls.length shouldBe 2\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 2\n      store.calls.length shouldBe 2\n    }\n  }\n\n  it should \"destroy container proxy when stopping due to timeout\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(2)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    probe watch machine\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    machine ! StateTimeout\n    client.send(machine, RetryRequestActivation)\n    probe.expectMsg(Transition(machine, Running, Pausing))\n    probe.expectMsgType[ContainerIsPaused]\n    probe.expectMsg(Transition(machine, Pausing, Paused))\n\n    machine ! StateTimeout\n    client.expectMsg(StopClientProxy)\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Paused, Removing))\n    client.send(machine, ClientClosed)\n\n    probe expectTerminated machine\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"destroy container proxy when stopping due to timeout and getting live count fails once\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCountFailFirstCall(2)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    probe watch machine\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    //register running container\n    dataManagementService.expectMsgType[RegisterData]\n\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    machine ! StateTimeout\n    client.send(machine, RetryRequestActivation)\n    probe.expectMsg(Transition(machine, Running, Pausing))\n    probe.expectMsgType[ContainerIsPaused]\n    probe.expectMsg(Transition(machine, Pausing, Paused))\n    //register paused warmed container\n    dataManagementService.expectMsgType[RegisterData]\n\n    machine ! StateTimeout\n    client.expectMsg(StopClientProxy)\n    dataManagementService.expectMsgType[UnregisterData]\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Paused, Removing))\n    client.send(machine, ClientClosed)\n\n    probe expectTerminated machine\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"destroy container proxy when stopping due to timeout and getting live count fails permanently\" in within(\n    timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCountFail(2)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    probe watch machine\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    //register running container\n    dataManagementService.expectMsgType[RegisterData]\n\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    machine ! StateTimeout\n    client.send(machine, RetryRequestActivation)\n    probe.expectMsg(Transition(machine, Running, Pausing))\n    probe.expectMsgType[ContainerIsPaused]\n    probe.expectMsg(Transition(machine, Pausing, Paused))\n    //register paused warmed container\n    dataManagementService.expectMsgType[RegisterData]\n\n    machine ! StateTimeout\n    client.expectMsg(StopClientProxy)\n    dataManagementService.expectMsgType[UnregisterData]\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Paused, Removing))\n    client.send(machine, ClientClosed)\n\n    probe expectTerminated machine\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"destroy container proxy even if there is no message from the client when stopping due to timeout\" in within(\n    timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(2)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    probe watch machine\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    machine ! StateTimeout\n    client.send(machine, RetryRequestActivation)\n    probe.expectMsg(Transition(machine, Running, Pausing))\n    probe.expectMsgType[ContainerIsPaused]\n    probe.expectMsg(Transition(machine, Pausing, Paused))\n\n    client.send(machine, StateTimeout)\n    client.expectMsg(StopClientProxy)\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Paused, Removing))\n\n    probe expectTerminated (machine, timeout)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"destroy container proxy even Even if there is 1 container, if timeout in keep\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    probe watch machine\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    machine ! StateTimeout\n    client.send(machine, RetryRequestActivation)\n    probe.expectMsg(Transition(machine, Running, Pausing))\n    probe.expectMsgType[ContainerIsPaused]\n    probe.expectMsg(Transition(machine, Pausing, Paused))\n\n    client.send(machine, StateTimeout)\n    client.send(machine, v2.Remove)\n    client.expectMsg(StopClientProxy)\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Paused, Removing))\n\n    probe expectTerminated (machine, timeout)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"Keep the container if live count is less than warmed container keeping count configuration\" in within(\n    timeout) {\n    stream.reset()\n    val warmedContainerTimeout = 10.seconds\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((2, warmedContainerTimeout)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    probe watch machine\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    machine ! StateTimeout\n    client.send(machine, RetryRequestActivation)\n    probe.expectMsg(Transition(machine, Running, Pausing))\n    probe.expectMsgType[ContainerIsPaused]\n    probe.expectMsg(Transition(machine, Pausing, Paused))\n\n    machine ! StateTimeout\n    probe.expectNoMessage(warmedContainerTimeout)\n    probe.expectMsgAllOf(warmedContainerTimeout, ContainerRemoved(true), Transition(machine, Paused, Removing))\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"Remove the container if live count is greater than warmed container keeping count configuration\" in within(\n    timeout) {\n    stream.reset()\n    val warmedContainerTimeout = 10.seconds\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(2)\n    val limit = getWarmedContainerLimit(Future.successful((1, warmedContainerTimeout)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    probe watch machine\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    machine ! StateTimeout\n    client.send(machine, RetryRequestActivation)\n    probe.expectMsg(Transition(machine, Running, Pausing))\n    probe.expectMsgType[ContainerIsPaused]\n    probe.expectMsg(Transition(machine, Pausing, Paused))\n\n    machine ! StateTimeout\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Paused, Removing))\n    probe expectTerminated (machine, timeout)\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"remove the ETCD data first when disabling the container proxy\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val instanceId = InvokerInstanceId(0, userMemory = defaultUserMemory)\n    val probe = TestProbe()\n    val machine =\n      TestFSMRef(\n        new FunctionPullingContainerProxy(\n          factory,\n          entityStore,\n          namespaceBlacklist,\n          get,\n          dataManagementService.ref,\n          clientFactory,\n          acker,\n          store,\n          collector,\n          counter,\n          limit,\n          instanceId,\n          invokerHealthManager.ref,\n          poolConfig,\n          timeoutConfig,\n          healthchecksConfig(),\n          None),\n        probe.ref)\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    awaitAssert {\n      machine.underlyingActor.stateData.getContainer should not be None\n    }\n\n    val containerId = machine.underlyingActor.stateData.getContainer match {\n      case Some(container) => container.containerId\n      case None            => ContainerId(\"\")\n    }\n\n    dataManagementService.expectMsg(RegisterData(\n      s\"${ContainerKeys.existingContainers(invocationNamespace.asString, fqn, action.rev, Some(instanceId), Some(containerId))}\",\n      \"\"))\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, message)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    machine ! GracefulShutdown\n\n    dataManagementService.expectMsg(\n      UnregisterData(ContainerKeys\n        .existingContainers(invocationNamespace.asString, fqn, action.rev, Some(instanceId), Some(containerId))))\n\n    client.expectMsg(GracefulShutdown)\n    client.send(machine, ClientClosed)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Running, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 2\n      collector.calls.length shouldBe 2\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 2\n      store.calls.length shouldBe 2\n    }\n  }\n\n  it should \"pause itself when timeout and recover when got a new Initialize\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n    val instanceId = InvokerInstanceId(0, userMemory = defaultUserMemory)\n\n    val pool = TestProbe()\n    val probe = TestProbe()\n    val machine =\n      pool.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            instanceId,\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    probe watch machine\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n    dataManagementService.expectMsg(\n      RegisterData(\n        ContainerKeys.existingContainers(\n          invocationNamespace.asString,\n          fqn,\n          DocRevision.empty,\n          Some(instanceId),\n          Some(testContainerId)),\n        \"\"))\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(pool)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    machine ! StateTimeout\n    client.send(machine, RetryRequestActivation)\n    probe.expectMsg(Transition(machine, Running, Pausing))\n    pool.expectMsgType[ContainerIsPaused]\n    dataManagementService.expectMsgAllOf(\n      RegisterData(\n        ContainerKeys\n          .warmedContainers(invocationNamespace.asString, fqn, DocRevision.empty, instanceId, testContainerId),\n        \"\"),\n      UnregisterData(\n        ContainerKeys.existingContainers(\n          invocationNamespace.asString,\n          fqn,\n          DocRevision.empty,\n          Some(instanceId),\n          Some(testContainerId))))\n    probe.expectMsg(Transition(machine, Pausing, Paused))\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    dataManagementService.expectMsgAllOf(\n      UnregisterData(\n        ContainerKeys\n          .warmedContainers(invocationNamespace.asString, fqn, DocRevision.empty, instanceId, testContainerId)),\n      RegisterData(\n        ContainerKeys.existingContainers(\n          invocationNamespace.asString,\n          fqn,\n          DocRevision.empty,\n          Some(instanceId),\n          Some(testContainerId)),\n        \"\"))\n\n    probe.expectMsg(Transition(machine, Paused, Running))\n    pool.expectMsgType[Resumed]\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 0\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n    }\n\n  }\n\n  it should \"not collect logs if the log-limit is set to 0\" in within(timeout) {\n    val noLogsAction = action.copy(limits = ActionLimits(logs = LogLimit(0.MB)))\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(noLogsAction.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker(noLogsAction)\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, noLogsAction, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, message)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount should be > 1\n      collector.calls.length shouldBe 0\n      container.destroyCount shouldBe 0\n      acker.calls.length should be > 1\n      store.calls.length should be > 1\n    }\n  }\n\n  it should \"complete the transaction and abort if container creation fails\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val exception = new Exception()\n    val container = new TestContainer\n    val factory = createFactory(Future.failed(exception))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (_, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsgAllOf(\n      Transition(machine, Uninitialized, CreatingClient),\n      ContainerCreationFailed(exception),\n      ContainerRemoved(true))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls.length shouldBe 0\n      container.destroyCount shouldBe 0\n      acker.calls.length shouldBe 0\n      store.calls.length shouldBe 0\n    }\n  }\n\n  it should \"complete the transaction and destroy the prewarm container on a failed init\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer {\n      override def initialize(initializer: JsObject,\n                              timeout: FiniteDuration,\n                              maxConcurrent: Int,\n                              entity: Option[WhiskAction] = None)(implicit transid: TransactionId): Future[Interval] = {\n        initializeCount += 1\n        Future.failed(InitializationError(initInterval, ActivationResponse.developerError(\"boom\")))\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, ClientCreated, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 0 // should not run the action\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      val activation = acker.calls(0)._2\n      activation.response shouldBe ActivationResponse.developerError(\"boom\")\n      activation.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"complete the transaction and destroy the cold start container on a failed init\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer {\n      override def initialize(initializer: JsObject,\n                              timeout: FiniteDuration,\n                              maxConcurrent: Int,\n                              entity: Option[WhiskAction] = None)(implicit transid: TransactionId): Future[Interval] = {\n        initializeCount += 1\n        Future.failed(InitializationError(initInterval, ActivationResponse.developerError(\"boom\")))\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, ClientCreated, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 0 // should not run the action\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      val activation = acker.calls(0)._2\n      activation.response shouldBe ActivationResponse.developerError(\"boom\")\n      activation.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"complete the transaction and destroy the container on a failed run IFF failure was containerError\" in within(\n    timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer {\n      override def run(parameters: JsValue,\n                       environment: JsObject,\n                       timeout: FiniteDuration,\n                       concurrent: Int,\n                       maxResponse: ByteSize,\n                       truncation: ByteSize,\n                       reschedule: Boolean)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n        atomicRunCount.incrementAndGet()\n        Future.successful((initInterval, ActivationResponse.developerError((\"boom\"))))\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Running, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      acker.calls(0)._2.response shouldBe ActivationResponse.developerError(\"boom\")\n      store.calls.length shouldBe 1\n    }\n  }\n\n  it should \"complete the transaction and reuse the container on a failed run IFF failure was applicationError\" in within(\n    timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer {\n      override def run(parameters: JsValue,\n                       environment: JsObject,\n                       timeout: FiniteDuration,\n                       concurrent: Int,\n                       maxResponse: ByteSize,\n                       truncation: ByteSize,\n                       reschedule: Boolean)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n        atomicRunCount.incrementAndGet()\n        //every other run fails\n        if (runCount % 2 == 0) {\n          Future.successful((runInterval, ActivationResponse.success()))\n        } else {\n          Future.successful((errorInterval, ActivationResponse.applicationError((\"boom\"))))\n        }\n      }\n    }\n\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, message)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount should be > 1\n      collector.calls.length should be > 1\n      container.destroyCount shouldBe 0\n      acker.calls.length should be > 1\n      store.calls.length should be > 1\n\n      val initErrorActivation = acker.calls(0)._2\n      initErrorActivation.duration shouldBe Some((initInterval.duration + errorInterval.duration).toMillis)\n      initErrorActivation.annotations\n        .get(WhiskActivation.initTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe initInterval.duration.toMillis\n      initErrorActivation.annotations\n        .get(WhiskActivation.waitTimeAnnotation)\n        .get\n        .convertTo[Int] shouldBe\n        Interval(message.transid.meta.start, initInterval.start).duration.toMillis\n\n      val runOnlyActivation = acker.calls(1)._2\n      runOnlyActivation.duration shouldBe Some(runInterval.duration.toMillis)\n      runOnlyActivation.annotations.get(WhiskActivation.initTimeAnnotation) shouldBe empty\n      runOnlyActivation.annotations.get(WhiskActivation.waitTimeAnnotation).get.convertTo[Int] shouldBe {\n        Interval(message.transid.meta.start, runInterval.start).duration.toMillis\n      }\n    }\n  }\n\n  it should \"complete the transaction and destroy the container if log reading failed\" in {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n\n    val partialLogs = Vector(\"this log line made it\", Messages.logFailure)\n    val collector = createCollector(Future.failed(LogCollectingException(ActivationLogs(partialLogs))))\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Running, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      acker.calls(0)._2.response shouldBe ActivationResponse.success()\n      store.calls.length shouldBe 1\n      store.calls(0)._2.logs shouldBe ActivationLogs(partialLogs)\n    }\n  }\n\n  it should \"complete the transaction and destroy the container if log reading failed terminally\" in {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector(Future.failed(new Exception))\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Running, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      acker.calls(0)._2.response shouldBe ActivationResponse.success()\n      store.calls.length shouldBe 1\n      store.calls(0)._2.logs shouldBe ActivationLogs(Vector(Messages.logFailure))\n    }\n  }\n\n  it should \"save the container id to etcd when don't destroy the container\" in within(timeout) {\n    implicit val transid: TransactionId = messageTransId\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val instance = InvokerInstanceId(0, userMemory = defaultUserMemory)\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            instance,\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 0\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n      dataManagementService.expectMsg(RegisterData(\n        s\"${ContainerKeys.existingContainers(invocationNamespace.asString, fqn, DocRevision.empty, Some(instance), Some(testContainerId))}\",\n        \"\"))\n    }\n  }\n\n  it should \"save the container id to etcd first and delete it when destroy the container\" in within(timeout) {\n    implicit val transid: TransactionId = messageTransId\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val instance = InvokerInstanceId(0, userMemory = defaultUserMemory)\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(2)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            instance,\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    dataManagementService.expectMsg(RegisterData(\n      s\"${ContainerKeys.existingContainers(invocationNamespace.asString, fqn, DocRevision.empty, Some(instance), Some(testContainerId))}\",\n      \"\"))\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, StateTimeout)\n    client.send(machine, RetryRequestActivation)\n\n    probe.expectMsg(Transition(machine, Running, Pausing))\n    probe.expectMsgType[ContainerIsPaused]\n    probe.expectMsg(Transition(machine, Pausing, Paused))\n\n    client.send(machine, StateTimeout)\n    client.expectMsg(StopClientProxy)\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Paused, Removing))\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 1\n      collector.calls.length shouldBe 1\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 1\n      dataManagementService.expectMsg(UnregisterData(\n        s\"${ContainerKeys.existingContainers(invocationNamespace.asString, fqn, DocRevision.empty, Some(instance), Some(testContainerId))}\"))\n    }\n  }\n\n  it should \"not destroy itself when time out happens but a new activation message comes\" in within(timeout) {\n    implicit val transid: TransactionId = messageTransId\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val instance = InvokerInstanceId(0, userMemory = defaultUserMemory)\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            instance,\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, Uninitialized, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    dataManagementService.expectMsg(RegisterData(\n      s\"${ContainerKeys.existingContainers(invocationNamespace.asString, fqn, DocRevision.empty, Some(instance), Some(testContainerId))}\",\n      \"\"))\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, StateTimeout) // make container time out\n    client.send(machine, message)\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount shouldBe 2\n      collector.calls.length shouldBe 2\n      container.destroyCount shouldBe 0\n      acker.calls.length shouldBe 2\n      store.calls.length shouldBe 2\n      dataManagementService.expectNoMessage()\n    }\n  }\n\n  it should \"get the latest NamespaceBlacklist when NamespaceBlacklist is updated in db\" in within(timeout) {\n    stream.reset()\n    implicit val transid: TransactionId = messageTransId\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val mockNamespaceBlacklist: MockNamespaceBlacklist = new MockNamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAckerForNamespaceBlacklist(mockNamespaceBlacklist = mockNamespaceBlacklist)\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            mockNamespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    mockNamespaceBlacklist.refreshBlacklist()\n    //the namespace:invocationSpace will be added to namespaceBlackboxlist\n    mockNamespaceBlacklist.refreshBlacklist()\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, ClientCreated, Removing))\n\n    stream.toString should include(s\"namespace invocationSpace was blocked in containerProxy\")\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 0\n      container.runCount shouldBe 0\n      collector.calls.length shouldBe 0\n      container.destroyCount shouldBe 1\n      acker.calls.length shouldBe 1\n      store.calls.length shouldBe 0\n    }\n  }\n\n  it should \"block further invocations after invocation space is added in the namespace blacklist\" in within(timeout) {\n    stream.reset()\n    implicit val transid: TransactionId = messageTransId\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val mockNamespaceBlacklist: MockNamespaceBlacklist = new MockNamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val acker = createAckerForNamespaceBlacklist(mockNamespaceBlacklist = mockNamespaceBlacklist)\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            mockNamespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    //first refresh, the namespace:invocationSpace is not in namespaceBlacklist, so activations are executed successfully\n    mockNamespaceBlacklist.refreshBlacklist()\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    stream.toString should include(s\"namespace ${message.user.namespace.name} is not in the namespaceBlacklist\")\n\n    //refresh again, the namespace:invocationSpace will be added to namespaceBlacklist\n    stream.reset()\n    Thread.sleep(1000)\n    mockNamespaceBlacklist.refreshBlacklist()\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, message)\n\n    probe.expectMsgAllOf(ContainerRemoved(true), Transition(machine, Running, Removing))\n    client.expectMsg(StopClientProxy)\n    stream.toString should include(s\"namespace ${message.user.namespace.name} was blocked in containerProxy\")\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount should be >= 1\n      collector.calls.length should be >= 1\n      container.destroyCount shouldBe 1\n      acker.calls.length should be >= 1\n      store.calls.length should be >= 1\n    }\n  }\n\n  it should \"not timeout when running long time action\" in within(longTimeout) {\n    implicit val transid: TransactionId = messageTransId\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val container = new TestContainer {\n      override def run(parameters: JsValue,\n                       environment: JsObject,\n                       timeout: FiniteDuration,\n                       concurrent: Int,\n                       maxResponse: ByteSize,\n                       truncation: ByteSize,\n                       reschedule: Boolean)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n        Thread.sleep((timeoutConfig.pauseGrace + 1.second).toMillis) // 6 sec actions\n        super.run(parameters, environment, timeout, concurrent, maxResponse, truncation)\n      }\n    }\n    val factory = createFactory(Future.successful(container))\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n    client.expectMsgPF(8.seconds) { // wait more than container running time(6 seconds)\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, message)\n    client.expectMsgPF(8.seconds) { // wait more than container running time(6 seconds)\n      case RequestActivation(Some(_), None) => true\n    }\n\n    awaitAssert {\n      factory.calls should have size 1\n      container.initializeCount shouldBe 1\n      container.runCount should be > 1\n      collector.calls.length should be > 1\n      container.destroyCount shouldBe 0\n      acker.calls.length should be > 1\n      store.calls.length should be > 1\n    }\n  }\n\n  it should \"start tcp ping to containers when action healthcheck enabled\" in within(timeout) {\n    implicit val transid: TransactionId = messageTransId\n\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n    val container = new TestContainer\n    val factory = createFactory(Future.successful(container))\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n    val healthchecks = healthchecksConfig(true)\n\n    val probe = TestProbe()\n    val tcpProbe = TestProbe()\n\n    val (client, clientFactory) = testClient\n\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            createAcker(),\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, Some(\"myname\"), userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig,\n            healthchecks,\n            tcp = Some(tcpProbe.ref)))\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    tcpProbe.expectMsg(Connect(new InetSocketAddress(\"0.0.0.0\", 12345)))\n    tcpProbe.expectMsg(Connect(new InetSocketAddress(\"0.0.0.0\", 12345)))\n    tcpProbe.expectMsg(Connect(new InetSocketAddress(\"0.0.0.0\", 12345)))\n\n    //pings should repeat till the container goes into Running state\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, transid)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    // Receive any unhandled messages\n    tcpProbe.receiveWhile(3.seconds, 200.milliseconds, 10) {\n      case Connect(_, None, Nil, None, false) =>\n        true\n    }\n\n    tcpProbe.expectNoMessage(healthchecks.checkPeriod + 100.milliseconds)\n\n    awaitAssert {\n      factory.calls should have size 1\n    }\n  }\n\n  it should \"reschedule the job to the queue if /init fails connection on ClientCreated\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n\n    // test container\n    val initPromise = Promise[Interval]()\n    val container = new TestContainer(initPromise = Some(initPromise))\n    val factory = createFactory(Future.successful(container))\n\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    // throw health error\n    initPromise.failure(ContainerHealthError(messageTransId, \"intentional failure\"))\n\n    val fqn = action.fullyQualifiedName(withVersion = true)\n    val rev = action.rev\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    // message should be rescheduled\n    client.expectMsg(RescheduleActivation(invocationNamespace.asString, fqn, rev, message))\n  }\n\n  it should \"reschedule the job to the queue if /run fails connection on ClientCreated\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n\n    // test container\n    val runPromises = Seq(Promise[(Interval, ActivationResponse)](), Promise[(Interval, ActivationResponse)]())\n    val container = new TestContainer(runPromises = runPromises)\n    val factory = createFactory(Future.successful(container))\n\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    // throw health error\n    runPromises.head.failure(ContainerHealthError(messageTransId, \"intentional failure\"))\n\n    val fqn = action.fullyQualifiedName(withVersion = true)\n    val rev = action.rev\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    // message should be rescheduled\n    client.expectMsg(ContainerWarmed)\n    client.expectMsg(RescheduleActivation(invocationNamespace.asString, fqn, rev, message))\n  }\n\n  it should \"reschedule the job to the queue if /run fails connection on Running\" in within(timeout) {\n    val authStore = mock[ArtifactWhiskAuthStore]\n    val namespaceBlacklist: NamespaceBlacklist = new NamespaceBlacklist(authStore)\n    val get = getWhiskAction(Future(action.toWhiskAction))\n    val dataManagementService = TestProbe()\n\n    val acker = createAcker()\n    val store = createStore\n    val collector = createCollector()\n    val counter = getLiveContainerCount(1)\n    val limit = getWarmedContainerLimit(Future.successful((1, 10.seconds)))\n\n    // test container\n    val runPromises = Seq(Promise[(Interval, ActivationResponse)](), Promise[(Interval, ActivationResponse)]())\n    val container = new TestContainer(runPromises = runPromises)\n    val factory = createFactory(Future.successful(container))\n\n    val (client, clientFactory) = testClient\n\n    val probe = TestProbe()\n    val machine =\n      probe.childActorOf(\n        FunctionPullingContainerProxy\n          .props(\n            factory,\n            entityStore,\n            namespaceBlacklist,\n            get,\n            dataManagementService.ref,\n            clientFactory,\n            acker,\n            store,\n            collector,\n            counter,\n            limit,\n            InvokerInstanceId(0, userMemory = defaultUserMemory),\n            invokerHealthManager.ref,\n            poolConfig,\n            timeoutConfig))\n    registerCallback(machine, probe)\n    preWarm(machine, probe)\n\n    // pass first request to become Running state\n    runPromises(0).success(runInterval, ActivationResponse.success())\n\n    // throw health error\n    runPromises(1).failure(ContainerHealthError(messageTransId, \"intentional failure\"))\n\n    val fqn = action.fullyQualifiedName(withVersion = true)\n    val rev = action.rev\n\n    machine ! Initialize(invocationNamespace.asString, fqn, action, schedulerHost, rpcPort, messageTransId)\n    probe.expectMsg(Transition(machine, ContainerCreated, CreatingClient))\n    client.expectMsg(StartClient)\n    client.send(machine, ClientCreationCompleted)\n\n    probe.expectMsg(Transition(machine, CreatingClient, ClientCreated))\n    expectInitialized(probe)\n    client.expectMsg(RequestActivation())\n    client.send(machine, message)\n\n    probe.expectMsg(Transition(machine, ClientCreated, Running))\n    client.expectMsg(ContainerWarmed)\n\n    client.expectMsgPF() {\n      case RequestActivation(Some(_), None) => true\n    }\n    client.send(machine, message)\n\n    // message should be rescheduled\n    client.expectMsg(RescheduleActivation(invocationNamespace.asString, fqn, rev, message))\n  }\n\n  /**\n   * Implements all the good cases of a perfect run to facilitate error case overriding.\n   */\n  class TestContainer(initPromise: Option[Promise[Interval]] = None,\n                      runPromises: Seq[Promise[(Interval, ActivationResponse)]] = Seq.empty,\n                      apiKeyMustBePresent: Boolean = true)\n      extends Container {\n    protected val id = testContainerId\n    protected[core] val addr = ContainerAddress(\"0.0.0.0\", 12345)\n    protected implicit val logging: Logging = log\n    protected implicit val ec: ExecutionContext = system.dispatcher\n    override implicit protected val as: ActorSystem = system\n    var destroyCount = 0\n    var initializeCount = 0\n    val atomicRunCount = new AtomicInteger(0) //need atomic tracking since we will test concurrent runs\n    var atomicLogsCount = new AtomicInteger(0)\n\n    def runCount = atomicRunCount.get()\n\n    override def destroy()(implicit transid: TransactionId): Future[Unit] = {\n      destroyCount += 1\n      super.destroy()\n    }\n    override def initialize(initializer: JsObject,\n                            timeout: FiniteDuration,\n                            maxConcurrent: Int,\n                            entity: Option[WhiskAction] = None)(implicit transid: TransactionId): Future[Interval] = {\n      initializeCount += 1\n      val envField = \"env\"\n\n      (initializer.fields - envField) shouldBe action.containerInitializer().fields - envField\n\n      timeout shouldBe action.limits.timeout.duration\n\n      val initializeEnv = initializer.fields(envField).asJsObject\n\n      initializeEnv.fields(\"__OW_NAMESPACE\") shouldBe invocationNamespace.name.toJson\n      initializeEnv.fields(\"__OW_ACTION_NAME\") shouldBe message.action.qualifiedNameWithLeadingSlash.toJson\n      initializeEnv.fields(\"__OW_ACTION_VERSION\") shouldBe message.action.version.toJson\n      initializeEnv.fields(\"__OW_ACTIVATION_ID\") shouldBe message.activationId.toJson\n      initializeEnv.fields(\"__OW_TRANSACTION_ID\") shouldBe transid.id.toJson\n\n      val convertedAuthKey = message.user.authkey.toEnvironment.fields.map(f => (\"__OW_\" + f._1.toUpperCase(), f._2))\n      val authEnvironment = initializeEnv.fields.filterKeys(convertedAuthKey.contains)\n      convertedAuthKey shouldBe authEnvironment\n\n      val deadline = Instant.ofEpochMilli(initializeEnv.fields(\"__OW_DEADLINE\").convertTo[String].toLong)\n      val maxDeadline = Instant.now.plusMillis(timeout.toMillis)\n\n      // The deadline should be in the future but must be smaller than or equal\n      // a freshly computed deadline, as they get computed slightly after each other\n      deadline should (be <= maxDeadline and be >= Instant.now)\n\n      initPromise.map(_.future).getOrElse(Future.successful(initInterval))\n    }\n\n    override def run(\n      parameters: JsValue,\n      environment: JsObject,\n      timeout: FiniteDuration,\n      concurrent: Int,\n      maxResponse: ByteSize,\n      truncation: ByteSize,\n      reschedule: Boolean = false)(implicit transid: TransactionId): Future[(Interval, ActivationResponse)] = {\n      val runCount = atomicRunCount.incrementAndGet()\n      environment.fields(\"namespace\") shouldBe invocationNamespace.name.toJson\n      environment.fields(\"action_name\") shouldBe message.action.qualifiedNameWithLeadingSlash.toJson\n      environment.fields(\"action_version\") shouldBe message.action.version.toJson\n      environment.fields(\"activation_id\") shouldBe message.activationId.toJson\n      environment.fields(\"transaction_id\") shouldBe transid.id.toJson\n      val authEnvironment = environment.fields.filterKeys(message.user.authkey.toEnvironment.fields.contains).toMap\n      message.user.authkey.toEnvironment shouldBe authEnvironment.toJson.asJsObject\n      val deadline = Instant.ofEpochMilli(environment.fields(\"deadline\").convertTo[String].toLong)\n      val maxDeadline = Instant.now.plusMillis(timeout.toMillis)\n\n      // The deadline should be in the future but must be smaller than or equal\n      // a freshly computed deadline, as they get computed slightly after each other\n      deadline should (be <= maxDeadline and be >= Instant.now)\n\n      //return the future for this run (if runPromises no empty), or a default response\n      runPromises\n        .lift(runCount - 1)\n        .map(_.future)\n        .getOrElse(Future.successful((runInterval, ActivationResponse.success())))\n    }\n\n    def logs(limit: ByteSize, waitForSentinel: Boolean)(implicit transid: TransactionId): Source[ByteString, Any] = {\n      atomicLogsCount.incrementAndGet()\n      Source.empty\n    }\n  }\n\n  abstract class ArtifactWhiskAuthStore extends ArtifactStore[WhiskAuth] {\n    override protected[core] implicit val executionContext: ExecutionContext = ece\n    override implicit val logging: Logging = log\n\n    override protected[core] def put(d: WhiskAuth)(implicit transid: TransactionId): Future[DocInfo] = ???\n\n    override protected[core] def del(doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] = ???\n\n    override protected[core] def get[A <: WhiskAuth](doc: DocInfo,\n                                                     attachmentHandler: Option[(A, Attachments.Attached) => A])(\n      implicit transid: TransactionId,\n      ma: Manifest[A]): Future[A] = ???\n\n    override protected[core] def query(table: String,\n                                       startKey: List[Any],\n                                       endKey: List[Any],\n                                       skip: Int,\n                                       limit: Int,\n                                       includeDocs: Boolean,\n                                       descending: Boolean,\n                                       reduce: Boolean,\n                                       stale: StaleParameter)(implicit transid: TransactionId): Future[List[JsObject]] =\n      ???\n\n    override protected[core] def count(table: String,\n                                       startKey: List[Any],\n                                       endKey: List[Any],\n                                       skip: Int,\n                                       stale: StaleParameter)(implicit transid: TransactionId): Future[Long] = ???\n\n    override protected[core] def putAndAttach[A <: WhiskAuth](d: A,\n                                                              update: (A, Attachments.Attached) => A,\n                                                              contentType: model.ContentType,\n                                                              docStream: Source[ByteString, _],\n                                                              oldAttachment: Option[Attachments.Attached])(\n      implicit transid: TransactionId): Future[(DocInfo, Attachments.Attached)] = ???\n\n    override protected[core] def readAttachment[T](\n      doc: DocInfo,\n      attached: Attachments.Attached,\n      sink: Sink[ByteString, Future[T]])(implicit transid: TransactionId): Future[T] = ???\n\n    override protected[core] def deleteAttachments[T](doc: DocInfo)(implicit transid: TransactionId): Future[Boolean] =\n      ???\n\n    override def shutdown(): Unit = ???\n  }\n\n  class MockNamespaceBlacklist(authStore: AuthStore) extends NamespaceBlacklist(authStore) {\n\n    var count = 0\n    var blacklist: Set[String] = Set.empty\n\n    override def isBlacklisted(identity: Identity): Boolean = {\n      blacklist.contains(identity.namespace.name.asString)\n    }\n\n    override def refreshBlacklist()(implicit ec: ExecutionContext, tid: TransactionId): Future[Set[String]] = {\n      count += 1\n      if (count == 1) {\n        //neverMatchNamespace is in the namespaceBlacklist\n        blacklist = Set(neverMatchNamespace.name)\n        Future.successful(blacklist)\n      } else {\n        //invocationNamespace is not in the namespaceBlacklist\n        blacklist = Set(invocationNamespace.name)\n        Future.successful(blacklist)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/v2/test/InvokerHealthManagerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.v2.test\n\nimport org.apache.pekko.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}\nimport org.apache.pekko.actor.{ActorRef, ActorRefFactory, ActorSystem}\nimport org.apache.pekko.testkit.{ImplicitSender, TestActor, TestFSMRef, TestKit, TestProbe}\nimport common.StreamLogging\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Offline, Unhealthy}\nimport org.apache.openwhisk.common.{Enable, GracefulShutdown, RingBuffer}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.InvokerResourceMessage\nimport org.apache.openwhisk.core.containerpool.v2._\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.{ExecManifest, InvokerInstanceId, WhiskEntityStore}\nimport org.apache.openwhisk.core.etcd.EtcdKV.InvokerKeys\nimport org.apache.openwhisk.core.service.UpdateDataOnChange\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.collection.mutable\nimport scala.concurrent.duration._\n@RunWith(classOf[JUnitRunner])\nclass InvokerHealthManagerTests\n    extends TestKit(ActorSystem(\"InvokerHealthManager\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with MockFactory\n    with ScalaFutures\n    with StreamLogging\n    with DbUtils {\n\n  override def afterAll = TestKit.shutdownActorSystem(system)\n\n  implicit val ec = system.dispatcher\n\n  val config = new WhiskConfig(ExecManifest.requiredProperties)\n\n  ExecManifest.initialize(config) should be a 'success\n\n  val timeout = 10.seconds\n\n  val instanceId = InvokerInstanceId(0, userMemory = 1024.MB, tags = Seq(\"gpu-enabled\", \"high-memory\"))\n\n  val entityStore = WhiskEntityStore.datastore()\n\n  val freeMemory: Long = 512\n\n  val busyMemory: Long = 256\n\n  val inProgressMemory: Long = 256\n\n  /** Creates a sequence of containers and a factory returning this sequence. */\n  def testContainers(n: Int) = {\n    val containers = (0 to n).map(_ => TestProbe())\n    val queue = mutable.Queue(containers: _*)\n    val factory = (fac: ActorRefFactory, manager: ActorRef) => queue.dequeue().ref\n    (containers, factory)\n  }\n\n  behavior of \"InvokerHealthManager\"\n\n  it should \"invoke health action and become healthy state\" in within(timeout) {\n    val (_, factory) = testContainers(InvokerHealthManager.bufferSize)\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n    val fsm = TestFSMRef(new InvokerHealthManager(instanceId, factory, dataManagementService.ref, entityStore))\n    fsm ! SubscribeTransitionCallBack(probe.ref)\n    probe.expectMsg(CurrentState(fsm, Offline))\n\n    fsm ! Enable\n    (1 to InvokerHealthManager.bufferSize - InvokerHealthManager.bufferErrorTolerance) foreach { _ =>\n      fsm ! HealthMessage(true)\n    }\n\n    probe.expectMsg(Transition(fsm, Offline, Unhealthy))\n    probe.expectMsg(Transition(fsm, Unhealthy, Healthy))\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Unhealthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n  }\n\n  it should \"invoke health action again when it becomes unhealthy\" in within(timeout) {\n    val (_, factory) = testContainers(InvokerHealthManager.bufferSize)\n    val dataManagementService = TestProbe()\n    val fsm = TestFSMRef(new InvokerHealthManager(instanceId, factory, dataManagementService.ref, entityStore))\n    val probe = TestProbe()\n    var buffer = new RingBuffer[Boolean](InvokerHealthManager.bufferSize)\n    (1 to InvokerHealthManager.bufferSize - InvokerHealthManager.bufferErrorTolerance) foreach { _ =>\n      buffer.add(true)\n    }\n    fsm.setState(Healthy, InvokerInfo(buffer, memory = MemoryInfo(instanceId.userMemory.toMB, 0, 0)))\n    fsm ! SubscribeTransitionCallBack(probe.ref)\n    probe.expectMsg(CurrentState(fsm, Healthy))\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n\n    (1 to InvokerHealthManager.bufferErrorTolerance + 1) foreach { _ =>\n      fsm ! HealthMessage(false)\n    }\n\n    probe.expectMsg(Transition(fsm, Healthy, Unhealthy))\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Unhealthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n  }\n\n  it should \"publish healthy status and pool info to etcd together\" in within(timeout) {\n    val (_, factory) = testContainers(InvokerHealthManager.bufferSize)\n    val dataManagementService = TestProbe()\n    val fsm = TestFSMRef(new InvokerHealthManager(instanceId, factory, dataManagementService.ref, entityStore))\n    val probe = TestProbe()\n    fsm ! SubscribeTransitionCallBack(probe.ref)\n    probe.expectMsg(CurrentState(fsm, Offline))\n\n    fsm ! Enable\n    (1 to InvokerHealthManager.bufferSize - InvokerHealthManager.bufferErrorTolerance) foreach { _ =>\n      fsm ! HealthMessage(true)\n    }\n\n    probe.expectMsg(Transition(fsm, Offline, Unhealthy))\n    probe.expectMsg(Transition(fsm, Unhealthy, Healthy))\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Unhealthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n\n    fsm ! MemoryInfo(freeMemory, busyMemory, inProgressMemory)\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          freeMemory,\n          busyMemory,\n          inProgressMemory,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n  }\n\n  it should \"change the invoker pool info to etcd when memory info has changes\" in within(timeout) {\n    val (_, factory) = testContainers(InvokerHealthManager.bufferSize)\n    val dataManagementService = TestProbe()\n    val fsm = TestFSMRef(new InvokerHealthManager(instanceId, factory, dataManagementService.ref, entityStore))\n    val probe = TestProbe()\n    fsm ! SubscribeTransitionCallBack(probe.ref)\n    probe.expectMsg(CurrentState(fsm, Offline))\n\n    fsm ! Enable\n    (1 to InvokerHealthManager.bufferSize - InvokerHealthManager.bufferErrorTolerance) foreach { _ =>\n      fsm ! HealthMessage(true)\n    }\n\n    probe.expectMsg(Transition(fsm, Offline, Unhealthy))\n    probe.expectMsg(Transition(fsm, Unhealthy, Healthy))\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Unhealthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n\n    fsm ! MemoryInfo(freeMemory, busyMemory, inProgressMemory)\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          freeMemory,\n          busyMemory,\n          inProgressMemory,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n\n    val changedFreeMemory = freeMemory - 256\n    val changedBusyMemory = busyMemory + 256\n    fsm ! MemoryInfo(changedFreeMemory, changedBusyMemory, inProgressMemory)\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          changedFreeMemory,\n          changedBusyMemory,\n          inProgressMemory,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n  }\n\n  it should \"disable and enable the invoker gracefully\" in within(timeout) {\n    val (_, factory) = testContainers(InvokerHealthManager.bufferSize)\n    val dataManagementService = TestProbe()\n    val fsm = TestFSMRef(new InvokerHealthManager(instanceId, factory, dataManagementService.ref, entityStore))\n\n    val probe = TestProbe()\n    fsm ! SubscribeTransitionCallBack(probe.ref)\n    probe.expectMsg(CurrentState(fsm, Offline))\n\n    fsm ! Enable\n    (1 to InvokerHealthManager.bufferSize - InvokerHealthManager.bufferErrorTolerance) foreach { _ =>\n      fsm ! HealthMessage(true)\n    }\n\n    probe.expectMsg(Transition(fsm, Offline, Unhealthy))\n    probe.expectMsg(Transition(fsm, Unhealthy, Healthy))\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Unhealthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n\n    fsm ! GracefulShutdown\n\n    probe.expectMsg(Transition(fsm, Healthy, Offline))\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Offline.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n\n    val mockHealthActionProxy = TestProbe()\n\n    mockHealthActionProxy.setAutoPilot((sender, msg) =>\n      msg match {\n        case _: Initialize =>\n          (1 to InvokerHealthManager.bufferSize - InvokerHealthManager.bufferErrorTolerance) foreach { _ =>\n            sender ! HealthMessage(true)\n          }\n          TestActor.KeepRunning\n\n        case GracefulShutdown =>\n          TestActor.KeepRunning\n    })\n\n    fsm.underlyingActor.healthActionProxy = Some(mockHealthActionProxy.ref)\n\n    fsm ! Enable\n\n    probe.expectMsg(Transition(fsm, Offline, Unhealthy))\n    probe.expectMsg(10.seconds, Transition(fsm, Unhealthy, Healthy))\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Unhealthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n  }\n\n  it should \"keep status Offline all the time in spite of receive healthMessage\" in within(timeout) {\n    val (_, factory) = testContainers(InvokerHealthManager.bufferSize)\n    val dataManagementService = TestProbe()\n    val fsm = TestFSMRef(new InvokerHealthManager(instanceId, factory, dataManagementService.ref, entityStore))\n\n    val probe = TestProbe()\n    fsm ! SubscribeTransitionCallBack(probe.ref)\n    probe.expectMsg(CurrentState(fsm, Offline))\n\n    fsm ! Enable\n    (1 to InvokerHealthManager.bufferSize - InvokerHealthManager.bufferErrorTolerance) foreach { _ =>\n      fsm ! HealthMessage(true)\n    }\n\n    probe.expectMsg(Transition(fsm, Offline, Unhealthy))\n    probe.expectMsg(Transition(fsm, Unhealthy, Healthy))\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Unhealthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Healthy.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n\n    fsm ! GracefulShutdown\n\n    probe.expectMsg(Transition(fsm, Healthy, Offline))\n\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Offline.asString,\n          instanceId.userMemory.toMB,\n          0,\n          0,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n\n    (1 to InvokerHealthManager.bufferSize - InvokerHealthManager.bufferErrorTolerance) foreach { _ =>\n      fsm ! HealthMessage(true)\n    }\n\n    // Keep the status Offline all the time unless enable it first\n    probe.expectNoMessage()\n\n    val changedFreeMemory = freeMemory - 256\n    val changedBusyMemory = busyMemory + 256\n    fsm ! MemoryInfo(changedFreeMemory, changedBusyMemory, inProgressMemory)\n\n    // In spite of the status is Offline, the Memory info may be changed during zerodowntime deployment for invoker.\n    dataManagementService.expectMsg(\n      UpdateDataOnChange(\n        InvokerKeys.health(instanceId),\n        InvokerResourceMessage(\n          Offline.asString,\n          changedFreeMemory,\n          changedBusyMemory,\n          inProgressMemory,\n          instanceId.tags,\n          instanceId.dedicatedNamespaces).serialize))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/yarn/test/MockYARNRM.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.yarn.test\n\nimport java.net.InetSocketAddress\nimport java.nio.charset.StandardCharsets\nimport java.util\nimport java.util.concurrent.atomic.AtomicLong\n\nimport org.apache.pekko.http.scaladsl.model.DateTime\nimport com.sun.net.httpserver.{HttpExchange, HttpServer}\nimport org.apache.openwhisk.core.yarn.YARNJsonProtocol._\nimport org.apache.openwhisk.core.yarn.{YARNResponseDefinition, _}\nimport spray.json._\n\nimport scala.collection.mutable\nimport scala.util.Random\n\n//Mocks the Hadoop YARN Resource Manager. Only supports simple authentication\nclass MockYARNRM(port: Int, delayMS: Int) {\n  val services: mutable.Map[String, ServiceDefinition] = mutable.Map[String, ServiceDefinition]()\n  val initCompletionTimes: mutable.Map[String, DateTime] = mutable.Map[String, DateTime]()\n  val flexCompletionTimes: mutable.Map[String, mutable.Map[String, DateTime]] =\n    mutable.Map[String, mutable.Map[String, DateTime]]()\n\n  private var server = HttpServer.create(new InetSocketAddress(port), -1)\n  val POST = \"POST\"\n  val GET = \"GET\"\n  val PUT = \"PUT\"\n  val DELETE = \"DELETE\"\n  private var container_instance_number = new AtomicLong(0)\n\n  this.server\n    .createContext(\n      \"/app/v1/services\",\n      (httpExchange: HttpExchange) => {\n        try {\n          if (getUserName(httpExchange).isEmpty) {\n            writeResponse(httpExchange, 403, \"Username not provided\")\n          } else {\n            val servicePattern = \"/app/v1/services/([a-z-0-9]+)\".r\n            val FlexUrlPattern = \"/app/v1/services/([a-z-0-9]+)/components/([a-z-0-9]+)\".r\n            (httpExchange.getRequestMethod, httpExchange.getRequestURI.getPath) match {\n              case (POST, \"/app/v1/services\") =>\n                val body: String = scala.io.Source.fromInputStream(httpExchange.getRequestBody).mkString\n                val servDef = body.parseJson.convertTo[ServiceDefinition]\n\n                if (this.services.contains(servDef.name.get)) {\n                  writeResponse(httpExchange, 400, YARNResponseDefinition(\"Invalid request. Service already exists\"))\n                } else {\n                  this.services.put(servDef.name.get, servDef.copy(state = Some(\"ACCEPTED\")))\n                  initCompletionTimes.put(servDef.name.get, DateTime.now.plus(delayMS))\n                  flexCompletionTimes.put(servDef.name.get, mutable.Map[String, DateTime]())\n                  writeResponse(httpExchange, 200, YARNResponseDefinition(\"Creating Service\"))\n                }\n\n              case (GET, servicePattern(serviceName)) =>\n                if (!this.services.contains(serviceName)) {\n                  writeResponse(httpExchange, 404, YARNResponseDefinition(\"Service not found\"))\n                } else {\n                  updateServDef(serviceName)\n\n                  writeResponse(httpExchange, 200, this.services(serviceName).toJson.compactPrint)\n                }\n\n              case (PUT, servicePattern(serviceName)) =>\n                val body: String = scala.io.Source.fromInputStream(httpExchange.getRequestBody).mkString\n                val incomingComponentDef = body.parseJson.convertTo[ServiceDefinition].components.head\n                if (!this.services.contains(serviceName)) {\n                  writeResponse(httpExchange, 404, YARNResponseDefinition(\"Service not found\"))\n                } else if (!this\n                             .services(serviceName)\n                             .components\n                             .exists(c => c.name.equals(incomingComponentDef.name))) {\n                  writeResponse(httpExchange, 404, YARNResponseDefinition(\"Component not found\"))\n                } else {\n                  val containerToRemove = incomingComponentDef.decommissioned_instances.head\n\n                  val serviceDef = this.services(serviceName)\n                  val componentDef = serviceDef.components.find(c => c.name.equals(incomingComponentDef.name)).get\n                  val originalSize = componentDef.number_of_containers.get\n                  var containerList = componentDef.containers.getOrElse(List[ContainerDefinition]())\n                  containerList = containerList.filterNot(c => c.component_instance_name.equals(containerToRemove))\n\n                  val newComponentDef =\n                    componentDef.copy(number_of_containers = Some(originalSize - 1), containers = Option(containerList))\n\n                  val partialComponentList = serviceDef.components.filter(c => !c.name.equals(componentDef.name))\n\n                  this.services.put(serviceName, serviceDef.copy(components = partialComponentList :+ newComponentDef))\n\n                  writeResponse(\n                    httpExchange,\n                    200,\n                    YARNResponseDefinition(s\"Service $serviceName has successfully decommissioned instances.\"))\n                }\n              case (DELETE, servicePattern(serviceName)) =>\n                if (!this.services.contains(serviceName)) {\n                  writeResponse(httpExchange, 404, YARNResponseDefinition(\"Service not found\"))\n                } else {\n                  this.services.remove(serviceName)\n                  this.initCompletionTimes.remove(serviceName)\n                  this.flexCompletionTimes.remove(serviceName)\n                  writeResponse(httpExchange, 200, YARNResponseDefinition(\"Service deleted\"))\n                }\n\n              case (PUT, FlexUrlPattern(serviceName, componentName)) =>\n                val serviceDef = this.services.get(serviceName).orNull\n                val body: String = scala.io.Source.fromInputStream(httpExchange.getRequestBody).mkString\n                val newSize = body.parseJson.asJsObject.fields.find(field => field._1.equals(\"number_of_containers\"))\n                if (serviceDef == null || !flexCompletionTimes.contains(serviceName)) {\n                  writeResponse(httpExchange, 404, YARNResponseDefinition(\"Service not found\"))\n                } else if (newSize.isEmpty) {\n                  writeResponse(\n                    httpExchange,\n                    400,\n                    YARNResponseDefinition(\"Invalid request. number_of_containers not specified\"))\n                } else {\n                  val newSizeInt: Int = newSize.get._2.asInstanceOf[JsNumber].value.toInt\n                  val componentDef = serviceDef.components.find(c => c.name.equals(componentName))\n\n                  if (componentDef.isEmpty) {\n                    writeResponse(\n                      httpExchange,\n                      400,\n                      YARNResponseDefinition(\"Invalid request. Component does not exist\"))\n                  } else {\n                    val originalSize = componentDef.get.number_of_containers.get\n\n                    var containerList = componentDef.get.containers.getOrElse(List[ContainerDefinition]())\n                    if (originalSize < newSizeInt) {\n                      containerList = containerList :+ ContainerDefinition(\n                        Some(\"127.0.0.1\"),\n                        Option(\"\"),\n                        componentName + \"-\" + container_instance_number.getAndIncrement(),\n                        Option(\"\"),\n                        Random.alphanumeric.take(10).mkString,\n                        0,\n                        \"INIT\")\n                      flexCompletionTimes.get(serviceName).orNull.put(componentName, DateTime.now.plus(delayMS))\n                    } else {\n                      containerList = containerList.init\n                    }\n\n                    val newComponentDef =\n                      componentDef.get.copy(number_of_containers = Some(newSizeInt), containers = Option(containerList))\n\n                    val partialComponentList = serviceDef.components.filter(c => !c.name.equals(componentName))\n\n                    this.services\n                      .put(serviceName, serviceDef.copy(components = partialComponentList :+ newComponentDef))\n\n                    writeResponse(\n                      httpExchange,\n                      200,\n                      YARNResponseDefinition(\n                        \"Updating component (\" + componentName + \") size from \" + originalSize + \" to \" + newSizeInt))\n                  }\n                }\n\n              case (_, _) =>\n                writeResponse(httpExchange, 404, YARNResponseDefinition(\"Invalid request\"))\n            }\n          }\n        } catch {\n          case exception: Throwable =>\n            writeResponse(httpExchange, 500, YARNResponseDefinition(\"Unknown error: \" + exception.getMessage))\n        }\n      })\n  this.server.setExecutor(null) // creates a default executor\n\n  def start(): Unit = {\n    this.server.start()\n  }\n  def stop(): Unit = {\n    this.server.stop(0)\n  }\n  //updates component and service states based on completion-time maps\n  def updateServDef(serviceName: String): Unit = {\n\n    var tempServiceDef = this.services.get(serviceName).orNull\n\n    if (tempServiceDef == null)\n      throw new IllegalArgumentException(\"Invalid serviceName: \" + serviceName)\n\n    if (this.initCompletionTimes(serviceName) < DateTime.now)\n      tempServiceDef = tempServiceDef.copy(state = Some(\"STABLE\"))\n\n    val updatedComponents = tempServiceDef.components.map(comp => {\n      val updatedContainers = comp.containers\n        .getOrElse(List[ContainerDefinition]())\n        .map(container => {\n          if (container.state.equals(\"INIT\") && this.flexCompletionTimes\n                .getOrElse(serviceName, mutable.Map[String, DateTime]())\n                .getOrElse(comp.name, DateTime.MinValue) < DateTime.now) {\n            val newContainer = container.copy(state = \"READY\")\n            newContainer\n          } else\n            container\n        })\n      comp.copy(containers = Option(updatedContainers))\n    })\n    this.services.put(serviceName, tempServiceDef.copy(components = updatedComponents))\n  }\n  //Gets username from query string\n  private def getUserName(httpExchange: HttpExchange): String = {\n    val query = httpExchange.getRequestURI.getQuery\n\n    val props = new util.HashMap[String, String]\n    query\n      .split(\"&\")\n      .foreach(param => {\n        val entry = param.split(\"=\")\n        if (entry.length > 1)\n          props.put(entry(0), entry(1))\n        else\n          props.put(entry(0), \"\")\n      })\n    props.get(\"user.name\")\n  }\n\n  private def writeResponse(t: HttpExchange, code: Int, content: YARNResponseDefinition): Unit = {\n    writeResponse(t, code, content.toJson.compactPrint)\n  }\n  private def writeResponse(t: HttpExchange, code: Int, content: String): Unit = {\n    val bytes = content.getBytes(StandardCharsets.UTF_8)\n    t.sendResponseHeaders(code, bytes.length)\n    val os = t.getResponseBody\n    os.write(bytes)\n    os.close()\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/containerpool/yarn/test/YARNContainerFactoryTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.containerpool.yarn.test\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.DateTime\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.WhiskConfig._\nimport org.apache.openwhisk.core.containerpool.ContainerArgsConfig\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.core.entity.{ByteSize, ExecManifest, InvokerInstanceId, SizeUnits}\nimport org.apache.openwhisk.core.yarn.{YARNConfig, YARNContainerFactory, YARNRESTUtil, YARNTask}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfter, BeforeAndAfterAll, Suite}\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\n\nimport scala.collection.immutable.Map\nimport scala.concurrent.Await\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass YARNContainerFactoryTests\n    extends Suite\n    with BeforeAndAfter\n    with AnyFlatSpecLike\n    with ExecHelpers\n    with BeforeAndAfterAll {\n\n  implicit val whiskConfig: WhiskConfig = new WhiskConfig(\n    ExecManifest.requiredProperties ++ Map(wskApiHostname -> \"apihost\") ++ wskApiHost)\n\n  val customManifest = Some(s\"\"\"\n                               |{ \"runtimes\": {\n                               |    \"runtime1\": [\n                               |      {\n                               |        \"kind\": \"somekind:1\",\n                               |        \"deprecated\": false,\n                               |        \"default\": true,\n                               |        \"image\": {\n                               |          \"registry\": \"local-docker:8888\",\n                               |          \"prefix\": \"openwhisk\",\n                               |          \"name\": \"somekind\",\n                               |          \"tag\": \"latest\"\n                               |        }\n                               |      }\n                               |    ],\n                               |    \"runtime2\": [\n                               |      {\n                               |        \"kind\": \"anotherkind:1\",\n                               |        \"deprecated\": false,\n                               |        \"default\": true,\n                               |        \"image\": {\n                               |          \"registry\": \"local-docker:8888\",\n                               |          \"prefix\": \"openwhisk\",\n                               |          \"name\": \"anotherkind\",\n                               |          \"tag\": \"latest\"\n                               |        }\n                               |      }\n                               |    ]\n                               |  }\n                               |}\n                               |\"\"\".stripMargin)\n  val images = Array(\n    ImageName(\"somekind\", Option(\"local-docker:8888\"), Option(\"openwhisk\"), Some(\"latest\")),\n    ImageName(\"anotherkind\", Option(\"local-docker:8888\"), Option(\"openwhisk\"), Some(\"latest\")))\n  val containerArgsConfig =\n    new ContainerArgsConfig(\n      \"net1\",\n      Seq(\"dns1\", \"dns2\"),\n      Seq.empty,\n      Seq.empty,\n      Seq.empty,\n      Map(\"extra1\" -> Set(\"e1\", \"e2\"), \"extra2\" -> Set(\"e3\", \"e4\")))\n  val yarnConfig =\n    YARNConfig(\n      \"http://localhost:8088\",\n      yarnLinkLogMessage = true,\n      \"openwhisk-action-service\",\n      YARNRESTUtil.SIMPLEAUTH,\n      \"\",\n      \"\",\n      \"default\",\n      \"256\",\n      1)\n  val instance0 = new InvokerInstanceId(0, Some(\"invoker0\"), Some(\"invoker0\"), ByteSize(0, SizeUnits.BYTE))\n  val instance1 = new InvokerInstanceId(1, Some(\"invoker1\"), Some(\"invoker1\"), ByteSize(0, SizeUnits.BYTE))\n  val serviceName0 = yarnConfig.serviceName + \"-0\"\n  val serviceName1 = yarnConfig.serviceName + \"-1\"\n  val properties: Map[String, Set[String]] = Map[String, Set[String]]()\n\n  behavior of \"YARNContainerFactory\"\n\n  override def beforeAll = {\n    ExecManifest.initialize(whiskConfig, customManifest)\n    super.beforeAll()\n  }\n\n  override def afterAll = {\n    super.afterAll()\n    ExecManifest.initialize(whiskConfig)\n  }\n\n  it should \"initialize correctly with zero containers\" in {\n\n    val rm = new MockYARNRM(8088, 1000)\n    rm.start()\n    val factory =\n      new YARNContainerFactory(\n        ActorSystem(),\n        logging,\n        whiskConfig,\n        instance0,\n        properties,\n        containerArgsConfig,\n        yarnConfig)\n    factory.init()\n\n    //Service was created\n    assert(rm.services.contains(serviceName0))\n\n    //Factory waited until service was stable\n    assert(rm.initCompletionTimes.getOrElse(serviceName0, DateTime.MaxValue) < DateTime.now)\n\n    //No containers were created\n    assert(rm.flexCompletionTimes.getOrElse(serviceName0, Map[String, DateTime]()).isEmpty)\n\n    val componentNamesInService = rm.services.get(serviceName0).orNull.components.map(c => c.name)\n    val missingImages = images\n      .map(e => e.name)\n      .filter(imageName => !componentNamesInService.contains(imageName))\n\n    //All images types were created\n    assert(missingImages.isEmpty)\n\n    //All components have zero containers\n    assert(rm.services.get(serviceName0).orNull.components.forall(c => c.number_of_containers.get == 0))\n\n    rm.stop()\n  }\n\n  it should \"create a container\" in {\n    val rm = new MockYARNRM(8088, 1000)\n    rm.start()\n    val factory =\n      new YARNContainerFactory(\n        ActorSystem(),\n        logging,\n        whiskConfig,\n        instance0,\n        properties,\n        containerArgsConfig,\n        yarnConfig)\n    factory.init()\n\n    val imageToCreate = images(0)\n    val containerFuture = factory.createContainer(\n      TransactionId.testing,\n      \"name\",\n      imageToCreate,\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n\n    Await.result(containerFuture, 60.seconds)\n\n    //Container of the correct type was created\n    assert(rm.services.contains(serviceName0))\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToCreate.name))\n        .orNull != null)\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToCreate.name))\n        .orNull\n        .number_of_containers\n        .get == 1)\n\n    //Factory waited for container to be stable\n    assert(\n      rm.flexCompletionTimes\n        .getOrElse(serviceName0, Map[String, DateTime]())\n        .getOrElse(imageToCreate.name, DateTime.MaxValue) < DateTime.now)\n\n    rm.stop()\n  }\n\n  it should \"destroy the correct container\" in {\n    val rm = new MockYARNRM(8088, 1000)\n    rm.start()\n    val factory =\n      new YARNContainerFactory(\n        ActorSystem(),\n        logging,\n        whiskConfig,\n        instance0,\n        properties,\n        containerArgsConfig,\n        yarnConfig)\n    factory.init()\n\n    val imageToDelete = images(0)\n    val imageNotToDelete = images(1)\n\n    val containerFuture1 = factory.createContainer(\n      TransactionId.testing,\n      \"name\",\n      imageNotToDelete,\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n\n    val containerFuture2 = factory.createContainer(\n      TransactionId.testing,\n      \"name\",\n      imageToDelete,\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n\n    val containerFuture3 = factory.createContainer(\n      TransactionId.testing,\n      \"name\",\n      imageToDelete,\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n\n    val containerFuture4 = factory.createContainer(\n      TransactionId.testing,\n      \"name\",\n      imageToDelete,\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n\n    val container1 = Await.result(containerFuture1, 30.seconds)\n    val container2 = Await.result(containerFuture2, 30.seconds)\n    val container3 = Await.result(containerFuture3, 30.seconds)\n    val container4 = Await.result(containerFuture4, 30.seconds)\n\n    //Ensure container was created\n    val containerToRemoveName = container2.asInstanceOf[YARNTask].component_instance_name\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToDelete.name))\n        .orNull\n        .containers\n        .get\n        .map(c => c.component_instance_name)\n        .contains(containerToRemoveName))\n\n    val destroyFuture = container2.destroy()(TransactionId.testing)\n    Await.result(destroyFuture, 30.seconds)\n\n    //Ensure container of the correct type was deleted\n    assert(rm.services.contains(serviceName0))\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageNotToDelete.name))\n        .orNull != null)\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageNotToDelete.name))\n        .orNull\n        .number_of_containers\n        .get == 1)\n\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToDelete.name))\n        .orNull != null)\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToDelete.name))\n        .orNull\n        .number_of_containers\n        .get == 2)\n\n    assert(\n      !rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToDelete.name))\n        .orNull\n        .containers\n        .get\n        .map(c => c.component_instance_name)\n        .contains(containerToRemoveName))\n\n    rm.stop()\n  }\n  it should \"create and destroy multiple containers\" in {\n    val rm = new MockYARNRM(8088, 1000)\n    rm.start()\n    val factory =\n      new YARNContainerFactory(\n        ActorSystem(),\n        logging,\n        whiskConfig,\n        instance0,\n        properties,\n        containerArgsConfig,\n        yarnConfig)\n    factory.init()\n\n    val container1Future = factory.createContainer(\n      TransactionId.testing,\n      \"name\",\n      images(0),\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n\n    val container2Future = factory.createContainer(\n      TransactionId.testing,\n      \"name\",\n      images(1),\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n\n    val container3Future = factory.createContainer(\n      TransactionId.testing,\n      \"name\",\n      images(0),\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n\n    Await.result(container1Future, 30.seconds)\n    val container2 = Await.result(container2Future, 30.seconds)\n    val container3 = Await.result(container3Future, 30.seconds)\n\n    val destroyFuture1 = container2.destroy()(TransactionId.testing)\n    Await.result(destroyFuture1, 30.seconds)\n\n    val destroyFuture2 = container3.destroy()(TransactionId.testing)\n    Await.result(destroyFuture2, 30.seconds)\n\n    //Containers of the correct type was deleted\n    assert(rm.services.contains(serviceName0))\n    assert(rm.services.get(serviceName0).orNull.components.find(c => c.name.equals(images(1).name)).orNull != null)\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(images(1).name))\n        .orNull\n        .number_of_containers\n        .get == 0)\n\n    assert(rm.services.get(serviceName0).orNull.components.find(c => c.name.equals(images(0).name)).orNull != null)\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(images(0).name))\n        .orNull\n        .number_of_containers\n        .get == 1)\n\n    //Factory waited for container to be stable\n    assert(\n      rm.flexCompletionTimes\n        .getOrElse(serviceName0, Map[String, DateTime]())\n        .getOrElse(images(0).name, DateTime.MaxValue) < DateTime.now)\n\n    rm.stop()\n  }\n  it should \"cleanup\" in {\n    val rm = new MockYARNRM(8088, 1000)\n    rm.start()\n    val factory =\n      new YARNContainerFactory(\n        ActorSystem(),\n        logging,\n        whiskConfig,\n        instance0,\n        properties,\n        containerArgsConfig,\n        yarnConfig)\n    factory.init()\n    factory.cleanup()\n\n    //Service was destroyed\n    assert(!rm.services.contains(serviceName0))\n    assert(!rm.initCompletionTimes.contains(serviceName0))\n    assert(!rm.flexCompletionTimes.contains(serviceName0))\n\n    rm.stop()\n  }\n\n  it should \"support HA\" in {\n    val rm = new MockYARNRM(8088, 1000)\n    rm.start()\n    val factory0 =\n      new YARNContainerFactory(\n        ActorSystem(),\n        logging,\n        whiskConfig,\n        instance0,\n        properties,\n        containerArgsConfig,\n        yarnConfig)\n    factory0.init()\n    val factory1 =\n      new YARNContainerFactory(\n        ActorSystem(),\n        logging,\n        whiskConfig,\n        instance1,\n        properties,\n        containerArgsConfig,\n        yarnConfig)\n    factory1.init()\n\n    val imageToCreate = images(0)\n    val containerFuture0 = factory0.createContainer(\n      TransactionId.testing,\n      \"name\",\n      imageToCreate,\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n    val containerFuture1 = factory1.createContainer(\n      TransactionId.testing,\n      \"name\",\n      imageToCreate,\n      unuseduserProvidedImage = true,\n      ByteSize(256, SizeUnits.MB),\n      1,\n      None)\n\n    Await.result(containerFuture0, 60.seconds)\n    Await.result(containerFuture1, 60.seconds)\n\n    //Container of the correct type was created for each invoker instance\n    assert(rm.services.contains(serviceName0))\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToCreate.name))\n        .orNull != null)\n    assert(\n      rm.services\n        .get(serviceName0)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToCreate.name))\n        .orNull\n        .number_of_containers\n        .get == 1)\n\n    assert(rm.services.contains(serviceName1))\n    assert(\n      rm.services\n        .get(serviceName1)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToCreate.name))\n        .orNull != null)\n    assert(\n      rm.services\n        .get(serviceName1)\n        .orNull\n        .components\n        .find(c => c.name.equals(imageToCreate.name))\n        .orNull\n        .number_of_containers\n        .get == 1)\n\n    //Both factories waited for container to be stable\n    assert(\n      rm.flexCompletionTimes\n        .getOrElse(serviceName0, Map[String, DateTime]())\n        .getOrElse(imageToCreate.name, DateTime.MaxValue) < DateTime.now)\n\n    assert(\n      rm.flexCompletionTimes\n        .getOrElse(serviceName1, Map[String, DateTime]())\n        .getOrElse(imageToCreate.name, DateTime.MaxValue) < DateTime.now)\n\n    rm.stop()\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/actions/test/SequenceAccountingTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.actions.test\n\nimport java.time.Instant\n\nimport scala.concurrent.duration.DurationInt\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.WskActorSystem\nimport spray.json._\nimport org.apache.openwhisk.core.controller.actions.SequenceAccounting\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.ActivationResponse\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.apache.openwhisk.http.Messages\n\n@RunWith(classOf[JUnitRunner])\nclass SequenceAccountingTests extends AnyFlatSpec with Matchers with WskActorSystem {\n\n  behavior of \"sequence accounting\"\n\n  val okRes1 = ActivationResponse.success(Some(JsObject(\"res\" -> JsNumber(1))))\n  val okRes2 = ActivationResponse.success(Some(JsObject(\"res\" -> JsNumber(2))))\n  val failedRes = ActivationResponse.applicationError(JsNumber(3))\n\n  val okActivation = WhiskActivation(\n    namespace = EntityPath(\"ns\"),\n    name = EntityName(\"a\"),\n    Subject(),\n    activationId = ActivationId.generate(),\n    start = Instant.now(),\n    end = Instant.now(),\n    response = okRes2,\n    annotations = Parameters(\"limits\", ActionLimits(TimeLimit(1.second), MemoryLimit(128.MB), LogLimit(1.MB)).toJson),\n    duration = Some(123))\n\n  val notOkActivation = WhiskActivation(\n    namespace = EntityPath(\"ns\"),\n    name = EntityName(\"a\"),\n    Subject(),\n    activationId = ActivationId.generate(),\n    start = Instant.now(),\n    end = Instant.now(),\n    response = failedRes,\n    annotations = Parameters(\"limits\", ActionLimits(TimeLimit(11.second), MemoryLimit(256.MB), LogLimit(2.MB)).toJson),\n    duration = Some(234))\n\n  it should \"create initial accounting object\" in {\n    val s = SequenceAccounting(2, okRes1)\n    s.atomicActionCnt shouldBe 2\n    s.previousResponse.get shouldBe okRes1\n    s.logs shouldBe empty\n    s.duration shouldBe 0\n    s.maxMemory shouldBe None\n    s.shortcircuit shouldBe false\n  }\n\n  it should \"resolve maybe to success and update accounting object\" in {\n    val p = SequenceAccounting(2, okRes1)\n    val n1 = p.maybe(okActivation, 3, 5)\n    n1.atomicActionCnt shouldBe 3\n    n1.previousResponse.get shouldBe okRes2\n    n1.logs.length shouldBe 1\n    n1.logs(0) shouldBe okActivation.activationId\n    n1.duration shouldBe 123\n    n1.maxMemory shouldBe Some(128)\n    n1.shortcircuit shouldBe false\n  }\n\n  it should \"resolve maybe and enable short circuit\" in {\n    val p = SequenceAccounting(2, okRes1)\n    val n1 = p.maybe(okActivation, 3, 5)\n    val n2 = n1.maybe(notOkActivation, 4, 5)\n    n2.atomicActionCnt shouldBe 4\n    n2.previousResponse.get shouldBe failedRes\n    n2.logs.length shouldBe 2\n    n2.logs(0) shouldBe okActivation.activationId\n    n2.logs(1) shouldBe notOkActivation.activationId\n    n2.duration shouldBe (123 + 234)\n    n2.maxMemory shouldBe Some(256)\n    n2.shortcircuit shouldBe true\n  }\n\n  it should \"record an activation that exceeds allowed limit but also short circuit\" in {\n    val p = SequenceAccounting(2, okRes1)\n    val n = p.maybe(okActivation, 3, 2)\n    n.atomicActionCnt shouldBe 3\n    n.previousResponse.get shouldBe ActivationResponse.applicationError(Messages.sequenceIsTooLong)\n    n.logs.length shouldBe 1\n    n.logs(0) shouldBe okActivation.activationId\n    n.duration shouldBe 123\n    n.maxMemory shouldBe Some(128)\n    n.shortcircuit shouldBe true\n  }\n\n  it should \"set failed response and short circuit on failure\" in {\n    val p = SequenceAccounting(2, okRes1)\n    val n = p.maybe(okActivation, 3, 3)\n    val f = n.fail(failedRes, None)\n    f.atomicActionCnt shouldBe 3\n    f.previousResponse.get shouldBe failedRes\n    f.logs.length shouldBe 1\n    f.logs(0) shouldBe okActivation.activationId\n    f.duration shouldBe 123\n    f.maxMemory shouldBe Some(128)\n    f.shortcircuit shouldBe true\n  }\n\n  it should \"resolve max memory\" in {\n    SequenceAccounting.maxMemory(None, None) shouldBe None\n    SequenceAccounting.maxMemory(None, Some(1)) shouldBe Some(1)\n    SequenceAccounting.maxMemory(Some(1), None) shouldBe Some(1)\n    SequenceAccounting.maxMemory(Some(1), Some(2)) shouldBe Some(2)\n    SequenceAccounting.maxMemory(Some(2), Some(1)) shouldBe Some(2)\n    SequenceAccounting.maxMemory(Some(2), Some(2)) shouldBe Some(2)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/ActionsApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport java.time.Instant\nimport scala.concurrent.duration.DurationInt\nimport scala.language.postfixOps\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller\nimport org.apache.pekko.http.scaladsl.server.Route\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.controller.WhiskActionsApi\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entitlement.Collection\nimport org.apache.openwhisk.http.ErrorResponse\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.core.database.UserContext\nimport org.apache.pekko.http.scaladsl.model.headers.RawHeader\nimport org.apache.commons.lang3.StringUtils\nimport org.apache.openwhisk.core.connector.ActivationMessage\nimport org.apache.openwhisk.core.entity.Attachments.Inline\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\n/**\n * Tests Actions API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass ActionsApiTests extends ControllerTestCommon with WhiskActionsApi {\n\n  /** Actions API tests */\n  behavior of \"Actions API\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val context = UserContext(creds)\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n\n  def aname() = MakeName.next(\"action_tests\")\n\n  val actionLimit = Exec.sizeLimit\n  val parametersLimit = Parameters.MAX_SIZE\n\n  val systemPayloadLimit = ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT\n  val namespacePayloadLimit = systemPayloadLimit - 100.KB\n\n  val credsWithPayloadLimit =\n    WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(maxPayloadSize = Some(namespacePayloadLimit)))\n\n  //// GET /actions\n  it should \"return empty list when no actions exist\" in {\n    implicit val tid = transid()\n    Get(collectionPath) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      responseAs[List[JsObject]] shouldBe 'empty\n    }\n  }\n\n  it should \"list actions by default namespace\" in {\n    implicit val tid = transid()\n    val actions = (1 to 2).map { i =>\n      WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    }.toList\n    actions foreach {\n      put(entityStore, _)\n    }\n    waitOnView(entityStore, WhiskAction, namespace, 2)\n    Get(collectionPath) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[JsObject]]\n      actions.length should be(response.length)\n      response should contain theSameElementsAs actions.map(_.summaryAsJson)\n    }\n  }\n\n  it should \"reject list when limit is greater than maximum allowed value\" in {\n    implicit val tid = transid()\n    val exceededMaxLimit = Collection.MAX_LIST_LIMIT + 1\n    val response = Get(s\"$collectionPath?limit=$exceededMaxLimit\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listLimitOutOfRange(Collection.ACTIONS, exceededMaxLimit, Collection.MAX_LIST_LIMIT)\n      }\n    }\n  }\n\n  it should \"reject list when limit is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?limit=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.ACTIONS, notAnInteger)\n      }\n    }\n  }\n\n  it should \"reject list when skip is negative\" in {\n    implicit val tid = transid()\n    val negativeSkip = -1\n    val response = Get(s\"$collectionPath?skip=$negativeSkip\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listSkipOutOfRange(Collection.ACTIONS, negativeSkip)\n      }\n    }\n  }\n\n  it should \"reject list when skip is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?skip=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.ACTIONS, notAnInteger)\n      }\n    }\n  }\n\n  // ?docs disabled\n  ignore should \"list action by default namespace with full docs\" in {\n    implicit val tid = transid()\n    val actions = (1 to 2).map { i =>\n      WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    }.toList\n    actions foreach {\n      put(entityStore, _)\n    }\n    waitOnView(entityStore, WhiskAction, namespace, 2)\n    Get(s\"$collectionPath?docs=true\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[WhiskAction]]\n      actions.length should be(response.length)\n      response should contain theSameElementsAs actions.map(_.summaryAsJson)\n    }\n  }\n\n  it should \"list action with explicit namespace\" in {\n    implicit val tid = transid()\n    val actions = (1 to 2).map { i =>\n      WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    }.toList\n    actions foreach {\n      put(entityStore, _)\n    }\n    waitOnView(entityStore, WhiskAction, namespace, 2)\n    Get(s\"/$namespace/${collection.path}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[JsObject]]\n      actions.length should be(response.length)\n      response should contain theSameElementsAs actions.map(_.summaryAsJson)\n    }\n\n    // it should \"reject list action with explicit namespace not owned by subject\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"list should reject request with post\" in {\n    implicit val tid = transid()\n    Post(collectionPath) ~> Route.seal(routes(creds)) ~> check {\n      status should be(MethodNotAllowed)\n    }\n  }\n\n  //// GET /actions/name\n  it should \"get action by name in default namespace\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    put(entityStore, action)\n\n    Get(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response should be(action)\n    }\n  }\n\n  it should \"get action with updated field\" in {\n    implicit val tid = transid()\n\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    put(entityStore, action)\n\n    // `updated` field should be compared with a document in DB\n    val a = get(entityStore, action.docid, WhiskAction)\n\n    Get(s\"/$namespace/${collection.path}/${action.name}?code=false\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val responseJson = responseAs[JsObject]\n      responseJson.fields(\"updated\").convertTo[Long] should be(a.updated.toEpochMilli)\n    }\n\n    Get(s\"/$namespace/${collection.path}/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val responseJson = responseAs[JsObject]\n      responseJson.fields(\"updated\").convertTo[Long] should be(a.updated.toEpochMilli)\n    }\n  }\n\n  it should \"ignore updated field when updating action\" in {\n    implicit val tid = transid()\n\n    val action = WhiskAction(namespace, aname(), jsDefault(\"\"))\n    val dummyUpdated = WhiskEntity.currentMillis().toEpochMilli\n\n    val content = JsObject(\n      \"exec\" -> JsObject(\"code\" -> \"\".toJson, \"kind\" -> action.exec.kind.toJson),\n      \"updated\" -> dummyUpdated.toJson)\n\n    Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response.updated.toEpochMilli should be > dummyUpdated\n    }\n  }\n\n  def getExecPermutations() = {\n    implicit val tid = transid()\n\n    // BlackBox: binary: true, main: bbMain\n    val bbAction1 = WhiskAction(namespace, aname(), bb(\"bb\", \"RHViZWU=\", Some(\"bbMain\")))\n    val bbAction1Content = Map(\"exec\" -> Map(\n      \"kind\" -> Exec.BLACKBOX,\n      \"code\" -> \"RHViZWU=\",\n      \"image\" -> \"bb\",\n      \"main\" -> \"bbMain\")).toJson.asJsObject\n    val bbAction1ExecMetaData = blackBoxMetaData(\"bb\", Some(\"bbMain\"), true)\n\n    // BlackBox: binary: false, main: bbMain\n    val bbAction2 = WhiskAction(namespace, aname(), bb(\"bb\", \"\", Some(\"bbMain\")))\n    val bbAction2Content =\n      Map(\"exec\" -> Map(\"kind\" -> Exec.BLACKBOX, \"code\" -> \"\", \"image\" -> \"bb\", \"main\" -> \"bbMain\")).toJson.asJsObject\n    val bbAction2ExecMetaData = blackBoxMetaData(\"bb\", Some(\"bbMain\"), false)\n\n    // BlackBox: binary: true, no main\n    val bbAction3 = WhiskAction(namespace, aname(), bb(\"bb\", \"RHViZWU=\"))\n    val bbAction3Content =\n      Map(\"exec\" -> Map(\"kind\" -> Exec.BLACKBOX, \"code\" -> \"RHViZWU=\", \"image\" -> \"bb\")).toJson.asJsObject\n    val bbAction3ExecMetaData = blackBoxMetaData(\"bb\", None, true)\n\n    // BlackBox: binary: false, no main\n    val bbAction4 = WhiskAction(namespace, aname(), bb(\"bb\", \"\"))\n    val bbAction4Content = Map(\"exec\" -> Map(\"kind\" -> Exec.BLACKBOX, \"code\" -> \"\", \"image\" -> \"bb\")).toJson.asJsObject\n    val bbAction4ExecMetaData = blackBoxMetaData(\"bb\", None, false)\n\n    // Attachment: binary: true, main: javaMain\n    val javaAction1 = WhiskAction(namespace, aname(), javaDefault(\"RHViZWU=\", Some(\"javaMain\")))\n    val javaAction1Content =\n      Map(\"exec\" -> Map(\"kind\" -> JAVA_DEFAULT, \"code\" -> \"RHViZWU=\", \"main\" -> \"javaMain\")).toJson.asJsObject\n    val javaAction1ExecMetaData = javaMetaData(Some(\"javaMain\"), true)\n\n    // String: binary: true, main: jsMain\n    val jsAction1 = WhiskAction(namespace, aname(), jsDefault(\"RHViZWU=\", Some(\"jsMain\")))\n    val jsAction1Content =\n      Map(\"exec\" -> Map(\"kind\" -> NODEJS, \"code\" -> \"RHViZWU=\", \"main\" -> \"jsMain\")).toJson.asJsObject\n    val jsAction1ExecMetaData = jsMetaData(Some(\"jsMain\"), true)\n\n    // String: binary: false, main: jsMain\n    val jsAction2 = WhiskAction(namespace, aname(), jsDefault(\"\", Some(\"jsMain\")))\n    val jsAction2Content = Map(\"exec\" -> Map(\"kind\" -> NODEJS, \"code\" -> \"\", \"main\" -> \"jsMain\")).toJson.asJsObject\n    val jsAction2ExecMetaData = jsMetaData(Some(\"jsMain\"), false)\n\n    // String: binary: true, no main\n    val jsAction3 = WhiskAction(namespace, aname(), jsDefault(\"RHViZWU=\"))\n    val jsAction3Content = Map(\"exec\" -> Map(\"kind\" -> NODEJS, \"code\" -> \"RHViZWU=\")).toJson.asJsObject\n    val jsAction3ExecMetaData = jsMetaData(None, true)\n\n    // String: binary: false, no main\n    val jsAction4 = WhiskAction(namespace, aname(), jsDefault(\"\"))\n    val jsAction4Content = Map(\"exec\" -> Map(\"kind\" -> NODEJS, \"code\" -> \"\")).toJson.asJsObject\n    val jsAction4ExecMetaData = jsMetaData(None, false)\n\n    // Sequence\n    val component = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    put(entityStore, component)\n    val components = Vector(s\"/$namespace/${component.name}\").map(stringToFullyQualifiedName(_))\n    val seqAction = WhiskAction(namespace, aname(), sequence(components), seqParameters(components))\n    val seqActionContent = JsObject(\n      \"exec\" -> JsObject(\"kind\" -> \"sequence\".toJson, \"components\" -> JsArray(s\"/$namespace/${component.name}\".toJson)))\n    val seqActionExecMetaData = sequenceMetaData(components)\n\n    Seq(\n      (bbAction1, bbAction1Content, bbAction1ExecMetaData),\n      (bbAction2, bbAction2Content, bbAction2ExecMetaData),\n      (bbAction3, bbAction3Content, bbAction3ExecMetaData),\n      (bbAction4, bbAction4Content, bbAction4ExecMetaData),\n      (javaAction1, javaAction1Content, javaAction1ExecMetaData),\n      (jsAction1, jsAction1Content, jsAction1ExecMetaData),\n      (jsAction2, jsAction2Content, jsAction2ExecMetaData),\n      (jsAction3, jsAction3Content, jsAction3ExecMetaData),\n      (jsAction4, jsAction4Content, jsAction4ExecMetaData),\n      (seqAction, seqActionContent, seqActionExecMetaData))\n  }\n\n  it should \"get action using code query parameter\" in {\n    implicit val tid = transid()\n\n    getExecPermutations.foreach {\n      case (action, content, execMetaData) =>\n        val expectedWhiskAction = WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(action.exec.kind))\n\n        val expectedWhiskActionMetaData = WhiskActionMetaData(\n          action.namespace,\n          action.name,\n          execMetaData,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(action.exec.kind))\n\n        Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(response, expectedWhiskAction)\n        }\n\n        Get(s\"$collectionPath/${action.name}?code=false\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val responseJson = responseAs[JsObject]\n          responseJson.fields(\"exec\").asJsObject.fields should not(contain key \"code\")\n          val response = responseAs[WhiskActionMetaData]\n          checkWhiskEntityResponse(response, expectedWhiskActionMetaData)\n        }\n\n        Seq(s\"$collectionPath/${action.name}\", s\"$collectionPath/${action.name}?code=true\").foreach { path =>\n          Get(path) ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[WhiskAction]\n            checkWhiskEntityResponse(response, expectedWhiskAction)\n          }\n        }\n\n        Delete(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(response, expectedWhiskAction)\n        }\n    }\n  }\n\n  it should \"report NotFound for get non existent action\" in {\n    implicit val tid = transid()\n    Get(s\"$collectionPath/xyz\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"report Conflict if the name was of a different type\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname())\n    put(entityStore, trigger)\n    Get(s\"/$namespace/${collection.path}/${trigger.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Conflict)\n    }\n  }\n\n  it should \"reject long entity names\" in {\n    implicit val tid = transid()\n    val longName = \"a\" * (EntityName.ENTITY_NAME_MAX_LENGTH + 1)\n    Get(s\"/$longName/${collection.path}/$longName\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] shouldBe {\n        Messages.entityNameTooLong(\n          SizeError(namespaceDescriptionForSizeError, longName.length.B, EntityName.ENTITY_NAME_MAX_LENGTH.B))\n      }\n    }\n\n    Seq(\n      s\"/$namespace/${collection.path}/$longName\",\n      s\"/$namespace/${collection.path}/pkg/$longName\",\n      s\"/$namespace/${collection.path}/$longName/a\",\n      s\"/$namespace/${collection.path}/$longName/$longName\").foreach { p =>\n      Get(p) ~> Route.seal(routes(creds)) ~> check {\n        status should be(BadRequest)\n        responseAs[String] shouldBe {\n          Messages.entityNameTooLong(\n            SizeError(segmentDescriptionForSizeError, longName.length.B, EntityName.ENTITY_NAME_MAX_LENGTH.B))\n        }\n      }\n    }\n  }\n\n  //// DEL /actions/name\n  it should \"delete action by name\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    put(entityStore, action)\n\n    // it should \"reject delete action by name not owned by subject\" in\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Delete(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response should be(action)\n    }\n  }\n\n  it should \"report NotFound for delete non existent action\" in {\n    implicit val tid = transid()\n    Delete(s\"$collectionPath/xyz\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  //// PUT /actions/name\n  it should \"put should reject request missing json content\" in {\n    implicit val tid = transid()\n    Put(s\"$collectionPath/xxx\", \"\") ~> Route.seal(routes(creds)) ~> check {\n      val response = responseAs[String]\n      status should be(UnsupportedMediaType)\n    }\n  }\n\n  it should \"put should reject request missing property exec\" in {\n    implicit val tid = transid()\n    val content = \"\"\"|{\"name\":\"name\",\"publish\":true}\"\"\".stripMargin.parseJson.asJsObject\n    Put(s\"$collectionPath/xxx\", content) ~> Route.seal(routes(creds)) ~> check {\n      val response = responseAs[String]\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"put should reject request with malformed property exec\" in {\n    implicit val tid = transid()\n    val content =\n      \"\"\"|{\"name\":\"name\",\n         |\"publish\":true,\n         |\"exec\":\"\"}\"\"\".stripMargin.parseJson.asJsObject\n    Put(s\"$collectionPath/xxx\", content) ~> Route.seal(routes(creds)) ~> check {\n      val response = responseAs[String]\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"reject create with exec which is too big\" in {\n    implicit val tid = transid()\n    val code = \"a\" * (actionLimit.toBytes.toInt + 1)\n    val exec: Exec = jsDefault(code)\n    val content = JsObject(\"exec\" -> exec.toJson)\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskAction.execFieldName, exec.size, Exec.sizeLimit))\n      }\n    }\n  }\n\n  it should \"reject exec with unknown or missing kind\" in {\n    implicit val tid = transid()\n    Seq(\"\", \"foobar\").foreach { kind =>\n      val content = s\"\"\"{\"exec\":{\"kind\": \"$kind\", \"code\":\"??\"}}\"\"\".stripMargin.parseJson.asJsObject\n      Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n        status should be(BadRequest)\n        responseAs[String] should include {\n          Messages.invalidRuntimeError(kind, Set.empty).dropRight(3)\n        }\n      }\n    }\n  }\n\n  it should \"reject update with exec which is too big\" in {\n    implicit val tid = transid()\n    val oldCode = \"function main()\"\n    val code = \"a\" * (actionLimit.toBytes.toInt + 1)\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val exec: Exec = jsDefault(code)\n    val content = JsObject(\"exec\" -> exec.toJson)\n    put(entityStore, action)\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskAction.execFieldName, exec.size, Exec.sizeLimit))\n      }\n    }\n  }\n\n  it should \"reject create with parameters which are too big\" in {\n    implicit val tid = transid()\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val parameters = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val content = s\"\"\"{\"exec\":{\"kind\":\"nodejs:default\",\"code\":\"??\"},\"parameters\":$parameters}\"\"\".stripMargin\n    Put(s\"$collectionPath/${aname()}\", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.paramsFieldName, parameters.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"allow create with parameters that do not exceed the namespace limit\" in {\n    implicit val tid = transid()\n\n    val namespaceLimit = Parameters.sizeLimit - 1.KB\n    val parameters = Parameters(\"a\", \"a\" * (namespaceLimit.toBytes.toInt - 10))\n    val credsWithLimits = creds.copy(limits = UserLimits(maxParameterSize = Some(namespaceLimit)))\n\n    val content = s\"\"\"{\"exec\":{\"kind\":\"nodejs:default\",\"code\":\"??\"},\"parameters\":$parameters}\"\"\".stripMargin\n    Put(s\"$collectionPath/${aname()}\", content.parseJson.asJsObject) ~> Route.seal(routes(credsWithLimits)) ~> check {\n      status should be(OK)\n    }\n  }\n\n  it should \"reject create with parameters that exceed the namespace limit\" in {\n    implicit val tid = transid()\n\n    val namespaceLimit = Parameters.sizeLimit - 1.KB\n    val parameters = Parameters(\"a\", \"a\" * (namespaceLimit.toBytes.toInt + 10))\n    val credsWithLimits = creds.copy(limits = UserLimits(maxParameterSize = Some(namespaceLimit)))\n\n    val content = s\"\"\"{\"exec\":{\"kind\":\"nodejs:default\",\"code\":\"??\"},\"parameters\":$parameters}\"\"\".stripMargin\n    Put(s\"$collectionPath/${aname()}\", content.parseJson.asJsObject) ~> Route.seal(routes(credsWithLimits)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.paramsFieldName, parameters.size, namespaceLimit))\n      }\n    }\n  }\n\n  it should \"reject create with annotations which are too big\" in {\n    implicit val tid = transid()\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val annotations = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val content = s\"\"\"{\"exec\":{\"kind\":\"nodejs:default\",\"code\":\"??\"},\"annotations\":$annotations}\"\"\".stripMargin\n    Put(s\"$collectionPath/${aname()}\", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.annotationsFieldName, annotations.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"allow create with annotations that do not exceed the namespace limit\" in {\n    implicit val tid = transid()\n\n    val namespaceLimit = Parameters.sizeLimit - 1.KB\n    val annotations = Parameters(\"a\", \"a\" * (namespaceLimit.toBytes.toInt - 10))\n    val credsWithLimits = creds.copy(limits = UserLimits(maxParameterSize = Some(namespaceLimit)))\n\n    val content = s\"\"\"{\"exec\":{\"kind\":\"nodejs:default\",\"code\":\"??\"},\"annotations\":$annotations}\"\"\".stripMargin\n    Put(s\"$collectionPath/${aname()}\", content.parseJson.asJsObject) ~> Route.seal(routes(credsWithLimits)) ~> check {\n      status should be(OK)\n    }\n  }\n\n  it should \"reject create with annotations that exceed the namespace limit\" in {\n    implicit val tid = transid()\n\n    val namespaceLimit = Parameters.sizeLimit - 1.KB\n    val annotations = Parameters(\"a\", \"a\" * (namespaceLimit.toBytes.toInt + 10))\n    val credsWithLimits = creds.copy(limits = UserLimits(maxParameterSize = Some(namespaceLimit)))\n\n    val content = s\"\"\"{\"exec\":{\"kind\":\"nodejs:default\",\"code\":\"??\"},\"annotations\":$annotations}\"\"\".stripMargin\n    Put(s\"$collectionPath/${aname()}\", content.parseJson.asJsObject) ~> Route.seal(routes(credsWithLimits)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.annotationsFieldName, annotations.size, namespaceLimit))\n      }\n    }\n  }\n\n  it should \"reject create when memory is greater than maximum allowed namespace limit\" in {\n    implicit val tid = transid()\n\n    val allowed = ByteSize(128, SizeUnits.MB)\n    val is = ByteSize(512, SizeUnits.MB)\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(maxActionMemory = Some(MemoryLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(is)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.sizeExceedsAllowedThreshold(MemoryLimit.memoryLimitFieldName, is.toMB.toInt, allowed.toMB.toInt)\n      }\n    }\n  }\n\n  it should \"reject create if exceeds the system memory limit and indicate namespace limit in message\" in {\n    implicit val tid = transid()\n\n    val allowed = MemoryLimit.MAX_MEMORY_DEFAULT - 1.MB // namespace limit\n    val is = MemoryLimit.MAX_MEMORY + 1.MB\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(maxActionMemory = Some(MemoryLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(is)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.sizeExceedsAllowedThreshold(MemoryLimit.memoryLimitFieldName, is.toMB.toInt, allowed.toMB.toInt)\n      }\n    }\n  }\n\n  it should \"reject create when memory is less than minimum allowed namespace limit\" in {\n    implicit val tid = transid()\n\n    val allowed = ByteSize(512, SizeUnits.MB)\n    val is = ByteSize(128, SizeUnits.MB)\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(minActionMemory = Some(MemoryLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(is)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.sizeBelowAllowedThreshold(MemoryLimit.memoryLimitFieldName, is.toMB.toInt, allowed.toMB.toInt)\n      }\n    }\n  }\n\n  it should \"reject create when log size is greater than maximum allowed namespace limit\" in {\n    implicit val tid = transid()\n\n    val allowed = ByteSize(5, SizeUnits.MB)\n    val is = ByteSize(7, SizeUnits.MB)\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(maxActionLogs = Some(LogLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(is)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.sizeExceedsAllowedThreshold(LogLimit.logLimitFieldName, is.toMB.toInt, allowed.toMB.toInt)\n      }\n    }\n  }\n\n  it should \"reject create if exceeds the system log size limit and indicate namespace limit in message\" in {\n    implicit val tid = transid()\n\n    val allowed = LogLimit.MAX_LOGSIZE_DEFAULT - 1.MB\n    val is = LogLimit.MAX_LOGSIZE + 1.MB\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(maxActionLogs = Some(LogLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(is)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.sizeExceedsAllowedThreshold(LogLimit.logLimitFieldName, is.toMB.toInt, allowed.toMB.toInt)\n      }\n    }\n  }\n\n  it should \"reject create when log size is less than minimum allowed namespace limit\" in {\n    implicit val tid = transid()\n\n    val allowed = ByteSize(3, SizeUnits.MB)\n    val is = ByteSize(1, SizeUnits.MB)\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(minActionLogs = Some(LogLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(is)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.sizeBelowAllowedThreshold(LogLimit.logLimitFieldName, is.toMB.toInt, allowed.toMB.toInt)\n      }\n    }\n  }\n\n  it should \"reject create when timeout is greater than maximum allowed namespace limit\" in {\n    implicit val tid = transid()\n\n    val allowed = TimeLimit.MAX_DURATION.minus(2.second)\n    val is = TimeLimit.MAX_DURATION.minus(1.second)\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(maxActionTimeout = Some(TimeLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(is)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.durationExceedsAllowedThreshold(TimeLimit.timeLimitFieldName, is, allowed)\n      }\n    }\n  }\n\n  it should \"reject create if exceeds the system timeout limit and indicate namespace limit in message\" in {\n    implicit val tid = transid()\n\n    val allowed = TimeLimit.MAX_DURATION_DEFAULT.minus(2.second)\n    val is = TimeLimit.MAX_DURATION.plus(1 second)\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(maxActionTimeout = Some(TimeLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(is)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.durationExceedsAllowedThreshold(TimeLimit.timeLimitFieldName, is, allowed)\n      }\n    }\n  }\n\n  it should \"reject create when timeout is less than minimum allowed namespace limit\" in {\n    implicit val tid = transid()\n\n    val allowed = TimeLimit.MIN_DURATION.plus(2.second)\n    val is = TimeLimit.MIN_DURATION.plus(1.second)\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(minActionTimeout = Some(TimeLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(is)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.durationBelowAllowedThreshold(TimeLimit.timeLimitFieldName, is, allowed)\n      }\n    }\n  }\n\n  it should \"reject create when max concurrency is greater than maximum allowed namespace limit\" in {\n    implicit val tid = transid()\n\n    val allowed = IntraConcurrencyLimit.MAX_CONCURRENT - 2\n    val is = IntraConcurrencyLimit.MAX_CONCURRENT - 1\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(maxActionConcurrency = Some(IntraConcurrencyLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(is)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.concurrencyExceedsAllowedThreshold(is, allowed)\n      }\n    }\n  }\n\n  it should \"reject create if exceeds the system max concurrency limit and indicate namespace limit in message\" in {\n    implicit val tid = transid()\n\n    val allowed = IntraConcurrencyLimit.MAX_CONCURRENT_DEFAULT - 1\n    val is = IntraConcurrencyLimit.MAX_CONCURRENT + 1\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(maxActionConcurrency = Some(IntraConcurrencyLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(is)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.concurrencyExceedsAllowedThreshold(is, allowed)\n      }\n    }\n  }\n\n  it should \"reject create when max concurrency is less than minimum allowed namespace limit\" in {\n    implicit val tid = transid()\n\n    val allowed = IntraConcurrencyLimit.MIN_CONCURRENT + 2\n    val is = IntraConcurrencyLimit.MIN_CONCURRENT + 1\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(minActionConcurrency = Some(IntraConcurrencyLimit(allowed))))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(is)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.concurrencyBelowAllowedThreshold(is, allowed)\n      }\n    }\n  }\n\n  it should \"reject create when max instance concurrency is greater than namespace's concurrency\" in {\n    implicit val tid = transid()\n\n    val credsWithNamespaceLimits = WhiskAuthHelpers\n      .newIdentity()\n      .copy(limits = UserLimits(concurrentInvocations = Some(30)))\n\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(ActionLimitsOption(None, None, None, None, Some(InstanceConcurrencyLimit(40)))))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(credsWithNamespaceLimits)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.maxActionInstanceConcurrencyExceedsNamespace(30)\n      }\n    }\n  }\n\n  it should \"reject activation with entity which is too big\" in {\n    implicit val tid = transid()\n    val code = \"a\" * (systemPayloadLimit.toBytes.toInt + 1)\n    val content = s\"\"\"{\"a\":\"$code\"}\"\"\".stripMargin\n    Post(s\"$collectionPath/${aname()}\", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(fieldDescriptionForSizeError, (content.length).B, systemPayloadLimit.toBytes.B))\n      }\n    }\n  }\n\n  it should \"reject activation with entity size exceeds allowed namespace limit\" in {\n    implicit val tid = transid()\n    val code = \"a\" * (namespacePayloadLimit.toBytes.toInt + 1)\n    val content = s\"\"\"{\"a\":\"$code\"}\"\"\".stripMargin\n    Post(s\"$collectionPath/${aname()}\", content.parseJson.asJsObject) ~> Route.seal(routes(credsWithPayloadLimit)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(\n          SizeError(fieldDescriptionForSizeError, (content.length).B, namespacePayloadLimit.toBytes.B))\n      }\n    }\n  }\n\n  it should \"put should accept request with missing optional properties\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"\"))\n    // only a kind must be defined (code otherwise could be empty)\n    val content = JsObject(\"exec\" -> JsObject(\"code\" -> \"\".toJson, \"kind\" -> action.exec.kind.toJson))\n    Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(NODEJS)))\n    }\n  }\n\n  it should \"put should accept blackbox exec with empty code property\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), bb(\"bb\"))\n    val content = Map(\"exec\" -> Map(\"kind\" -> \"blackbox\", \"code\" -> \"\", \"image\" -> \"bb\")).toJson.asJsObject\n    Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(BLACKBOX)))\n      response.exec shouldBe an[BlackBoxExec]\n      response.exec.asInstanceOf[BlackBoxExec].code shouldBe empty\n    }\n  }\n\n  it should \"put should accept blackbox exec with non-empty code property\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), bb(\"bb\", \"cc\"))\n    val content = Map(\"exec\" -> Map(\"kind\" -> \"blackbox\", \"code\" -> \"cc\", \"image\" -> \"bb\")).toJson.asJsObject\n    Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(BLACKBOX)))\n      response.exec shouldBe an[BlackBoxExec]\n      val bb = response.exec.asInstanceOf[BlackBoxExec]\n      bb.code shouldBe Some(Inline(\"cc\"))\n      bb.binary shouldBe false\n    }\n  }\n\n  // this test is to ensure pre-existing actions can continue to to opt-out of new system annotations\n  it should \"preserve annotations on pre-existing actions\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"\"))\n    put(entityStore, action, false) // install the action into the database directly\n\n    var content = JsObject(\"exec\" -> JsObject(\"code\" -> \"\".toJson, \"kind\" -> action.exec.kind.toJson))\n\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version.upPatch,\n          action.publish,\n          action.annotations ++ Parameters(WhiskAction.execFieldName, action.exec.kind)))\n    }\n\n    content = \"\"\"{\"annotations\":[{\"key\":\"a\",\"value\":\"B\"}]}\"\"\".parseJson.asJsObject\n\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version.upPatch.upPatch,\n          action.publish,\n          action.annotations ++ Parameters(\"a\", \"B\") ++ Parameters(WhiskAction.execFieldName, action.exec.kind)))\n    }\n  }\n\n  private implicit val fqnSerdes = FullyQualifiedEntityName.serdes\n\n  private def seqParameters(seq: Vector[FullyQualifiedEntityName]) =\n    Parameters(\"_actions\", seq.map(\"/\" + _.asString).toJson)\n\n  // this test is sneaky; the installation of the sequence is done directly in the db\n  // and api checks are skipped\n  it should \"reset parameters when changing sequence action to non sequence\" in {\n    implicit val tid = transid()\n    val components = Vector(\"x/a\", \"x/b\").map(stringToFullyQualifiedName(_))\n    val action = WhiskAction(namespace, aname(), sequence(components), seqParameters(components))\n    val content = WhiskActionPut(Some(jsDefault(\"\")))\n    put(entityStore, action, false)\n\n    // create an action sequence\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response.exec.kind should be(NODEJS)\n      response.parameters shouldBe Parameters()\n    }\n  }\n\n  // this test is sneaky; the installation of the sequence is done directly in the db\n  // and api checks are skipped\n  it should \"preserve new parameters when changing sequence action to non sequence\" in {\n    implicit val tid = transid()\n    val components = Vector(\"x/a\", \"x/b\").map(stringToFullyQualifiedName(_))\n    val action = WhiskAction(namespace, aname(), sequence(components), seqParameters(components))\n    val content = WhiskActionPut(Some(jsDefault(\"\")), parameters = Some(Parameters(\"a\", \"A\")))\n    put(entityStore, action, false)\n\n    // create an action sequence\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response.exec.kind should be(NODEJS)\n      response.parameters should be(Parameters(\"a\", \"A\"))\n    }\n  }\n\n  it should \"put should accept request with parameters property\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    val content = WhiskActionPut(Some(action.exec), Some(action.parameters))\n\n    // it should \"reject put action in namespace not owned by subject\" in\n    val auser = WhiskAuthHelpers.newIdentity()\n    Put(s\"/$namespace/${collection.path}/${action.name}\", content) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(NODEJS)))\n    }\n  }\n\n  it should \"put should reject request with parameters property as jsobject\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    val content = WhiskActionPut(Some(action.exec), Some(action.parameters))\n    val params = \"\"\"{ \"parameters\": { \"a\": \"b\" } }\"\"\".parseJson.asJsObject\n    val json = JsObject(WhiskActionPut.serdes.write(content).asJsObject.fields ++ params.fields)\n    Put(s\"$collectionPath/${action.name}\", json) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"put should accept request with limits property\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    val content = WhiskActionPut(\n      Some(action.exec),\n      Some(action.parameters),\n      Some(\n        ActionLimitsOption(\n          Some(action.limits.timeout),\n          Some(action.limits.memory),\n          Some(action.limits.logs),\n          Some(action.limits.concurrency))))\n    Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(NODEJS)))\n    }\n  }\n\n  it should \"put and then get an action from cache\" in {\n    implicit val tid = transid()\n    val javaAction =\n      WhiskAction(namespace, aname(), javaDefault(\"ZHViZWU=\", Some(\"hello\")), annotations = Parameters(\"exec\", \"java\"))\n    val nodeAction = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    val actions = Seq((javaAction, JAVA_DEFAULT), (nodeAction, NODEJS))\n\n    actions.foreach {\n      case (action, kind) =>\n        val content = WhiskActionPut(\n          Some(action.exec),\n          Some(action.parameters),\n          Some(\n            ActionLimitsOption(\n              Some(action.limits.timeout),\n              Some(action.limits.memory),\n              Some(action.limits.logs),\n              Some(action.limits.concurrency))))\n\n        // first request invalidates any previous entries and caches new result\n        Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n        stream.toString should include(s\"caching ${CacheKey(action)}\")\n        stream.toString should not include (s\"invalidating ${CacheKey(action)} on delete\")\n        stream.reset()\n\n        // second request should fetch from cache\n        Get(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n        stream.toString should include(s\"serving from cache: ${CacheKey(action)}\")\n        stream.reset()\n\n        // update should invalidate cache\n        Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version.upPatch,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n        stream.toString should include(s\"entity exists, will try to update '$action'\")\n        stream.toString should include(s\"invalidating ${CacheKey(action)}\")\n        stream.toString should include(s\"caching ${CacheKey(action)}\")\n        stream.reset()\n\n        // delete should invalidate cache\n        Delete(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version.upPatch,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n        stream.toString should include(s\"invalidating ${CacheKey(action)}\")\n        stream.reset()\n    }\n  }\n\n  it should \"put and then get an action with attachment from cache\" in {\n    val javaAction =\n      WhiskAction(\n        namespace,\n        aname(),\n        javaDefault(nonInlinedCode(entityStore), Some(\"hello\")),\n        annotations = Parameters(\"exec\", \"java\"))\n    val nodeAction = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters(\"x\", \"b\"))\n    val swiftAction = WhiskAction(namespace, aname(), swift(nonInlinedCode(entityStore)), Parameters(\"x\", \"b\"))\n    val bbAction = WhiskAction(namespace, aname(), bb(\"bb\", nonInlinedCode(entityStore), Some(\"bbMain\")))\n    val actions = Seq((javaAction, JAVA_DEFAULT), (nodeAction, NODEJS), (swiftAction, SWIFT5), (bbAction, BLACKBOX))\n\n    actions.foreach {\n      case (action, kind) =>\n        val content = WhiskActionPut(\n          Some(action.exec),\n          Some(action.parameters),\n          Some(\n            ActionLimitsOption(\n              Some(action.limits.timeout),\n              Some(action.limits.memory),\n              Some(action.limits.logs),\n              Some(action.limits.concurrency))))\n        val cacheKey = s\"${CacheKey(action)}\".replace(\"(\", \"\\\\(\").replace(\")\", \"\\\\)\")\n\n        val expectedPutLog =\n          Seq(\n            s\"uploading attachment '[\\\\w-]+' of document 'id: ${action.namespace}/${action.name}\",\n            s\"caching $cacheKey\")\n            .mkString(\"(?s).*\")\n        val notExpectedGetLog = Seq(\n          s\"finding document: 'id: ${action.namespace}/${action.name}\",\n          s\"finding attachment '[\\\\w-/:]+' of document 'id: ${action.namespace}/${action.name}\").mkString(\"(?s).*\")\n\n        // first request invalidates any previous entries and caches new result\n        Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n\n        stream.toString should not include (s\"invalidating ${CacheKey(action)} on delete\")\n        stream.toString should include regex (expectedPutLog)\n        stream.reset()\n\n        // second request should fetch from cache\n        Get(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n        stream.toString should include(s\"serving from cache: ${CacheKey(action)}\")\n        stream.toString should not include regex(notExpectedGetLog)\n        stream.reset()\n\n        // delete should invalidate cache\n        Delete(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n\n        stream.toString should include(s\"invalidating ${CacheKey(action)}\")\n        stream.reset()\n    }\n  }\n\n  it should \"put and then get an action with inlined attachment\" in {\n    assumeAttachmentInliningEnabled(entityStore)\n    val action =\n      WhiskAction(\n        namespace,\n        aname(),\n        javaDefault(encodedRandomBytes(inlinedAttachmentSize(entityStore)), Some(\"hello\")),\n        annotations = Parameters(\"exec\", \"java\"))\n    val content = WhiskActionPut(\n      Some(action.exec),\n      Some(action.parameters),\n      Some(\n        ActionLimitsOption(\n          Some(action.limits.timeout),\n          Some(action.limits.memory),\n          Some(action.limits.logs),\n          Some(action.limits.concurrency))))\n    val name = action.name\n    val cacheKey = s\"${CacheKey(action)}\".replace(\"(\", \"\\\\(\").replace(\")\", \"\\\\)\")\n    val notExpectedGetLog = Seq(\n      s\"finding document: 'id: ${action.namespace}/${action.name}\",\n      s\"finding attachment '[\\\\w-/:]+' of document 'id: ${action.namespace}/${action.name}\").mkString(\"(?s).*\")\n\n    // first request invalidates any previous entries and caches new result\n    Put(s\"$collectionPath/$name\", content) ~> Route.seal(routes(creds)(transid())) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(JAVA_DEFAULT)))\n    }\n\n    stream.toString should not include (s\"invalidating ${CacheKey(action)} on delete\")\n    stream.toString should not include (\"uploading attachment\")\n    stream.reset()\n\n    // second request should fetch from cache\n    Get(s\"$collectionPath/$name\") ~> Route.seal(routes(creds)(transid())) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(JAVA_DEFAULT)))\n    }\n\n    stream.toString should include(s\"serving from cache: ${CacheKey(action)}\")\n    stream.toString should not include regex(notExpectedGetLog)\n    stream.reset()\n\n    // delete should invalidate cache\n    Delete(s\"$collectionPath/$name\") ~> Route.seal(routes(creds)(transid())) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(JAVA_DEFAULT)))\n    }\n    stream.toString should include(s\"invalidating ${CacheKey(action)}\")\n    stream.reset()\n  }\n\n  it should \"get an action with attachment that is not cached\" in {\n    implicit val tid = transid()\n    val nodeAction = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters(\"x\", \"b\"))\n    val swiftAction = WhiskAction(namespace, aname(), swift(nonInlinedCode(entityStore)), Parameters(\"x\", \"b\"))\n    val bbAction = WhiskAction(namespace, aname(), bb(\"bb\", nonInlinedCode(entityStore), Some(\"bbMain\")))\n    val actions = Seq((nodeAction, NODEJS), (swiftAction, SWIFT5), (bbAction, BLACKBOX))\n\n    actions.foreach {\n      case (action, kind) =>\n        val content = WhiskActionPut(\n          Some(action.exec),\n          Some(action.parameters),\n          Some(\n            ActionLimitsOption(\n              Some(action.limits.timeout),\n              Some(action.limits.memory),\n              Some(action.limits.logs),\n              Some(action.limits.concurrency))))\n        val name = action.name\n        val cacheKey = s\"${CacheKey(action)}\".replace(\"(\", \"\\\\(\").replace(\")\", \"\\\\)\")\n        val expectedGetLog = Seq(\n          s\"finding document: 'id: ${action.namespace}/${action.name}\",\n          s\"finding attachment '[\\\\w-/:]+' of document 'id: ${action.namespace}/${action.name}\").mkString(\"(?s).*\")\n\n        Put(s\"$collectionPath/$name\", content) ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n        }\n\n        removeFromCache(action, WhiskAction)\n\n        // second request should not fetch from cache\n        Get(s\"$collectionPath/$name\") ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n\n        stream.toString should include regex (expectedGetLog)\n        stream.reset()\n    }\n  }\n\n  it should \"concurrently get an action with attachment that is not cached\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters(\"x\", \"b\"))\n    val kind = NODEJS\n\n    val content = WhiskActionPut(\n      Some(action.exec),\n      Some(action.parameters),\n      Some(\n        ActionLimitsOption(\n          Some(action.limits.timeout),\n          Some(action.limits.memory),\n          Some(action.limits.logs),\n          Some(action.limits.concurrency))))\n    val name = action.name\n    val cacheKey = s\"${CacheKey(action)}\".replace(\"(\", \"\\\\(\").replace(\")\", \"\\\\)\")\n    val expectedGetLog = Seq(\n      s\"finding document: 'id: ${action.namespace}/${action.name}\",\n      s\"finding attachment '[\\\\w-/:]+' of document 'id: ${action.namespace}/${action.name}\").mkString(\"(?s).*\")\n\n    Put(s\"$collectionPath/$name\", content) ~> Route.seal(routes(creds)(transid())) ~> check {\n      status should be(OK)\n    }\n\n    removeFromCache(action, WhiskAction)\n\n    stream.reset()\n\n    val expectedAction = WhiskAction(\n      action.namespace,\n      action.name,\n      action.exec,\n      action.parameters,\n      action.limits,\n      action.version,\n      action.publish,\n      action.annotations ++ systemAnnotations(kind))\n\n    (0 until 5).map { i =>\n      Get(s\"$collectionPath/$name\") ~> Route.seal(routes(creds)(transid())) ~> check {\n        status should be(OK)\n        val response = responseAs[WhiskAction]\n        checkWhiskEntityResponse(response, expectedAction)\n      }\n    }\n\n    //Loading action with attachment concurrently should load only attachment once\n    val logs = stream.toString\n    withClue(s\"db logs $logs\") {\n      StringUtils.countMatches(logs, \"finding document\") shouldBe 1\n      StringUtils.countMatches(logs, \"finding attachment\") shouldBe 1\n    }\n    stream.reset()\n  }\n\n  it should \"update an existing action with attachment that is not cached\" in {\n    implicit val tid = transid()\n    val nodeAction = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters(\"x\", \"b\"))\n    val swiftAction = WhiskAction(namespace, aname(), swift(nonInlinedCode(entityStore)), Parameters(\"x\", \"b\"))\n    val bbAction = WhiskAction(namespace, aname(), bb(\"bb\", nonInlinedCode(entityStore), Some(\"bbMain\")))\n    val actions = Seq((nodeAction, NODEJS), (swiftAction, SWIFT5), (bbAction, BLACKBOX))\n\n    actions.foreach {\n      case (action, kind) =>\n        val content = WhiskActionPut(\n          Some(action.exec),\n          Some(action.parameters),\n          Some(\n            ActionLimitsOption(\n              Some(action.limits.timeout),\n              Some(action.limits.memory),\n              Some(action.limits.logs),\n              Some(action.limits.concurrency))))\n        val name = action.name\n        val cacheKey = s\"${CacheKey(action)}\".replace(\"(\", \"\\\\(\").replace(\")\", \"\\\\)\")\n        val expectedPutLog =\n          Seq(\n            s\"uploading attachment '[\\\\w-/:]+' of document 'id: ${action.namespace}/${action.name}\",\n            s\"caching $cacheKey\")\n            .mkString(\"(?s).*\")\n\n        Put(s\"$collectionPath/$name\", content) ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n        }\n\n        removeFromCache(action, WhiskAction)\n\n        Put(s\"$collectionPath/$name?overwrite=true\", content) ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version.upPatch,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n        stream.toString should include regex (expectedPutLog)\n        stream.reset()\n\n        // delete should invalidate cache\n        Delete(s\"$collectionPath/$name\") ~> Route.seal(routes(creds)(transid())) ~> check {\n          status should be(OK)\n          val response = responseAs[WhiskAction]\n          checkWhiskEntityResponse(\n            response,\n            WhiskAction(\n              action.namespace,\n              action.name,\n              action.exec,\n              action.parameters,\n              action.limits,\n              action.version.upPatch,\n              action.publish,\n              action.annotations ++ systemAnnotations(kind)))\n        }\n        stream.toString should include(s\"invalidating ${CacheKey(action)}\")\n        stream.reset()\n    }\n  }\n\n  it should \"ensure old and new action schemas are supported\" in {\n    implicit val tid = transid()\n    val code = nonInlinedCode(entityStore)\n    val actionOldSchema = WhiskAction(namespace, aname(), jsOld(code))\n    val actionNewSchema = WhiskAction(namespace, aname(), jsDefault(code))\n    val content = WhiskActionPut(\n      Some(actionOldSchema.exec),\n      Some(actionOldSchema.parameters),\n      Some(\n        ActionLimitsOption(\n          Some(actionOldSchema.limits.timeout),\n          Some(actionOldSchema.limits.memory),\n          Some(actionOldSchema.limits.logs),\n          Some(actionOldSchema.limits.concurrency))))\n    val expectedPutLog =\n      Seq(s\"uploading attachment '[\\\\w-/:]+' of document 'id: ${actionOldSchema.namespace}/${actionOldSchema.name}\")\n        .mkString(\"(?s).*\")\n\n    put(entityStore, actionOldSchema)\n\n    stream.toString should not include regex(expectedPutLog)\n    stream.reset()\n\n    Post(s\"$collectionPath/${actionOldSchema.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n    }\n\n    Put(s\"$collectionPath/${actionOldSchema.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          actionOldSchema.namespace,\n          actionOldSchema.name,\n          actionNewSchema.exec,\n          actionOldSchema.parameters,\n          actionOldSchema.limits,\n          actionOldSchema.version.upPatch,\n          actionOldSchema.publish,\n          actionOldSchema.annotations ++ systemAnnotations(NODEJS, create = false)))\n    }\n\n    stream.toString should include regex (expectedPutLog)\n    stream.reset()\n\n    Post(s\"$collectionPath/${actionOldSchema.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n    }\n\n    Delete(s\"$collectionPath/${actionOldSchema.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          actionOldSchema.namespace,\n          actionOldSchema.name,\n          actionNewSchema.exec,\n          actionOldSchema.parameters,\n          actionOldSchema.limits,\n          actionOldSchema.version.upPatch,\n          actionOldSchema.publish,\n          actionOldSchema.annotations ++ systemAnnotations(NODEJS, create = false)))\n    }\n  }\n\n  it should \"reject put with conflict for pre-existing action\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    val content = WhiskActionPut(Some(action.exec))\n    put(entityStore, action)\n    Put(s\"$collectionPath/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Conflict)\n    }\n  }\n\n  it should \"update action with a put\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"), ActionLimits())\n    val content = WhiskActionPut(\n      Some(jsDefault(\"_\")),\n      Some(Parameters(\"x\", \"X\")),\n      Some(\n        ActionLimitsOption(\n          Some(TimeLimit(TimeLimit.MAX_DURATION)),\n          Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),\n          Some(LogLimit(LogLimit.MAX_LOGSIZE)),\n          Some(IntraConcurrencyLimit(IntraConcurrencyLimit.MAX_CONCURRENT)))))\n    put(entityStore, action)\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n\n      response.updated should not be action.updated\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          content.exec.get,\n          content.parameters.get,\n          ActionLimits(\n            content.limits.get.timeout.get,\n            content.limits.get.memory.get,\n            content.limits.get.logs.get,\n            content.limits.get.concurrency.get),\n          version = action.version.upPatch,\n          annotations = action.annotations ++ systemAnnotations(NODEJS, create = false)))\n    }\n  }\n\n  it should \"update action parameters with a put\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    val content = WhiskActionPut(parameters = Some(Parameters(\"x\", \"X\")))\n    put(entityStore, action)\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      checkWhiskEntityResponse(\n        response,\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          content.parameters.get,\n          version = action.version.upPatch,\n          annotations = action.annotations ++ systemAnnotations(NODEJS, false)))\n    }\n  }\n\n  //// POST /actions/name\n  it should \"invoke an action with arguments, nonblocking\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"x\", \"b\"))\n    val args = JsObject(\"xxx\" -> \"yyy\".toJson)\n    put(entityStore, action)\n\n    // it should \"reject post to action in namespace not owned by subject\"\n    val auser = WhiskAuthHelpers.newIdentity()\n    Post(s\"/$namespace/${collection.path}/${action.name}\", args) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Post(s\"$collectionPath/${action.name}\", args) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n      headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n    }\n\n    // it should \"ignore &result when invoking nonblocking action\"\n    Post(s\"$collectionPath/${action.name}?result=true\", args) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n      headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n    }\n  }\n\n  it should \"invoke an action with init arguments\" in {\n    implicit val tid = transid()\n    val action =\n      WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(\"E\", \"e\", init = true) ++ Parameters(\"a\", \"A\"))\n    put(entityStore, action)\n\n    loadBalancer.activationMessageChecker = Some { msg: ActivationMessage =>\n      msg.initArgs shouldBe Set(\"E\")\n      msg.content shouldBe Some {\n        JsObject(\"E\" -> JsString(\"e\"), \"a\" -> JsString(\"A\"))\n      }\n    }\n\n    Post(s\"$collectionPath/${action.name}\", JsObject.empty) ~> Route.seal(routes(creds)) ~> check {\n      loadBalancer.activationMessageChecker = None\n      status should be(Accepted)\n    }\n\n    // overriding an init param is permitted\n    val args = JsObject(\"E\" -> \"E\".toJson)\n\n    loadBalancer.activationMessageChecker = Some { msg: ActivationMessage =>\n      msg.initArgs shouldBe Set(\"E\")\n      msg.content shouldBe Some {\n        JsObject(\"E\" -> JsString(\"E\"), \"a\" -> JsString(\"A\"))\n      }\n    }\n\n    Post(s\"$collectionPath/${action.name}\", args) ~> Route.seal(routes(creds)) ~> check {\n      loadBalancer.activationMessageChecker = None\n      status should be(Accepted)\n    }\n  }\n\n  it should \"invoke an action, nonblocking\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    put(entityStore, action)\n    Post(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n      headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n    }\n  }\n\n  it should \"not invoke an action when final parameters are redefined\" in {\n    implicit val tid = transid()\n    val annotations = Parameters(Annotations.FinalParamsAnnotationName, JsTrue)\n    val parameters = Parameters(\"a\", \"A\") ++ Parameters(\"empty\", JsNull)\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"), parameters = parameters, annotations = annotations)\n    put(entityStore, action)\n    Seq((Parameters(\"a\", \"B\"), BadRequest), (Parameters(\"empty\", \"C\"), Accepted)).foreach {\n      case (p, code) =>\n        Post(s\"$collectionPath/${action.name}\", p.toJsObject) ~> Route.seal(routes(creds)) ~> check {\n          status should be(code)\n          if (code == BadRequest) {\n            responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n          }\n        }\n    }\n  }\n\n  it should \"invoke a blocking action and retrieve result via active ack\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val activation = WhiskActivation(\n      action.namespace,\n      action.name,\n      creds.subject,\n      activationIdFactory.make(),\n      start = Instant.now,\n      end = Instant.now,\n      response = ActivationResponse.success(Some(JsObject(\"test\" -> \"yes\".toJson))))\n    put(entityStore, action)\n\n    try {\n      // do not store the activation in the db, instead register it as the response to generate on active ack\n      loadBalancer.whiskActivationStub = Some((1.milliseconds, Right(activation)))\n\n      Post(s\"$collectionPath/${action.name}?blocking=true\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.withoutLogs.toExtendedJson())\n      }\n\n      // repeat invoke, get only result back\n      Post(s\"$collectionPath/${action.name}?blocking=true&result=true\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.resultAsJson)\n      }\n    } finally {\n      loadBalancer.whiskActivationStub = None\n    }\n  }\n\n  it should \"invoke a blocking action, waiting up to specified timeout and retrieve result via active ack\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val activation = WhiskActivation(\n      action.namespace,\n      action.name,\n      creds.subject,\n      activationIdFactory.make(),\n      start = Instant.now,\n      end = Instant.now,\n      response = ActivationResponse.success(Some(JsObject(\"test\" -> \"yes\".toJson))))\n    put(entityStore, action)\n\n    try {\n      // do not store the activation in the db, instead register it as the response to generate on active ack\n      loadBalancer.whiskActivationStub = Some((300.milliseconds, Right(activation)))\n\n      Post(s\"$collectionPath/${action.name}?blocking=true&timeout=0\") ~> Route.seal(routes(creds)) ~> check {\n        status shouldBe BadRequest\n        responseAs[String] should include(\n          Messages.invalidTimeout(controllerActivationConfig.maxWaitForBlockingActivation))\n      }\n\n      Post(s\"$collectionPath/${action.name}?blocking=true&timeout=65000\") ~> Route.seal(routes(creds)) ~> check {\n        status shouldBe BadRequest\n        responseAs[String] should include(\n          Messages.invalidTimeout(controllerActivationConfig.maxWaitForBlockingActivation))\n      }\n\n      // will not wait long enough should get accepted status\n      Post(s\"$collectionPath/${action.name}?blocking=true&timeout=100\") ~> Route.seal(routes(creds)) ~> check {\n        val response = responseAs[JsObject]\n        status shouldBe Accepted\n        headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n      }\n\n      // repeat this time wait longer than active ack delay\n      Post(s\"$collectionPath/${action.name}?blocking=true&timeout=500\") ~> Route.seal(routes(creds)) ~> check {\n        status shouldBe OK\n        val response = responseAs[JsObject]\n        response shouldBe activation.withoutLogs.toExtendedJson()\n        headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n      }\n    } finally {\n      loadBalancer.whiskActivationStub = None\n    }\n  }\n\n  it should \"ensure WhiskActionMetadata is used to invoke an action\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    put(entityStore, action)\n    Post(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n      headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n    }\n    stream.toString should include(s\"[WhiskActionMetaData] [GET] serving from datastore: ${CacheKey(action)}\")\n    stream.reset()\n  }\n\n  it should \"report proper error when record is corrupted on delete\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    Delete(s\"$collectionPath/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when record is corrupted on get\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    Get(s\"$collectionPath/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when record is corrupted on put\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    val components = Vector(stringToFullyQualifiedName(s\"$namespace/${entity.name}\"))\n    val content = WhiskActionPut(Some(sequence(components)))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  // get and delete allowed, create/update with deprecated exec not allowed, post/invoke not allowed\n  it should \"report proper error when runtime is deprecated\" in {\n    implicit val tid = transid()\n\n    try {\n      val okKind = \"test:1\"\n      val deprecatedKind = \"test:2\"\n\n      val customManifest = Some(s\"\"\"\n           |{ \"runtimes\": {\n           |    \"test\": [\n           |      {\n           |        \"kind\": \"$okKind\",\n           |        \"deprecated\": false,\n           |        \"default\": true,\n           |        \"image\": {\n           |          \"name\": \"xyz\"\n           |        }\n           |      }, {\n           |        \"kind\": \"$deprecatedKind\",\n           |        \"deprecated\": true,\n           |        \"image\": {\n           |          \"name\": \"xyz\"\n           |        }\n           |      }\n           |    ]\n           |  }\n           |}\n           |\"\"\".stripMargin)\n      ExecManifest.initialize(whiskConfig, customManifest)\n\n      val deprecatedManifest = ExecManifest.runtimesManifest.resolveDefaultRuntime(deprecatedKind).get\n      val deprecatedExec =\n        CodeExecAsAttachment(deprecatedManifest, Attachments.serdes[String].read(JsString(\"??\")), None)\n\n      val okManifest = ExecManifest.runtimesManifest.resolveDefaultRuntime(okKind).get\n      val okExec = CodeExecAsAttachment(okManifest, Attachments.serdes[String].read(JsString(\"??\")), None)\n\n      val action = WhiskAction(namespace, aname(), deprecatedExec)\n      val okUpdate = WhiskActionPut(Some(okExec))\n      val badUpdate = WhiskActionPut(Some(deprecatedExec))\n\n      Put(s\"$collectionPath/${action.name}\", WhiskActionPut(Some(action.exec))) ~> Route.seal(routes(creds)) ~> check {\n        status shouldBe BadRequest\n        responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)\n      }\n\n      Put(s\"$collectionPath/${action.name}?overwrite=true\", WhiskActionPut(Some(action.exec))) ~> Route.seal(\n        routes(creds)) ~> check {\n        status shouldBe BadRequest\n        responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)\n      }\n\n      put(entityStore, action)\n\n      Put(s\"$collectionPath/${action.name}?overwrite=true\", JsObject.empty) ~> Route.seal(routes(creds)) ~> check {\n        status shouldBe BadRequest\n        responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)\n      }\n\n      Put(s\"$collectionPath/${action.name}?overwrite=true\", badUpdate) ~> Route.seal(routes(creds)) ~> check {\n        status shouldBe BadRequest\n        responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)\n      }\n\n      Post(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n        status shouldBe BadRequest\n        responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)\n      }\n\n      Get(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n        status shouldBe OK\n      }\n\n      Delete(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n        status shouldBe OK\n      }\n\n      put(entityStore, action)\n\n      Put(s\"$collectionPath/${action.name}?overwrite=true\", okUpdate) ~> Route.seal(routes(creds)) ~> check {\n        deleteAction(action.docid)\n        status shouldBe OK\n      }\n    } finally {\n      // restore manifest\n      ExecManifest.initialize(whiskConfig)\n    }\n  }\n}\n\n@RunWith(classOf[JUnitRunner])\nclass WhiskActionsApiTests extends AnyFlatSpec with Matchers with ExecHelpers {\n  import WhiskActionsApi.amendAnnotations\n  import Annotations.ProvideApiKeyAnnotationName\n  import WhiskAction.execFieldName\n\n  val baseParams = Parameters(\"a\", JsString(\"A\")) ++ Parameters(\"b\", JsString(\"B\"))\n  val keyTruthyAnnotation = Parameters(ProvideApiKeyAnnotationName, JsTrue)\n  val keyFalsyAnnotation = Parameters(ProvideApiKeyAnnotationName, JsString.empty) // falsy other than JsFalse\n  val execAnnotation = Parameters(execFieldName, JsString(\"foo\"))\n  val exec: Exec = jsDefault(\"??\")\n\n  it should \"add key annotation if it is not present already\" in {\n    Seq(Parameters(), baseParams).foreach { p =>\n      amendAnnotations(p, exec) shouldBe {\n        p ++ Parameters(ProvideApiKeyAnnotationName, JsFalse) ++\n          Parameters(WhiskAction.execFieldName, exec.kind)\n      }\n    }\n  }\n\n  it should \"not add key annotation if already present regardless of value\" in {\n    Seq(keyTruthyAnnotation, keyFalsyAnnotation).foreach { p =>\n      amendAnnotations(p, exec) shouldBe {\n        p ++ Parameters(WhiskAction.execFieldName, exec.kind)\n      }\n    }\n  }\n\n  it should \"override system annotation as necessary\" in {\n    amendAnnotations(baseParams ++ execAnnotation, exec) shouldBe {\n      baseParams ++ Parameters(ProvideApiKeyAnnotationName, JsFalse) ++ Parameters(WhiskAction.execFieldName, exec.kind)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/ActionsApiWithDbPollingTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport java.time.Instant\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.model.headers.RawHeader\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.openwhisk.core.controller.WhiskActionsApi\nimport org.apache.openwhisk.core.controller.actions.ControllerActivationConfig\nimport org.apache.openwhisk.core.database.{ActivationStoreLevel, UserContext}\nimport org.apache.openwhisk.core.entity._\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.concurrent.duration.DurationInt\n\n/**\n * Tests Actions API. These tests enable the secondary activation completion path.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass ActionsApiWithDbPollingTests extends ControllerTestCommon with WhiskActionsApi {\n\n  /** Actions API tests */\n  behavior of \"Actions API with DB Polling\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val context = UserContext(creds)\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n\n  def aname() = MakeName.next(\"action_tests\")\n\n  val actionLimit = Exec.sizeLimit\n  val parametersLimit = Parameters.sizeLimit\n\n  override val controllerActivationConfig = ControllerActivationConfig(true, 60.seconds)\n\n  it should \"invoke a blocking action and retrieve result via db polling\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val activation = WhiskActivation(\n      action.namespace,\n      action.name,\n      creds.subject,\n      activationIdFactory.make(),\n      start = Instant.now,\n      end = Instant.now,\n      response = ActivationResponse.success(Some(JsObject(\"test\" -> \"yes\".toJson))),\n      logs = ActivationLogs(Vector(\"first line\", \"second line\")))\n    put(entityStore, action)\n    // storing the activation in the db will allow the db polling to retrieve it\n    // the test harness makes sure the activation id observed by the test matches\n    // the one generated by the api handler\n    storeActivation(activation, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context)\n    try {\n      Post(s\"$collectionPath/${action.name}?blocking=true\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.withoutLogs.toExtendedJson())\n      }\n\n      // repeat invoke, get only result back\n      Post(s\"$collectionPath/${action.name}?blocking=true&result=true\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.resultAsJson)\n        headers should contain(RawHeader(ActivationIdHeader, activation.activationId.asString))\n      }\n    } finally {\n      deleteActivation(ActivationId(activation.docid.asString), context)\n    }\n  }\n\n  it should \"invoke a blocking action and return error response when activation fails\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val activation = WhiskActivation(\n      action.namespace,\n      action.name,\n      creds.subject,\n      activationIdFactory.make(),\n      start = Instant.now,\n      end = Instant.now,\n      response = ActivationResponse.whiskError(\"test\"))\n    put(entityStore, action)\n    // storing the activation in the db will allow the db polling to retrieve it\n    // the test harness makes sure the activation id observed by the test matches\n    // the one generated by the api handler\n    storeActivation(activation, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context)\n    try {\n      Post(s\"$collectionPath/${action.name}?blocking=true\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(InternalServerError)\n        val response = responseAs[JsObject]\n        response should be(activation.withoutLogs.toExtendedJson())\n        headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n      }\n    } finally {\n      deleteActivation(ActivationId(activation.docid.asString), context)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/ActionsApiWithoutDbPollingTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport java.time.Instant\n\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.model.headers.RawHeader\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.openwhisk.core.controller.WhiskActionsApi\nimport org.apache.openwhisk.core.controller.actions.ControllerActivationConfig\nimport org.apache.openwhisk.core.database.UserContext\nimport org.apache.openwhisk.core.entity._\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.concurrent.duration.DurationInt\n\n/**\n * Tests Actions API. These tests disable the secondary activation completion path.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass ActionsApiWithoutDbPollingTests extends ControllerTestCommon with WhiskActionsApi {\n\n  /** Actions API tests */\n  behavior of \"Actions API without DB Polling\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val context = UserContext(creds)\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n\n  def aname() = MakeName.next(\"action_tests\")\n\n  val actionLimit = Exec.sizeLimit\n  val parametersLimit = Parameters.sizeLimit\n\n  override val controllerActivationConfig = ControllerActivationConfig(false, 2.seconds)\n\n  it should \"invoke a blocking action which is converted to a non-blocking due to delayed active ack\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(\n      namespace,\n      aname(),\n      jsDefault(\"??\"),\n      limits = ActionLimits(\n        TimeLimit(controllerActivationConfig.maxWaitForBlockingActivation - 1.second),\n        MemoryLimit(),\n        LogLimit()))\n\n    put(entityStore, action)\n    val start = Instant.now\n    Post(s\"$collectionPath/${action.name}?blocking=true\") ~> Route.seal(routes(creds)) ~> check {\n      // status should be accepted because there is no active ack response and\n      // db polling will fail since there is no record of the activation\n      // as a result, the api handler will convert this to a non-blocking request\n      status should be(Accepted)\n      val duration = Instant.now.toEpochMilli - start.toEpochMilli\n      val response = responseAs[JsObject]\n\n      response.fields.size shouldBe 1\n      response.fields(\"activationId\") should not be None\n      headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n\n      // all blocking requests wait up to the specified blocking timeout regadless of the action time limit\n      duration should be >= controllerActivationConfig.maxWaitForBlockingActivation.toMillis\n    }\n  }\n\n  it should \"invoke a blocking action which completes with an activation id only\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    put(entityStore, action)\n\n    try {\n      // do not store the activation in the db, instead register it as the response to generate on active ack\n      loadBalancer.whiskActivationStub = Some((1.milliseconds, Left(activationIdFactory.make())))\n\n      val start = Instant.now\n      Post(s\"$collectionPath/${action.name}?blocking=true\") ~> Route.seal(routes(creds)) ~> check {\n        // status should be accepted because the test is simulating a response which only has\n        // an activation id response from the invoker; unlike the previous test, here the invoker\n        // does respond to complete the api handler's waiting promise\n        status should be(Accepted)\n        val duration = Instant.now.toEpochMilli - start.toEpochMilli\n        val response = responseAs[JsObject]\n\n        response.fields.size shouldBe 1\n        response.fields(\"activationId\") should not be None\n        headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n        duration should be <= 1.second.toMillis\n      }\n    } finally {\n      loadBalancer.whiskActivationStub = None\n    }\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/ActivationsApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport java.time.{Clock, Instant}\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.core.controller.WhiskActivationsApi\nimport org.apache.openwhisk.core.entitlement.Collection\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.{ErrorResponse, Messages}\nimport org.apache.openwhisk.core.database.{ActivationStoreLevel, UserContext}\n\n/**\n * Tests Activations API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass ActivationsApiTests extends ControllerTestCommon with WhiskActivationsApi {\n\n  /** Activations API tests */\n  behavior of \"Activations API\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val context = UserContext(creds)\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n\n  def aname() = MakeName.next(\"activations_tests\")\n\n  def checkCount(filter: String, expected: Int, user: Identity = creds) = {\n    implicit val tid = transid()\n    withClue(s\"count did not match for filter: $filter\") {\n      org.apache.openwhisk.utils.retry {\n        Get(s\"$collectionPath?count=true&$filter\") ~> Route.seal(routes(user)) ~> check {\n          status should be(OK)\n          responseAs[JsObject] shouldBe JsObject(collection.path -> JsNumber(expected))\n        }\n      }\n    }\n  }\n\n  //// GET /activations\n  it should \"get summary activation by namespace\" in {\n    implicit val tid = transid()\n    // create two sets of activation records, and check that only one set is served back\n    val creds1 = WhiskAuthHelpers.newAuth()\n    val notExpectedActivations = (1 to 2).map { i =>\n      WhiskActivation(\n        EntityPath(creds1.subject.asString),\n        aname(),\n        creds1.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    }\n    val actionName = aname()\n    val activations = (1 to 2).map { i =>\n      WhiskActivation(\n        namespace,\n        actionName,\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    }.toList\n    try {\n      (notExpectedActivations ++ activations).foreach(\n        storeActivation(_, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context))\n      waitOnListActivationsInNamespace(namespace, 2, context)\n\n      org.apache.openwhisk.utils.retry {\n        Get(s\"$collectionPath\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val response = responseAs[List[JsObject]]\n          activations.length should be(response.length)\n          response should contain theSameElementsAs activations.map(_.summaryAsJson)\n          response forall { a =>\n            a.getFields(\"for\") match {\n              case Seq(JsString(n)) => n == actionName.asString\n              case _                => false\n            }\n          }\n        }\n      }\n\n      // it should \"list activations with explicit namespace owned by subject\" in {\n      org.apache.openwhisk.utils.retry {\n        Get(s\"/$namespace/${collection.path}\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val response = responseAs[List[JsObject]]\n          activations.length should be(response.length)\n          response should contain theSameElementsAs activations.map(_.summaryAsJson)\n          response forall { a =>\n            a.getFields(\"for\") match {\n              case Seq(JsString(n)) => n == actionName.asString\n              case _                => false\n            }\n          }\n        }\n      }\n\n      // it should \"reject list activations with explicit namespace not owned by subject\" in {\n      val auser = WhiskAuthHelpers.newIdentity()\n      Get(s\"/$namespace/${collection.path}\") ~> Route.seal(routes(auser)) ~> check {\n        status should be(Forbidden)\n      }\n\n    } finally {\n      (notExpectedActivations ++ activations).foreach(activation =>\n        deleteActivation(ActivationId(activation.docid.asString), context))\n    }\n  }\n\n  //// GET /activations?docs=true\n  it should \"return empty list when no activations exist\" in {\n    implicit val tid = transid()\n    org.apache.openwhisk.utils\n      .retry { // retry because view will be stale from previous test and result in null doc fields\n        Get(s\"$collectionPath?docs=true\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          responseAs[List[JsObject]] shouldBe 'empty\n        }\n      }\n  }\n\n  it should \"get full activation by namespace\" in {\n    implicit val tid = transid()\n    // create two sets of activation records, and check that only one set is served back\n    val creds1 = WhiskAuthHelpers.newAuth()\n    val notExpectedActivations = (1 to 2).map { i =>\n      WhiskActivation(\n        EntityPath(creds1.subject.asString),\n        aname(),\n        creds1.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    }\n    val actionName = aname()\n    val activations = (1 to 2).map { i =>\n      WhiskActivation(\n        namespace,\n        actionName,\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now,\n        response = ActivationResponse.success(Some(JsNumber(5))))\n    }.toList\n\n    try {\n      (notExpectedActivations ++ activations).foreach(\n        storeActivation(_, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context))\n      waitOnListActivationsInNamespace(namespace, 2, context)\n      checkCount(\"\", 2)\n\n      org.apache.openwhisk.utils.retry {\n        Get(s\"$collectionPath?docs=true\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val response = responseAs[List[JsObject]]\n          activations.length should be(response.length)\n          response should contain theSameElementsAs activations.map(_.toExtendedJson())\n        }\n      }\n    } finally {\n      (notExpectedActivations ++ activations).foreach(activation =>\n        deleteActivation(ActivationId(activation.docid.asString), context))\n    }\n  }\n\n  //// GET /activations?docs=true&since=xxx&upto=yyy\n  it should \"get full activation by namespace within a date range\" in {\n    implicit val tid = transid()\n    // create two sets of activation records, and check that only one set is served back\n    val creds1 = WhiskAuthHelpers.newAuth()\n    val notExpectedActivations = (1 to 2).map { i =>\n      WhiskActivation(\n        EntityPath(creds1.subject.asString),\n        aname(),\n        creds1.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    }\n\n    val actionName = aname()\n    val now = Instant.now(Clock.systemUTC())\n    val since = now.plusSeconds(10)\n    val upto = now.plusSeconds(30)\n    val activations = Seq(\n      WhiskActivation(\n        namespace,\n        actionName,\n        creds.subject,\n        ActivationId.generate(),\n        start = now.plusSeconds(9),\n        end = now.plusSeconds(9)),\n      WhiskActivation(\n        namespace,\n        actionName,\n        creds.subject,\n        ActivationId.generate(),\n        start = now.plusSeconds(20),\n        end = now.plusSeconds(20)), // should match\n      WhiskActivation(\n        namespace,\n        actionName,\n        creds.subject,\n        ActivationId.generate(),\n        start = now.plusSeconds(10),\n        end = now.plusSeconds(20)), // should match\n      WhiskActivation(\n        namespace,\n        actionName,\n        creds.subject,\n        ActivationId.generate(),\n        start = now.plusSeconds(31),\n        end = now.plusSeconds(31)),\n      WhiskActivation(\n        namespace,\n        actionName,\n        creds.subject,\n        ActivationId.generate(),\n        start = now.plusSeconds(30),\n        end = now.plusSeconds(30))) // should match\n\n    try {\n      (notExpectedActivations ++ activations).foreach(\n        storeActivation(_, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context))\n      waitOnListActivationsInNamespace(namespace, activations.length, context)\n\n      { // get between two time stamps\n        val filter = s\"since=${since.toEpochMilli}&upto=${upto.toEpochMilli}\"\n        val expected = activations.filter { e =>\n          (e.start.equals(since) || e.start.equals(upto) || (e.start.isAfter(since) && e.start.isBefore(upto)))\n        }\n\n        checkCount(filter, expected.length)\n\n        org.apache.openwhisk.utils.retry {\n          Get(s\"$collectionPath?docs=true&$filter\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[List[JsObject]]\n            expected.length should be(response.length)\n            response should contain theSameElementsAs expected.map(_.toExtendedJson())\n          }\n        }\n      }\n\n      { // get 'upto' with no defined since value should return all activation 'upto'\n        val expected = activations.filter(e => e.start.equals(upto) || e.start.isBefore(upto))\n        val filter = s\"upto=${upto.toEpochMilli}\"\n\n        checkCount(filter, expected.length)\n\n        org.apache.openwhisk.utils.retry {\n          Get(s\"$collectionPath?docs=true&$filter\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[List[JsObject]]\n            expected.length should be(response.length)\n            response should contain theSameElementsAs expected.map(_.toExtendedJson())\n          }\n        }\n      }\n\n      { // get 'since' with no defined upto value should return all activation 'since'\n        org.apache.openwhisk.utils.retry {\n          val expected = activations.filter(e => e.start.equals(since) || e.start.isAfter(since))\n          val filter = s\"since=${since.toEpochMilli}\"\n\n          checkCount(filter, expected.length)\n\n          Get(s\"$collectionPath?docs=true&$filter\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[List[JsObject]]\n            expected.length should be(response.length)\n            response should contain theSameElementsAs expected.map(_.toExtendedJson())\n          }\n        }\n      }\n\n    } finally {\n      (notExpectedActivations ++ activations).foreach(activation =>\n        deleteActivation(ActivationId(activation.docid.asString), context))\n    }\n  }\n\n  //// GET /activations?name=xyz\n  it should \"accept valid name parameters and reject invalid ones\" in {\n    implicit val tid = transid()\n\n    Seq((\"\", OK), (\"name=\", OK), (\"name=abc\", OK), (\"name=abc/xyz\", OK), (\"name=abc/xyz/123\", BadRequest)).foreach {\n      case (p, s) =>\n        Get(s\"$collectionPath?$p\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(s)\n          if (s == BadRequest) {\n            responseAs[String] should include(Messages.badNameFilter(p.drop(5)))\n          }\n        }\n    }\n  }\n\n  it should \"get summary activation by namespace and action name\" in {\n    implicit val tid = transid()\n\n    // create two sets of activation records, and check that only one set is served back\n    val creds1 = WhiskAuthHelpers.newAuth()\n    val notExpectedActivations = (1 to 2).map { i =>\n      WhiskActivation(\n        EntityPath(creds1.subject.asString),\n        aname(),\n        creds1.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    }\n    val activations = (1 to 2).map { i =>\n      WhiskActivation(\n        namespace,\n        EntityName(s\"xyz\"),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    }.toList\n\n    val activationsInPackage = (1 to 2).map { i =>\n      WhiskActivation(\n        namespace,\n        EntityName(s\"xyz\"),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now,\n        annotations = Parameters(\"path\", s\"${namespace.asString}/pkg/xyz\"))\n    }.toList\n    try {\n      (notExpectedActivations ++ activations ++ activationsInPackage).foreach(\n        storeActivation(_, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context))\n      waitOnListActivationsMatchingName(namespace, EntityPath(\"xyz\"), activations.length, context)\n      waitOnListActivationsMatchingName(\n        namespace,\n        EntityName(\"pkg\").addPath(EntityName(\"xyz\")),\n        activations.length,\n        context)\n      checkCount(\"name=xyz\", activations.length)\n\n      org.apache.openwhisk.utils.retry {\n        Get(s\"$collectionPath?name=xyz\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val response = responseAs[List[JsObject]]\n          activations.length should be(response.length)\n          response should contain theSameElementsAs activations.map(_.summaryAsJson)\n        }\n      }\n\n      checkCount(\"name=pkg/xyz\", activations.length)\n\n      org.apache.openwhisk.utils.retry {\n        Get(s\"$collectionPath?name=pkg/xyz\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val response = responseAs[List[JsObject]]\n          activationsInPackage.length should be(response.length)\n          response should contain theSameElementsAs activationsInPackage.map(_.summaryAsJson)\n        }\n      }\n    } finally {\n      (notExpectedActivations ++ activations ++ activationsInPackage).foreach(activation =>\n        deleteActivation(ActivationId(activation.docid.asString), context))\n    }\n\n  }\n\n  it should \"reject invalid query parameter combinations\" in {\n    implicit val tid = transid()\n    org.apache.openwhisk.utils\n      .retry { // retry because view will be stale from previous test and result in null doc fields\n        Get(s\"$collectionPath?docs=true&count=true\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.docsNotAllowedWithCount\n        }\n      }\n  }\n\n  it should \"reject list when limit is greater than maximum allowed value\" in {\n    implicit val tid = transid()\n    val exceededMaxLimit = Collection.MAX_LIST_LIMIT + 1\n    val response = Get(s\"$collectionPath?limit=$exceededMaxLimit\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listLimitOutOfRange(Collection.ACTIVATIONS, exceededMaxLimit, Collection.MAX_LIST_LIMIT)\n      }\n    }\n  }\n\n  it should \"reject list when limit is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?limit=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.ACTIVATIONS, notAnInteger)\n      }\n    }\n  }\n\n  it should \"reject list when skip is negative\" in {\n    implicit val tid = transid()\n    val negativeSkip = -1\n    val response = Get(s\"$collectionPath?skip=$negativeSkip\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listSkipOutOfRange(Collection.ACTIVATIONS, negativeSkip)\n      }\n    }\n  }\n\n  it should \"reject list when skip is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?skip=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.ACTIVATIONS, notAnInteger)\n      }\n    }\n  }\n\n  it should \"reject get activation by namespace and action name when action name is not a valid name\" in {\n    implicit val tid = transid()\n    Get(s\"$collectionPath?name=0%20\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"reject get activation with invalid since/upto value\" in {\n    implicit val tid = transid()\n    Get(s\"$collectionPath?since=xxx\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n    Get(s\"$collectionPath?upto=yyy\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"skip activations and return correct ones\" in {\n    implicit val tid = transid()\n    val activations: Seq[WhiskActivation] = (1 to 3).map { i =>\n      //make sure the time is different for each activation\n      val time = Instant.now.plusMillis(i)\n      WhiskActivation(namespace, aname(), creds.subject, ActivationId.generate(), start = time, end = time)\n    }.toList\n\n    try {\n      activations.foreach(\n        storeActivation(_, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context))\n      waitOnListActivationsInNamespace(namespace, activations.size, context)\n\n      Get(s\"$collectionPath?skip=1\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val resultActivationIds = responseAs[List[JsObject]].map(_.fields(\"name\"))\n        val expectedActivationIds = activations.map(_.toJson.fields(\"name\")).reverse.drop(1)\n        resultActivationIds should be(expectedActivationIds)\n      }\n    } finally {\n      activations.foreach(a => deleteActivation(ActivationId(a.docid.asString), context))\n      waitOnListActivationsInNamespace(namespace, 0, context)\n    }\n  }\n\n  it should \"return last activation\" in {\n    implicit val tid = transid()\n    val activations = (1 to 3).map { i =>\n      //make sure the time is different for each activation\n      val time = Instant.now.plusMillis(i)\n      WhiskActivation(namespace, aname(), creds.subject, ActivationId.generate(), start = time, end = time)\n    }.toList\n\n    try {\n      activations.foreach(\n        storeActivation(_, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context))\n      waitOnListActivationsInNamespace(namespace, activations.size, context)\n\n      Get(s\"$collectionPath?limit=1\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val activationsJson = activations.map(_.toJson)\n        withClue(s\"Original activations: ${activationsJson}\") {\n          val respNames = responseAs[List[JsObject]].map(_.fields(\"name\"))\n          val expectNames = activationsJson.map(_.fields(\"name\")).drop(2)\n          respNames should be(expectNames)\n        }\n      }\n    } finally {\n      activations.foreach(a => deleteActivation(ActivationId(a.docid.asString), context))\n      waitOnListActivationsInNamespace(namespace, 0, context)\n    }\n  }\n\n  //// GET /activations/id\n  it should \"get activation by id\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    try {\n      storeActivation(activation, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context)\n\n      Get(s\"$collectionPath/${activation.activationId.asString}\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.toExtendedJson())\n      }\n\n      // it should \"get activation by name in explicit namespace owned by subject\" in\n      Get(s\"/$namespace/${collection.path}/${activation.activationId.asString}\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.toExtendedJson())\n      }\n\n      // it should \"reject get activation by name in explicit namespace not owned by subject\" in\n      val auser = WhiskAuthHelpers.newIdentity()\n      Get(s\"/$namespace/${collection.path}/${activation.activationId.asString}\") ~> Route.seal(routes(auser)) ~> check {\n        status should be(Forbidden)\n      }\n    } finally {\n      deleteActivation(ActivationId(activation.docid.asString), context)\n    }\n  }\n\n  //// GET /activations/id/result\n  it should \"get activation result by id\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    try {\n      storeActivation(activation, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context)\n\n      Get(s\"$collectionPath/${activation.activationId.asString}/result\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.response.toExtendedJson)\n      }\n    } finally {\n      deleteActivation(ActivationId(activation.docid.asString), context)\n    }\n  }\n\n  //// GET /activations/id/result when db store is disabled\n  it should \"return activation empty when db store is set to failures for successful blocking\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n\n    storeActivation(activation, true, ActivationStoreLevel.STORE_FAILURES, ActivationStoreLevel.STORE_ALWAYS, context)\n\n    Get(s\"$collectionPath/${activation.activationId.asString}/result\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  //// GET /activations/id/result when db store is disabled\n  it should \"return activation empty when db store is set to failures for non-blocking\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n\n    storeActivation(activation, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_FAILURES, context)\n\n    Get(s\"$collectionPath/${activation.activationId.asString}/result\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  //// GET /activations/id/result when store is disabled and activation is not blocking\n  it should \"get activation result by id when db store is disabled for successful blocking and activation is not blocking\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    try {\n      storeActivation(\n        activation,\n        false,\n        ActivationStoreLevel.STORE_FAILURES,\n        ActivationStoreLevel.STORE_ALWAYS,\n        context)\n\n      Get(s\"$collectionPath/${activation.activationId.asString}/result\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.response.toExtendedJson)\n      }\n    } finally {\n      deleteActivation(ActivationId(activation.docid.asString), context)\n    }\n  }\n\n  //// GET /activations/id/result when store is disabled and activation is unsuccessful\n  it should \"get activation result by id when db store is set to failures and activation is unsuccessful\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now,\n        response = ActivationResponse.whiskError(\"activation error\"))\n    try {\n      storeActivation(\n        activation,\n        true,\n        ActivationStoreLevel.STORE_FAILURES,\n        ActivationStoreLevel.STORE_FAILURES,\n        context)\n\n      Get(s\"$collectionPath/${activation.activationId.asString}/result\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.response.toExtendedJson)\n      }\n    } finally {\n      deleteActivation(ActivationId(activation.docid.asString), context)\n    }\n  }\n\n  it should \"return activation empty when db store is set to not application failures and activation is application failure\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now,\n        response = ActivationResponse.applicationError(\"activation error\"))\n\n    storeActivation(\n      activation,\n      true,\n      ActivationStoreLevel.STORE_FAILURES_NOT_APPLICATION_ERRORS,\n      ActivationStoreLevel.STORE_FAILURES_NOT_APPLICATION_ERRORS,\n      context)\n\n    Get(s\"$collectionPath/${activation.activationId.asString}/result\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"get activation result by id when db store is set to not application failures failures and activation is unsuccessful\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now,\n        response = ActivationResponse.whiskError(\"activation error\"))\n    try {\n      storeActivation(\n        activation,\n        true,\n        ActivationStoreLevel.STORE_FAILURES_NOT_APPLICATION_ERRORS,\n        ActivationStoreLevel.STORE_FAILURES_NOT_APPLICATION_ERRORS,\n        context)\n\n      Get(s\"$collectionPath/${activation.activationId.asString}/result\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.response.toExtendedJson)\n      }\n    } finally {\n      deleteActivation(ActivationId(activation.docid.asString), context)\n    }\n  }\n\n  //// GET /activations/id/logs\n  it should \"get activation logs by id\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    try {\n      storeActivation(activation, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context)\n\n      Get(s\"$collectionPath/${activation.activationId.asString}/logs\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response should be(activation.logs.toJsonObject)\n      }\n    } finally {\n      deleteActivation(ActivationId(activation.docid.asString), context)\n    }\n  }\n\n  //// GET /activations/id/bogus\n  it should \"reject request to get invalid activation resource\" in {\n    implicit val tid = transid()\n    val activation =\n      WhiskActivation(\n        namespace,\n        aname(),\n        creds.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now)\n    storeActivation(activation, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context)\n    try {\n\n      Get(s\"$collectionPath/${activation.activationId.asString}/bogus\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(NotFound)\n      }\n    } finally {\n      deleteActivation(ActivationId(activation.docid.asString), context)\n    }\n  }\n\n  it should \"reject get requests with invalid activation ids\" in {\n    implicit val tid = transid()\n    val activationId = ActivationId.generate().toString\n    val tooshort = activationId.substring(0, 31)\n    val toolong = activationId + \"xxx\"\n    val malformed = tooshort + \"z\"\n\n    Get(s\"$collectionPath/$tooshort\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] shouldBe Messages.activationIdLengthError(SizeError(\"Activation id\", tooshort.length.B, 32.B))\n    }\n\n    Get(s\"$collectionPath/$toolong\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] shouldBe Messages.activationIdLengthError(SizeError(\"Activation id\", toolong.length.B, 32.B))\n    }\n\n    Get(s\"$collectionPath/$malformed\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"reject request with put\" in {\n    implicit val tid = transid()\n    Put(s\"$collectionPath/${ActivationId.generate()}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(MethodNotAllowed)\n    }\n  }\n\n  it should \"reject request with post\" in {\n    implicit val tid = transid()\n    Post(s\"$collectionPath/${ActivationId.generate()}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(MethodNotAllowed)\n    }\n  }\n\n  it should \"reject request with delete\" in {\n    implicit val tid = transid()\n    Delete(s\"$collectionPath/${ActivationId.generate()}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(MethodNotAllowed)\n    }\n  }\n\n  it should \"report proper error when record is corrupted on get\" in {\n    implicit val tid = transid()\n\n    //A bad activation type which breaks the deserialization by removing the subject entry\n    class BadActivation(override val namespace: EntityPath,\n                        override val name: EntityName,\n                        override val subject: Subject,\n                        override val activationId: ActivationId,\n                        override val start: Instant,\n                        override val end: Instant)\n        extends WhiskActivation(namespace, name, subject, activationId, start, end) {\n      override def toJson = {\n        val json = super.toJson\n        JsObject(json.fields - \"subject\")\n      }\n    }\n\n    val activation =\n      new BadActivation(namespace, aname(), creds.subject, ActivationId.generate(), Instant.now, Instant.now)\n    storeActivation(activation, false, ActivationStoreLevel.STORE_ALWAYS, ActivationStoreLevel.STORE_ALWAYS, context)\n\n    Get(s\"$collectionPath/${activation.activationId}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/BasicAuthenticateTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport scala.concurrent.Await\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\n\nimport org.apache.openwhisk.core.controller.BasicAuthenticationDirective\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entitlement.Privilege\n\n/**\n * Tests authentication handler which guards API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass BasicAuthenticateTests extends ControllerTestCommon {\n  behavior of \"Authenticate\"\n\n  it should \"authorize a known user using different namespaces and cache key, and reject invalid secret\" in {\n    implicit val tid = transid()\n    val subject = Subject()\n\n    val uuid1 = UUID()\n    val uuid2 = UUID()\n\n    val namespaces = Set(\n      WhiskNamespace(\n        Namespace(MakeName.next(\"authenticatev_tests\"), uuid1),\n        BasicAuthenticationAuthKey(uuid1, Secret())),\n      WhiskNamespace(\n        Namespace(MakeName.next(\"authenticatev_tests\"), uuid2),\n        BasicAuthenticationAuthKey(uuid2, Secret())))\n    val entry = WhiskAuth(subject, namespaces)\n    put(authStore, entry) // this test entry is reclaimed when the test completes\n\n    // Try to login with each specific namespace\n    namespaces.foreach { ns =>\n      withClue(s\"Trying to login to $ns\") {\n        waitOnView(authStore, ns.authkey, 1) // wait for the view to be updated\n        val pass = BasicHttpCredentials(ns.authkey.uuid.asString, ns.authkey.key.asString)\n        val user = Await.result(\n          BasicAuthenticationDirective\n            .validateCredentials(Some(pass))(transid, executionContext, logging, authStore),\n          dbOpTimeout)\n        user.get shouldBe Identity(subject, ns.namespace, ns.authkey, rights = Privilege.ALL)\n\n        // first lookup should have been from datastore\n        stream.toString should include(s\"serving from datastore: ${CacheKey(ns.authkey)}\")\n        stream.reset()\n\n        // repeat query, now should be served from cache\n        val cachedUser = Await.result(\n          BasicAuthenticationDirective\n            .validateCredentials(Some(pass))(transid, executionContext, logging, authStore),\n          dbOpTimeout)\n        cachedUser.get shouldBe Identity(subject, ns.namespace, ns.authkey, rights = Privilege.ALL)\n\n        stream.toString should include(s\"serving from cache: ${CacheKey(ns.authkey)}\")\n        stream.reset()\n      }\n    }\n\n    // check that invalid keys are rejected\n    val ns = namespaces.head\n    val key = ns.authkey.key.asString\n    Seq(key.drop(1), key.dropRight(1), key + \"x\", BasicAuthenticationAuthKey().key.asString).foreach { k =>\n      val pass = BasicHttpCredentials(ns.authkey.uuid.asString, k)\n      val user = Await.result(\n        BasicAuthenticationDirective\n          .validateCredentials(Some(pass))(transid, executionContext, logging, authStore),\n        dbOpTimeout)\n      user shouldBe empty\n    }\n  }\n\n  it should \"not log key during validation\" in {\n    implicit val tid = transid()\n    val creds = WhiskAuthHelpers.newIdentity()\n    val pass = creds.authkey.getCredentials.asInstanceOf[Option[BasicHttpCredentials]]\n    val user = Await.result(\n      BasicAuthenticationDirective.validateCredentials(pass)(transid, executionContext, logging, authStore),\n      dbOpTimeout)\n    user should be(None)\n    stream.toString should not include pass.get.password\n  }\n\n  it should \"not authorize an unknown user\" in {\n    implicit val tid = transid()\n    val creds = WhiskAuthHelpers.newIdentity()\n    val pass = creds.authkey.getCredentials.asInstanceOf[Option[BasicHttpCredentials]]\n    val user = Await.result(\n      BasicAuthenticationDirective.validateCredentials(pass)(transid, executionContext, logging, authStore),\n      dbOpTimeout)\n    user should be(None)\n  }\n\n  it should \"not authorize when no user creds are provided\" in {\n    implicit val tid = transid()\n    val user = Await.result(\n      BasicAuthenticationDirective.validateCredentials(None)(transid, executionContext, logging, authStore),\n      dbOpTimeout)\n    user should be(None)\n  }\n\n  it should \"not authorize when malformed user is provided\" in {\n    implicit val tid = transid()\n    val pass = BasicHttpCredentials(\"x\", Secret().asString)\n    val user = Await.result(\n      BasicAuthenticationDirective\n        .validateCredentials(Some(pass))(transid, executionContext, logging, authStore),\n      dbOpTimeout)\n    user should be(None)\n  }\n\n  it should \"not authorize when malformed secret is provided\" in {\n    implicit val tid = transid()\n    val pass = BasicHttpCredentials(UUID().asString, \"x\")\n    val user = Await.result(\n      BasicAuthenticationDirective\n        .validateCredentials(Some(pass))(transid, executionContext, logging, authStore),\n      dbOpTimeout)\n    user should be(None)\n  }\n\n  it should \"not authorize when malformed creds are provided\" in {\n    implicit val tid = transid()\n    val pass = BasicHttpCredentials(\"x\", \"y\")\n    val user = Await.result(\n      BasicAuthenticationDirective\n        .validateCredentials(Some(pass))(transid, executionContext, logging, authStore),\n      dbOpTimeout)\n    user should be(None)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/ConductorsApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport java.time.Instant\n\nimport scala.concurrent.Future\nimport scala.concurrent.ExecutionContext\nimport scala.language.postfixOps\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller\nimport org.apache.pekko.http.scaladsl.server.Route\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.ActivationMessage\nimport org.apache.openwhisk.core.controller.WhiskActionsApi\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.Messages._\n\nimport scala.util.Success\n\n/**\n * Tests Conductor Actions API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n */\n@RunWith(classOf[JUnitRunner])\nclass ConductorsApiTests extends ControllerTestCommon with WhiskActionsApi {\n\n  /** Conductors API tests */\n  behavior of \"Conductor\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n\n  val alternateCreds = WhiskAuthHelpers.newIdentity()\n  val alternateNamespace = EntityPath(alternateCreds.subject.asString)\n\n  // test actions\n  val echo = MakeName.next(\"echo\")\n  val conductor = MakeName.next(\"conductor\")\n  val step = MakeName.next(\"step\")\n  val missing = MakeName.next(\"missingAction\") // undefined\n  val invalid = \"invalid#Action\" // invalid name\n\n  val testString = \"this is a test\"\n  val duration = 42\n\n  val limit = whiskConfig.actionSequenceLimit.toInt\n\n  override val loadBalancer = new FakeLoadBalancerService(whiskConfig)\n  override val activationIdFactory = new ActivationId.ActivationIdGenerator() {}\n\n  it should \"invoke a conductor action with no dynamic steps\" in {\n    implicit val tid = transid()\n    put(entityStore, WhiskAction(namespace, echo, jsDefault(\"??\"), annotations = Parameters(\"conductor\", \"true\")))\n\n    // a normal result\n    Post(s\"$collectionPath/${echo}?blocking=true\", JsObject(\"payload\" -> testString.toJson)) ~> Route.seal(\n      routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"payload\" -> testString.toJson)\n      response.fields(\"duration\") shouldBe duration.toJson\n      val annotations = response.fields(\"annotations\").convertTo[Parameters]\n      annotations.getAs[Boolean](\"conductor\") shouldBe Success(true)\n      annotations.getAs[String](\"kind\") shouldBe Success(\"sequence\")\n      annotations.getAs[Boolean](\"topmost\") shouldBe Success(true)\n      annotations.get(\"limits\") should not be None\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 1\n    }\n\n    // an error result\n    Post(s\"$collectionPath/${echo}?blocking=true\", JsObject(\"error\" -> testString.toJson)) ~> Route.seal(routes(creds)) ~> check {\n      status should not be (OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"status\") shouldBe \"application error\".toJson\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"error\" -> testString.toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 1\n    }\n\n    // a wrapped result { params: result } is unwrapped by the controller\n    Post(s\"$collectionPath/${echo}?blocking=true\", JsObject(\"params\" -> JsObject(\"payload\" -> testString.toJson))) ~> Route\n      .seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"payload\" -> testString.toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 1\n    }\n\n    // an invalid action name\n    Post(s\"$collectionPath/${echo}?blocking=true\", JsObject(\"payload\" -> testString.toJson, \"action\" -> invalid.toJson)) ~> Route\n      .seal(routes(creds)) ~> check {\n      status should not be (OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"status\") shouldBe \"application error\".toJson\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\n        \"error\" -> compositionComponentInvalid(invalid.toJson).toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 2\n    }\n\n    // an undefined action\n    Post(s\"$collectionPath/${echo}?blocking=true\", JsObject(\"payload\" -> testString.toJson, \"action\" -> missing.toJson)) ~> Route\n      .seal(routes(creds)) ~> check {\n      status should not be (OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"status\") shouldBe \"application error\".toJson\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\n        \"error\" -> compositionComponentNotFound(s\"$namespace/$missing\").toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 2\n    }\n  }\n\n  it should \"invoke a conductor action with dynamic steps\" in {\n    implicit val tid = transid()\n    put(entityStore, WhiskAction(namespace, conductor, jsDefault(\"??\"), annotations = Parameters(\"conductor\", \"true\")))\n    put(entityStore, WhiskAction(namespace, step, jsDefault(\"??\")))\n    put(entityStore, WhiskAction(alternateNamespace, step, jsDefault(\"??\"))) // forbidden action\n    val forbidden = s\"/$alternateNamespace/$step\" // forbidden action name\n\n    // dynamically invoke step action\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\"action\" -> step.toJson, \"params\" -> JsObject(\"n\" -> 1.toJson))) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"n\" -> 2.toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 3\n      response.fields(\"duration\") shouldBe (3 * duration).toJson\n    }\n\n    // dynamically invoke step action with an error result\n    Post(s\"$collectionPath/${conductor}?blocking=true\", JsObject(\"action\" -> step.toJson)) ~> Route.seal(routes(creds)) ~> check {\n      status should not be (OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"status\") shouldBe \"application error\".toJson\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"error\" -> \"missing parameter\".toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 3\n      response.fields(\"duration\") shouldBe (3 * duration).toJson\n    }\n\n    // dynamically invoke step action, forwarding state\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\"action\" -> step.toJson, \"state\" -> JsObject(\"witness\" -> 42.toJson), \"n\" -> 1.toJson)) ~> Route.seal(\n      routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"n\" -> 2.toJson, \"witness\" -> 42.toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 3\n      response.fields(\"duration\") shouldBe (3 * duration).toJson\n    }\n\n    // dynamically invoke a forbidden action\n    Post(s\"$collectionPath/${conductor}?blocking=true\", JsObject(\"action\" -> forbidden.toJson)) ~> Route.seal(\n      routes(creds)) ~> check {\n      status should not be (OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"status\") shouldBe \"application error\".toJson\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\n        \"error\" -> compositionComponentNotAccessible(forbidden.drop(1)).toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 2\n    }\n\n    // dynamically invoke step action twice, forwarding state\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\"action\" -> step.toJson, \"state\" -> JsObject(\"action\" -> step.toJson), \"n\" -> 1.toJson)) ~> Route.seal(\n      routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"n\" -> 3.toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 5\n      response.fields(\"duration\") shouldBe (5 * duration).toJson\n    }\n\n    // invoke nested conductor with single step\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\"action\" -> conductor.toJson, \"params\" -> JsObject(\"action\" -> step.toJson), \"n\" -> 1.toJson)) ~> Route\n      .seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"n\" -> 2.toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 3\n      response.fields(\"duration\") shouldBe (5 * duration).toJson\n    }\n\n    // nested step followed by outer step\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\n        \"action\" -> conductor.toJson,\n        \"state\" -> JsObject(\"action\" -> step.toJson),\n        \"params\" -> JsObject(\"action\" -> step.toJson),\n        \"n\" -> 1.toJson)) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"n\" -> 3.toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 5\n      response.fields(\"duration\") shouldBe (7 * duration).toJson\n    }\n\n    // two levels of nesting, three steps\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\n        \"action\" -> conductor.toJson,\n        \"state\" -> JsObject(\"action\" -> step.toJson),\n        \"params\" -> JsObject(\n          \"action\" -> conductor.toJson,\n          \"state\" -> JsObject(\"action\" -> step.toJson),\n          \"params\" -> JsObject(\"action\" -> step.toJson)),\n        \"n\" -> 1.toJson)) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"n\" -> 4.toJson)\n      response.fields(\"logs\").convertTo[JsArray].elements.size shouldBe 5\n      response.fields(\"duration\") shouldBe (11 * duration).toJson\n    }\n  }\n\n  it should \"abort if composition is too long\" in {\n    implicit val tid = transid()\n    put(entityStore, WhiskAction(namespace, conductor, jsDefault(\"??\"), annotations = Parameters(\"conductor\", \"true\")))\n    put(entityStore, WhiskAction(namespace, step, jsDefault(\"??\")))\n\n    // stay just below limit\n    var params = Map[String, JsValue]()\n    for (i <- 1 to limit) {\n      params = Map(\"action\" -> step.toJson, \"state\" -> JsObject(params))\n    }\n    Post(s\"$collectionPath/${conductor}?blocking=true\", JsObject(params + (\"n\" -> 0.toJson))) ~> Route.seal(\n      routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"n\" -> limit.toJson)\n      response.fields(\"duration\") shouldBe (101 * duration).toJson\n    }\n\n    // add one extra step\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\"action\" -> step.toJson, \"state\" -> JsObject(params), \"n\" -> 0.toJson)) ~> Route.seal(routes(creds)) ~> check {\n      status should not be (OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"status\") shouldBe \"application error\".toJson\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"error\" -> compositionIsTooLong.toJson)\n    }\n\n    // nesting a composition at the limit should be ok\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\"action\" -> conductor.toJson, \"params\" -> JsObject(params), \"n\" -> 0.toJson)) ~> Route.seal(\n      routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"n\" -> limit.toJson)\n    }\n\n    // nesting a composition beyond the limit should fail\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\n        \"action\" -> conductor.toJson,\n        \"params\" -> JsObject(\"action\" -> step.toJson, \"state\" -> JsObject(params)),\n        \"n\" -> 0.toJson)) ~> Route.seal(routes(creds)) ~> check {\n      status should not be (OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"status\") shouldBe \"application error\".toJson\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"error\" -> compositionIsTooLong.toJson)\n    }\n\n    // recursing at the limit should be ok\n    params = Map[String, JsValue]()\n    for (i <- 1 to limit) {\n      params = Map(\"action\" -> conductor.toJson, \"params\" -> JsObject(params))\n    }\n    Post(s\"$collectionPath/${conductor}?blocking=true\", JsObject(params + (\"n\" -> 0.toJson))) ~> Route.seal(\n      routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"n\" -> 0.toJson)\n    }\n\n    // recursing beyond the limit should fail\n    Post(\n      s\"$collectionPath/${conductor}?blocking=true\",\n      JsObject(\"action\" -> conductor.toJson, \"params\" -> JsObject(params), \"n\" -> 0.toJson)) ~> Route.seal(\n      routes(creds)) ~> check {\n      status should not be (OK)\n      val response = responseAs[JsObject]\n      response.fields(\"response\").asJsObject.fields(\"status\") shouldBe \"application error\".toJson\n      response.fields(\"response\").asJsObject.fields(\"result\") shouldBe JsObject(\"error\" -> compositionIsTooLong.toJson)\n    }\n  }\n\n  // fake load balancer to emulate a handful of actions\n  class FakeLoadBalancerService(config: WhiskConfig)(implicit ec: ExecutionContext)\n      extends DegenerateLoadBalancerService(config) {\n\n    private def respond(action: ExecutableWhiskActionMetaData, msg: ActivationMessage, result: JsValue) = {\n      val response = {\n        result match {\n          case JsObject(fields) =>\n            if (fields.get(\"error\") isDefined)\n              ActivationResponse(ActivationResponse.ApplicationError, Some(result))\n            else ActivationResponse.success(Some(result))\n          case _ => ActivationResponse.success(Some(result))\n        }\n      }\n      val start = Instant.now\n      WhiskActivation(\n        action.namespace,\n        action.name,\n        msg.user.subject,\n        msg.activationId,\n        start,\n        end = start.plusMillis(duration),\n        response = response)\n    }\n\n    override def publish(action: ExecutableWhiskActionMetaData, msg: ActivationMessage)(\n      implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]] =\n      msg.content map { args =>\n        Future.successful {\n          action.name match {\n            case `echo` => // echo action\n              Future(Right(respond(action, msg, args)))\n            case `conductor` => // see tests/dat/actions/conductor.js\n              val result = {\n                args match {\n                  case JsObject(fields) =>\n                    if (fields.get(\"error\") isDefined) args\n                    else {\n                      val action = fields.get(\"action\") map { action =>\n                        Map(\"action\" -> action)\n                      } getOrElse Map.empty\n                      val state = fields.get(\"state\") map { state =>\n                        Map(\"state\" -> state)\n                      } getOrElse Map.empty\n                      val wrappedParams = fields.getOrElse(\"params\", JsObject.empty).asJsObject.fields\n                      val escapedParams = fields - \"action\" - \"state\" - \"params\"\n                      val params = Map(\"params\" -> JsObject(wrappedParams ++ escapedParams))\n                      JsObject(params ++ action ++ state)\n                    }\n                  case _ => JsObject.empty\n                }\n\n              }\n              Future(Right(respond(action, msg, result)))\n            case `step` => // see tests/dat/actions/step.js\n              val result = {\n                args match {\n                  case JsObject(fields) =>\n                    fields.get(\"n\") map { n =>\n                      JsObject(\"n\" -> (n.convertTo[BigDecimal] + 1).toJson)\n                    } getOrElse {\n                      JsObject(\"error\" -> \"missing parameter\".toJson)\n                    }\n                  case _ => JsObject.empty\n                }\n\n              }\n              Future(Right(respond(action, msg, result)))\n            case _ =>\n              Future.failed(new IllegalArgumentException(\"Unknown action invoked in conductor test\"))\n          }\n        }\n      } getOrElse Future.failed(new IllegalArgumentException(\"No invocation parameters in conductor test\"))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/ControllerApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport common.StreamLogging\nimport io.restassured.RestAssured\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.entity.{ExecManifest, LogLimit, MemoryLimit, TimeLimit}\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport system.rest.RestUtil\n\n/**\n * Integration tests for Controller routes\n */\n@RunWith(classOf[JUnitRunner])\nclass ControllerApiTests extends AnyFlatSpec with RestUtil with Matchers with StreamLogging {\n\n  it should \"ensure controller returns info\" in {\n    val response = RestAssured.given.config(sslconfig).get(getServiceURL)\n    val config = new WhiskConfig(\n      Map(\n        WhiskConfig.actionInvokePerMinuteLimit -> null,\n        WhiskConfig.triggerFirePerMinuteLimit -> null,\n        WhiskConfig.actionInvokeConcurrentLimit -> null,\n        WhiskConfig.runtimesManifest -> null,\n        WhiskConfig.actionSequenceMaxLimit -> null))\n    ExecManifest.initialize(config) should be a 'success\n\n    val expectedJson = JsObject(\n      \"support\" -> JsObject(\n        \"github\" -> \"https://github.com/apache/openwhisk/issues\".toJson,\n        \"slack\" -> \"http://slack.openwhisk.org\".toJson),\n      \"description\" -> \"OpenWhisk\".toJson,\n      \"api_paths\" -> JsArray(\"/api/v1\".toJson),\n      \"runtimes\" -> ExecManifest.runtimesManifest.toJson,\n      \"limits\" -> JsObject(\n        \"actions_per_minute\" -> config.actionInvokePerMinuteLimit.toInt.toJson,\n        \"triggers_per_minute\" -> config.triggerFirePerMinuteLimit.toInt.toJson,\n        \"concurrent_actions\" -> config.actionInvokeConcurrentLimit.toInt.toJson,\n        \"sequence_length\" -> config.actionSequenceLimit.toInt.toJson,\n        \"default_min_action_duration\" -> TimeLimit.namespaceDefaultConfig.min.toMillis.toJson,\n        \"default_max_action_duration\" -> TimeLimit.namespaceDefaultConfig.max.toMillis.toJson,\n        \"default_min_action_memory\" -> MemoryLimit.namespaceDefaultConfig.min.toBytes.toJson,\n        \"default_max_action_memory\" -> MemoryLimit.namespaceDefaultConfig.max.toBytes.toJson,\n        \"default_min_action_logs\" -> LogLimit.namespaceDefaultConfig.min.toBytes.toJson,\n        \"default_max_action_logs\" -> LogLimit.namespaceDefaultConfig.max.toBytes.toJson,\n        \"min_action_duration\" -> TimeLimit.config.min.toMillis.toJson,\n        \"max_action_duration\" -> TimeLimit.config.max.toMillis.toJson,\n        \"min_action_memory\" -> MemoryLimit.config.min.toBytes.toJson,\n        \"max_action_memory\" -> MemoryLimit.config.max.toBytes.toJson,\n        \"min_action_logs\" -> LogLimit.config.min.toBytes.toJson,\n        \"max_action_logs\" -> LogLimit.config.max.toBytes.toJson))\n    response.statusCode should be(200)\n    response.body.asString.parseJson shouldBe (expectedJson)\n  }\n\n  behavior of \"Controller\"\n\n  it should \"return list of invokers\" in {\n    val response = RestAssured.given.config(sslconfig).get(s\"$getServiceURL/invokers\")\n\n    response.statusCode shouldBe 200\n    response.body.asString shouldBe \"{\\\"invoker0/0\\\":\\\"up\\\",\\\"invoker1/1\\\":\\\"up\\\"}\"\n  }\n\n  it should \"return healthy invokers status\" in {\n    val response = RestAssured.given.config(sslconfig).get(s\"$getServiceURL/invokers/healthy/count\")\n\n    response.statusCode shouldBe 200\n    response.body.asString shouldBe \"2\"\n  }\n\n  it should \"return healthy invokers\" in {\n    val response = RestAssured.given.config(sslconfig).get(s\"$getServiceURL/invokers/ready\")\n\n    response.statusCode shouldBe 200\n    response.body.asString shouldBe \"{\\\"healthy\\\":\\\"2/2\\\"}\"\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/ControllerRoutesTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.openwhisk.common.PekkoLogging\nimport org.apache.openwhisk.core.controller.Controller\nimport org.apache.openwhisk.core.entity.ExecManifest.Runtimes\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatestplus.junit.JUnitRunner\nimport system.rest.RestUtil\nimport spray.json._\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport spray.json.DefaultJsonProtocol._\n\n/**\n * Tests controller readiness.\n *\n * These tests will validate the endpoints that provide information on how many healthy invokers are available\n */\n\n@RunWith(classOf[JUnitRunner])\nclass ControllerRoutesTests extends ControllerTestCommon with BeforeAndAfterEach with RestUtil {\n\n  implicit val logger = new PekkoLogging(org.apache.pekko.event.Logging.getLogger(actorSystem, this))\n\n  behavior of \"Controller\"\n\n  it should \"return unhealthy invokers status\" in {\n\n    configureBuildInfo()\n\n    val controller =\n      new Controller(instance, Runtimes(Set.empty, Set.empty, None), whiskConfig, system, logger)\n    Get(\"/invokers/ready\") ~> Route.seal(controller.internalInvokerHealth) ~> check {\n      status shouldBe InternalServerError\n      responseAs[JsObject].fields(\"unhealthy\") shouldBe JsString(\"0/0\")\n    }\n  }\n\n  it should \"return ready state true when healthy == total invokers\" in {\n\n    val res = Controller.readyState(5, 5, 1.0)\n    res shouldBe true\n  }\n\n  it should \"return ready state false when 0 invokers\" in {\n\n    val res = Controller.readyState(0, 0, 0.5)\n    res shouldBe false\n  }\n\n  it should \"return ready state false when threshold < (healthy / total)\" in {\n\n    val res = Controller.readyState(7, 3, 0.5)\n    res shouldBe false\n  }\n\n  private def configureBuildInfo(): Unit = {\n    System.setProperty(\"whisk.info.build-no\", \"\")\n    System.setProperty(\"whisk.info.date\", \"\")\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/ControllerTestCommon.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport scala.concurrent.{Await, Future}\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.duration.{Duration, DurationInt, FiniteDuration}\nimport scala.language.postfixOps\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport common.StreamLogging\nimport org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest\nimport org.apache.pekko.http.scaladsl.testkit.RouteTestTimeout\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.{FeatureFlags, WhiskConfig}\nimport org.apache.openwhisk.core.connector.ActivationMessage\nimport org.apache.openwhisk.core.containerpool.logging.LogStoreProvider\nimport org.apache.openwhisk.core.controller.{CustomHeaders, RestApiCommons, WhiskServices}\nimport org.apache.openwhisk.core.database.{\n  ActivationStoreLevel,\n  ActivationStoreProvider,\n  CacheChangeNotification,\n  DocumentFactory,\n  UserContext\n}\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entitlement._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancer\nimport org.apache.openwhisk.spi.SpiLoader\n\nprotected trait ControllerTestCommon\n    extends AnyFlatSpec\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with ScalatestRouteTest\n    with Matchers\n    with DbUtils\n    with ExecHelpers\n    with WhiskServices\n    with StreamLogging\n    with CustomHeaders {\n\n  val activeAckTopicIndex = ControllerInstanceId(\"0\")\n\n  implicit val routeTestTimeout = RouteTestTimeout(90 seconds)\n\n  override implicit val actorSystem = system // defined in ScalatestRouteTest\n  override val executionContext = actorSystem.dispatcher\n\n  override val whiskConfig = new WhiskConfig(RestApiCommons.requiredProperties ++ WhiskConfig.kafkaHosts)\n  assert(whiskConfig.isValid)\n\n  // initialize runtimes manifest\n  ExecManifest.initialize(whiskConfig)\n\n  override val loadBalancer = new DegenerateLoadBalancerService(whiskConfig)\n\n  override lazy val entitlementProvider: EntitlementProvider =\n    new LocalEntitlementProvider(whiskConfig, loadBalancer, instance)\n\n  override val activationIdFactory = new ActivationId.ActivationIdGenerator {\n    // need a static activation id to test activations api\n    private val fixedId = ActivationId.generate()\n    override def make = fixedId\n  }\n\n  implicit val cacheChangeNotification = Some {\n    new CacheChangeNotification {\n      override def apply(k: CacheKey): Future[Unit] = Future.successful(())\n    }\n  }\n\n  def checkWhiskEntityResponse(response: WhiskEntity, expected: WhiskEntity): Unit = {\n    // Used to ignore `updated` field because timestamp is not known before inserting into the DB\n    // If you use this method, test case that checks timestamp must be added\n    val r = response match {\n      case whiskAction: WhiskAction                 => whiskAction.copy(updated = expected.updated)\n      case whiskActionMetaData: WhiskActionMetaData => whiskActionMetaData.copy(updated = expected.updated)\n      case whiskTrigger: WhiskTrigger               => whiskTrigger.copy(updated = expected.updated)\n      case whiskPackage: WhiskPackage               => whiskPackage.copy(updated = expected.updated)\n    }\n    r should be(expected)\n  }\n\n  def systemAnnotations(kind: String, create: Boolean = true): Parameters = {\n    val base = if (create && FeatureFlags.requireApiKeyAnnotation) {\n      Parameters(Annotations.ProvideApiKeyAnnotationName, JsFalse)\n    } else {\n      Parameters()\n    }\n    base ++ Parameters(WhiskAction.execFieldName, kind)\n  }\n\n  val entityStore = WhiskEntityStore.datastore()\n  val authStore = WhiskAuthStore.datastore()\n  val logStore = SpiLoader.get[LogStoreProvider].instance(actorSystem)\n  val activationStore = SpiLoader.get[ActivationStoreProvider].instance(actorSystem, logging)\n\n  def deleteAction(doc: DocId)(implicit transid: TransactionId) = {\n    Await.result(WhiskAction.get(entityStore, doc) flatMap { doc =>\n      logging.debug(this, s\"deleting ${doc.docinfo}\")\n      WhiskAction.del(entityStore, doc.docinfo)\n    }, dbOpTimeout)\n  }\n\n  def getActivation(activationId: ActivationId, context: UserContext)(\n    implicit transid: TransactionId,\n    timeout: Duration = 10 seconds): WhiskActivation = {\n    Await.result(activationStore.get(activationId, context), timeout)\n  }\n\n  def storeActivation(\n    activation: WhiskActivation,\n    isBlockingActivation: Boolean,\n    blockingStoreLevel: ActivationStoreLevel.Value,\n    nonBlockingStoreLevel: ActivationStoreLevel.Value,\n    context: UserContext)(implicit transid: TransactionId, timeout: Duration = 10 seconds): DocInfo = {\n    val docFuture = activationStore.storeAfterCheck(\n      activation,\n      isBlockingActivation,\n      Some(blockingStoreLevel),\n      Some(nonBlockingStoreLevel),\n      context)(transid, notifier = None, logging)\n    val doc = Await.result(docFuture, timeout)\n    assert(doc != null)\n    doc\n  }\n\n  def deleteActivation(activationId: ActivationId, context: UserContext)(implicit transid: TransactionId) = {\n    val res = Await.result(activationStore.delete(activationId, context), dbOpTimeout)\n    assert(res, true)\n    res\n  }\n\n  def waitOnListActivationsInNamespace(namespace: EntityPath, count: Int, context: UserContext)(\n    implicit ec: ExecutionContext,\n    transid: TransactionId,\n    timeout: Duration) = {\n    val success = retry(\n      () => {\n        val activations: Future[Either[List[JsObject], List[WhiskActivation]]] =\n          activationStore.listActivationsInNamespace(namespace, 0, 0, context = context)\n        val listFuture: Future[List[JsObject]] = activations map (_.fold(\n          (js) => js,\n          (wa) => wa.map(_.toExtendedJson())))\n\n        listFuture map { l =>\n          if (l.length != count) {\n            throw RetryOp()\n          } else true\n        }\n      },\n      timeout)\n\n    assert(success.isSuccess, \"wait aborted\")\n  }\n\n  def waitOnListActivationsMatchingName(namespace: EntityPath, name: EntityPath, count: Int, context: UserContext)(\n    implicit ex: ExecutionContext,\n    transid: TransactionId,\n    timeout: Duration) = {\n    val success = retry(\n      () => {\n        val activations: Future[Either[List[JsObject], List[WhiskActivation]]] =\n          activationStore.listActivationsMatchingName(namespace, name, 0, 0, context = context)\n        val listFuture: Future[List[JsObject]] = activations map (_.fold(\n          (js) => js,\n          (wa) => wa.map(_.toExtendedJson())))\n\n        listFuture map { l =>\n          if (l.length != count) {\n            throw RetryOp()\n          } else true\n        }\n      },\n      timeout)\n\n    assert(success.isSuccess, \"wait aborted\")\n  }\n\n  def deleteTrigger(doc: DocId)(implicit transid: TransactionId) = {\n    Await.result(WhiskTrigger.get(entityStore, doc) flatMap { doc =>\n      logging.debug(this, s\"deleting ${doc.docinfo}\")\n      WhiskAction.del(entityStore, doc.docinfo)\n    }, dbOpTimeout)\n  }\n\n  def deleteRule(doc: DocId)(implicit transid: TransactionId) = {\n    Await.result(WhiskRule.get(entityStore, doc) flatMap { doc =>\n      logging.debug(this, s\"deleting ${doc.docinfo}\")\n      WhiskRule.del(entityStore, doc.docinfo)\n    }, dbOpTimeout)\n  }\n\n  def deletePackage(doc: DocId)(implicit transid: TransactionId) = {\n    Await.result(WhiskPackage.get(entityStore, doc) flatMap { doc =>\n      logging.debug(this, s\"deleting ${doc.docinfo}\")\n      WhiskPackage.del(entityStore, doc.docinfo)\n    }, dbOpTimeout)\n  }\n\n  def stringToFullyQualifiedName(s: String) = FullyQualifiedEntityName.serdes.read(JsString(s))\n\n  object MakeName {\n    @volatile var counter = 1\n    def next(prefix: String = \"test\")(): EntityName = {\n      counter = counter + 1\n      EntityName(s\"${prefix}_name$counter\")\n    }\n  }\n\n  Collection.initialize(entityStore)\n\n  val ACTIONS = Collection(Collection.ACTIONS)\n  val TRIGGERS = Collection(Collection.TRIGGERS)\n  val RULES = Collection(Collection.RULES)\n  val ACTIVATIONS = Collection(Collection.ACTIVATIONS)\n  val NAMESPACES = Collection(Collection.NAMESPACES)\n  val PACKAGES = Collection(Collection.PACKAGES)\n\n  override def afterEach() = {\n    cleanup()\n  }\n\n  override def afterAll() = {\n    println(\"Shutting down db connections\");\n    entityStore.shutdown()\n    authStore.shutdown()\n  }\n\n  protected case class BadEntity(namespace: EntityPath,\n                                 override val name: EntityName,\n                                 version: SemVer = SemVer(),\n                                 publish: Boolean = false,\n                                 annotations: Parameters = Parameters())\n      extends WhiskEntity(name, \"badEntity\") {\n\n    override def toJson = BadEntity.serdes.write(this).asJsObject\n  }\n\n  protected object BadEntity extends DocumentFactory[BadEntity] with DefaultJsonProtocol {\n    implicit val serdes = jsonFormat5(BadEntity.apply)\n    override val cacheEnabled = true\n  }\n\n  /**\n   * Makes a simple sequence action and installs it in the db (no call to wsk api/cli).\n   * All actions are in the default package.\n   *\n   * @param sequenceName the name of the sequence\n   * @param ns           the namespace to be used when creating the component actions and the sequence action\n   * @param components   the names of the actions (entity names, no namespace)\n   */\n  protected def putSimpleSequenceInDB(sequenceName: String, ns: EntityPath, components: Vector[String])(\n    implicit tid: TransactionId) = {\n    val seqAction = makeSimpleSequence(sequenceName, ns, components)\n    put(entityStore, seqAction)\n  }\n\n  /**\n   * Returns a WhiskAction that can be used to create/update a sequence.\n   * If instructed to do so, installs the component actions in the db.\n   * All actions are in the default package.\n   *\n   * @param sequenceName   the name of the sequence\n   * @param ns             the namespace to be used when creating the component actions and the sequence action\n   * @param componentNames the names of the actions (entity names, no namespace)\n   * @param installDB      if true, installs the component actions in the db (default true)\n   */\n  protected def makeSimpleSequence(sequenceName: String,\n                                   ns: EntityPath,\n                                   componentNames: Vector[String],\n                                   installDB: Boolean = true)(implicit tid: TransactionId): WhiskAction = {\n    if (installDB) {\n      // create bogus wsk actions\n      val wskActions = componentNames.toSet[String] map { c =>\n        WhiskAction(ns, EntityName(c), jsDefault(\"??\"))\n      }\n      // add them to the db\n      wskActions.foreach {\n        put(entityStore, _)\n      }\n    }\n    // add namespace to component names\n    val components = componentNames map { c =>\n      stringToFullyQualifiedName(s\"/$ns/$c\")\n    }\n    // create wsk action for the sequence\n    WhiskAction(ns, EntityName(sequenceName), sequence(components))\n  }\n}\n\nclass DegenerateLoadBalancerService(config: WhiskConfig)(implicit ec: ExecutionContext) extends LoadBalancer {\n  import scala.concurrent.blocking\n\n  // unit tests that need an activation via active ack/fast path should set this to value expected\n  var whiskActivationStub: Option[(FiniteDuration, Either[ActivationId, WhiskActivation])] = None\n  var activationMessageChecker: Option[ActivationMessage => Unit] = None\n\n  override def totalActiveActivations = Future.successful(0)\n  override def activeActivationsFor(namespace: UUID) = Future.successful(0)\n  override def activeActivationsByController(controller: String): Future[Int] = Future.successful(0)\n  override def activeActivationsByController: Future[List[(String, String)]] = Future.successful(List((\"\", \"\")))\n  override def activeActivationsByInvoker(invoker: String): Future[Int] = Future.successful(0)\n\n  override def publish(action: ExecutableWhiskActionMetaData, msg: ActivationMessage)(\n    implicit transid: TransactionId): Future[Future[Either[ActivationId, WhiskActivation]]] = {\n    activationMessageChecker.foreach(_(msg))\n\n    Future.successful {\n      whiskActivationStub map {\n        case (timeout, activation) =>\n          Future {\n            blocking {\n              println(s\"load balancer active ack stub: waiting for $timeout...\")\n              Thread.sleep(timeout.toMillis)\n              println(\".... done waiting\")\n            }\n            activation\n          }\n      } getOrElse Future.failed(new IllegalArgumentException(\"Unit test does not need fast path\"))\n    }\n  }\n\n  override def invokerHealth() = Future.successful(IndexedSeq.empty)\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/EntitlementProviderTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport scala.collection.mutable.ListBuffer\nimport scala.concurrent.Await\nimport scala.concurrent.duration.DurationInt\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.openwhisk.core.controller.RejectRequest\nimport org.apache.openwhisk.core.entitlement._\nimport org.apache.openwhisk.core.entitlement.Privilege._\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.http.Messages\n\n/**\n * Tests authorization handler which guards resources.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass EntitlementProviderTests extends ControllerTestCommon with ScalaFutures {\n\n  behavior of \"Entitlement Provider\"\n\n  val requestTimeout = 10.seconds\n  val someUser = WhiskAuthHelpers.newIdentity()\n  val anotherUser = WhiskAuthHelpers.newIdentity()\n  val adminUser = WhiskAuthHelpers.newIdentity(Subject(\"admin\"))\n  val guestUser = WhiskAuthHelpers.newIdentity(Subject(\"anonym\"))\n\n  val allowedKinds = Set(\"nodejs:20\", \"python\")\n  val disallowedKinds = Set(\"golang\", \"blackbox\")\n\n  def getExec(kind: String): Exec = {\n    CodeExecAsString(RuntimeManifest(kind, ImageName(kind ++ \"action\")), \"function main(){}\", None)\n  }\n\n  it should \"authorize a user to only read from their collection\" in {\n    implicit val tid = transid()\n    val collections = Seq(ACTIONS, RULES, TRIGGERS, PACKAGES, ACTIVATIONS, NAMESPACES)\n    val resources = collections map { Resource(someUser.namespace.name.toPath, _, None) }\n\n    resources foreach { r =>\n      Await.ready(entitlementProvider.check(someUser, READ, r), requestTimeout).eitherValue.get shouldBe Right({})\n      Await.ready(entitlementProvider.check(someUser, PUT, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(someUser, DELETE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(someUser, ACTIVATE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(someUser, REJECT, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n    }\n  }\n\n  it should \"authorize a user to only read from their set of collections\" in {\n    implicit val tid = transid()\n    val collections = Seq(ACTIONS, RULES, TRIGGERS, PACKAGES, ACTIVATIONS, NAMESPACES)\n    val resources = collections map { c: Collection =>\n      c match {\n        case RULES      => Resource(anotherUser.namespace.name.toPath, c, None)\n        case NAMESPACES => Resource(anotherUser.namespace.name.toPath, c, None)\n        case _          => Resource(someUser.namespace.name.toPath, c, None)\n      }\n    }\n\n    // Sets aren't ordered, but we need to compared an ordered list of namespaces in the output; so\n    // create a sorted list of namespaces per the iterated through the set.  The output list of namespaces\n    // will also be in sorted order.\n    val resourcesSet = resources.toSet\n    val resourcesList = ListBuffer[Resource]()\n    resourcesSet.map(r => resourcesList += r)\n    val resourceNames = resourcesList.map(r => r.fqname).sorted.toSet.mkString(\", \")\n    val resourceOtherNames = Seq(\n      Resource(anotherUser.namespace.name.toPath, RULES, None),\n      Resource(anotherUser.namespace.name.toPath, NAMESPACES, None)).map(r => r.fqname).toSet.mkString(\", \")\n\n    Await.ready(entitlementProvider.check(someUser, READ, resourcesSet), requestTimeout).eitherValue.get shouldBe Left(\n      RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceOtherNames)))\n    Await.ready(entitlementProvider.check(someUser, PUT, resourcesSet), requestTimeout).eitherValue.get shouldBe Left(\n      RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceNames)))\n    Await\n      .ready(entitlementProvider.check(someUser, DELETE, resourcesSet), requestTimeout)\n      .eitherValue\n      .get shouldBe Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceNames)))\n    Await\n      .ready(entitlementProvider.check(someUser, ACTIVATE, resourcesSet), requestTimeout)\n      .eitherValue\n      .get shouldBe Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceNames)))\n    Await\n      .ready(entitlementProvider.check(someUser, REJECT, resourcesSet), requestTimeout)\n      .eitherValue\n      .get shouldBe Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceNames)))\n  }\n\n  it should \"not authorize a user to list someone else's collection or access it by other other right\" in {\n    implicit val tid = transid()\n    val collections = Seq(ACTIONS, RULES, TRIGGERS, PACKAGES, ACTIVATIONS, NAMESPACES)\n    val resources = collections map { Resource(someUser.namespace.name.toPath, _, None) }\n    resources foreach { r =>\n      // it is permissible to list packages in any namespace (provided they are either owned by\n      // the subject requesting access or the packages are public); that is, the entitlement is more\n      // fine grained and applies to public vs private private packages (hence permit READ on PACKAGES to\n      // be true\n      if ((r.collection == PACKAGES)) {\n        Await.ready(entitlementProvider.check(guestUser, READ, r), requestTimeout).eitherValue.get shouldBe Right({})\n      } else {\n        Await.ready(entitlementProvider.check(guestUser, READ, r), requestTimeout).eitherValue.get shouldBe Left(\n          RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      }\n      Await.ready(entitlementProvider.check(guestUser, PUT, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(guestUser, DELETE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(guestUser, ACTIVATE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(guestUser, REJECT, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n    }\n  }\n\n  it should \"authorize a user to CRUD or activate (if supported) an entity in a collection\" in {\n    implicit val tid = transid()\n    // packages are tested separately\n    val collections = Seq(ACTIONS, RULES, TRIGGERS)\n    val resources = collections map { Resource(someUser.namespace.name.toPath, _, Some(\"xyz\")) }\n    resources foreach { r =>\n      Await.ready(entitlementProvider.check(someUser, READ, r), requestTimeout).eitherValue.get shouldBe Right({})\n      Await.ready(entitlementProvider.check(someUser, PUT, r), requestTimeout).eitherValue.get shouldBe Right({})\n      Await.ready(entitlementProvider.check(someUser, DELETE, r), requestTimeout).eitherValue.get shouldBe Right({})\n      Await.ready(entitlementProvider.check(someUser, ACTIVATE, r), requestTimeout).eitherValue.get shouldBe Right({})\n    }\n  }\n\n  it should \"not authorize a user to CRUD an entity in a collection if authkey has no CRUD rights\" in {\n    implicit val tid = transid()\n    val subject = Subject()\n    val uuid = UUID()\n    val someUser =\n      Identity(\n        subject,\n        Namespace(EntityName(subject.asString), uuid),\n        BasicAuthenticationAuthKey(uuid, Secret()),\n        rights = Set(Privilege.ACTIVATE))\n    val collections = Seq(ACTIONS, RULES, TRIGGERS)\n    val resources = collections map { Resource(someUser.namespace.name.toPath, _, Some(\"xyz\")) }\n    resources foreach { r =>\n      Await.ready(entitlementProvider.check(someUser, READ, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(someUser, PUT, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(someUser, DELETE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(someUser, ACTIVATE, r), requestTimeout).eitherValue.get shouldBe Right({})\n    }\n  }\n\n  it should \"not authorize a user to CRUD or activate an entity in a collection that does not support CRUD or activate\" in {\n    implicit val tid = transid()\n    val collections = Seq(NAMESPACES, ACTIVATIONS)\n    val resources = collections map { Resource(someUser.namespace.name.toPath, _, Some(\"xyz\")) }\n    resources foreach { r =>\n      Await.ready(entitlementProvider.check(someUser, READ, r), requestTimeout).eitherValue.get shouldBe Right({})\n      Await.ready(entitlementProvider.check(someUser, PUT, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(someUser, DELETE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(someUser, ACTIVATE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n    }\n  }\n\n  it should \"not authorize a user to CRUD or activate an entity in someone else's collection\" in {\n    implicit val tid = transid()\n    val collections = Seq(ACTIONS, RULES, TRIGGERS, PACKAGES)\n    val resources = collections map { Resource(someUser.namespace.name.toPath, _, Some(\"xyz\")) }\n    resources foreach { r =>\n      Await.ready(entitlementProvider.check(guestUser, READ, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(guestUser, PUT, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(guestUser, DELETE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n      Await.ready(entitlementProvider.check(guestUser, ACTIVATE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n    }\n  }\n\n  it should \"authorize a user to list, create/update/delete a package\" in {\n    implicit val tid = transid()\n    val collections = Seq(PACKAGES)\n    val resources = collections map { Resource(someUser.namespace.name.toPath, _, Some(\"xyz\")) }\n    resources foreach { r =>\n      // read should fail because the lookup for the package will fail\n      Await.ready(entitlementProvider.check(someUser, READ, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(NotFound))\n      // create/put/delete should be allowed\n      Await.ready(entitlementProvider.check(someUser, PUT, r), requestTimeout).eitherValue.get shouldBe Right({})\n      Await.ready(entitlementProvider.check(someUser, DELETE, r), requestTimeout).eitherValue.get shouldBe Right({})\n      // activate is not allowed on a package\n      Await.ready(entitlementProvider.check(someUser, ACTIVATE, r), requestTimeout).eitherValue.get shouldBe Left(\n        RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(r.fqname)))\n    }\n  }\n\n  it should \"grant access to entire collection to another user\" in {\n    implicit val tid = transid()\n    val all = Resource(someUser.namespace.name.toPath, ACTIONS, None)\n    val one = Resource(someUser.namespace.name.toPath, ACTIONS, Some(\"xyz\"))\n    Await.ready(entitlementProvider.check(adminUser, READ, all), requestTimeout).eitherValue.get should not be Right({})\n    Await.ready(entitlementProvider.check(adminUser, READ, one), requestTimeout).eitherValue.get should not be Right({})\n    Await.result(entitlementProvider.grant(adminUser, READ, all), requestTimeout) // granted\n    Await.ready(entitlementProvider.check(adminUser, READ, all), requestTimeout).eitherValue.get shouldBe Right({})\n    Await.ready(entitlementProvider.check(adminUser, READ, one), requestTimeout).eitherValue.get shouldBe Right({})\n    Await.result(entitlementProvider.revoke(adminUser, READ, all), requestTimeout) // revoked\n  }\n\n  it should \"grant access to specific resource to a user\" in {\n    implicit val tid = transid()\n    val all = Resource(someUser.namespace.name.toPath, ACTIONS, None)\n    val one = Resource(someUser.namespace.name.toPath, ACTIONS, Some(\"xyz\"))\n    Await.ready(entitlementProvider.check(adminUser, READ, all), requestTimeout).eitherValue.get should not be Right({})\n    Await.ready(entitlementProvider.check(adminUser, READ, one), requestTimeout).eitherValue.get should not be Right({})\n    Await\n      .ready(entitlementProvider.check(adminUser, DELETE, one), requestTimeout)\n      .eitherValue\n      .get should not be Right({})\n    Await.result(entitlementProvider.grant(adminUser, READ, one), requestTimeout) // granted\n    Await.ready(entitlementProvider.check(adminUser, READ, all), requestTimeout).eitherValue.get should not be Right({})\n    Await.ready(entitlementProvider.check(adminUser, READ, one), requestTimeout).eitherValue.get shouldBe Right({})\n    Await\n      .ready(entitlementProvider.check(adminUser, DELETE, one), requestTimeout)\n      .eitherValue\n      .get should not be Right({})\n    Await.result(entitlementProvider.revoke(adminUser, READ, one), requestTimeout) // revoked\n  }\n\n  behavior of \"Package Collection\"\n\n  it should \"only allow read access for listing package collection\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val paths = Seq(\n      (READ, someUser, Right(true)),\n      (PUT, someUser, Right(false)),\n      (DELETE, someUser, Right(false)),\n      (ACTIVATE, someUser, Right(false)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(true)),\n      (PUT, guestUser, Right(false)),\n      (DELETE, guestUser, Right(false)),\n      (ACTIVATE, guestUser, Right(false)),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new PackageCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          // any user can list any namespace packages\n          // (because this performs a db view lookup which is later filtered)\n          Resource(someUser.namespace.name.toPath, PACKAGES, None))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"reject entitlement if package doesn't exist\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val paths = Seq(\n      (READ, someUser, Left(RejectRequest(NotFound))), // for owner, give more information\n      (PUT, someUser, Right(true)),\n      (DELETE, someUser, Right(true)),\n      (ACTIVATE, someUser, Right(false)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(false)),\n      (PUT, guestUser, Right(false)),\n      (DELETE, guestUser, Right(false)),\n      (ACTIVATE, guestUser, Right(false)),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new PackageCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(someUser.namespace.name.toPath, PACKAGES, Some(\"xyz\")))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"reject entitlement if package doesn't deserialize\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val paths = Seq(\n      (READ, someUser, Left(RejectRequest(Conflict, Messages.conformanceMessage))), // for owner, give more information\n      (PUT, someUser, Right(true)),\n      (DELETE, someUser, Right(true)),\n      (ACTIVATE, someUser, Right(false)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(false)),\n      (PUT, guestUser, Right(false)),\n      (DELETE, guestUser, Right(false)),\n      (ACTIVATE, guestUser, Right(false)),\n      (REJECT, guestUser, Right(false)))\n\n    // this forces a doc mismatch error\n    val action = WhiskAction(someUser.namespace.name.toPath, MakeName.next(), jsDefault(\"\"))\n    put(entityStore, action)\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new PackageCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(someUser.namespace.name.toPath, PACKAGES, Some(action.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"not allow guest access to private package\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val provider = WhiskPackage(someUser.namespace.name.toPath, MakeName.next())\n    put(entityStore, provider)\n\n    val paths = Seq(\n      (READ, someUser, Right(true)),\n      (PUT, someUser, Right(true)),\n      (DELETE, someUser, Right(true)),\n      (ACTIVATE, someUser, Right(false)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(false)),\n      (PUT, guestUser, Right(false)),\n      (DELETE, guestUser, Right(false)),\n      (ACTIVATE, guestUser, Right(false)),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new PackageCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(someUser.namespace.name.toPath, PACKAGES, Some(provider.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"not allow guest access to binding of private package\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    // simulate entitlement change on package for which binding was once entitled\n    val provider = WhiskPackage(someUser.namespace.name.toPath, MakeName.next())\n    val binding = WhiskPackage(guestUser.namespace.name.toPath, MakeName.next(), provider.bind)\n    put(entityStore, provider, false)\n    put(entityStore, binding)\n\n    val paths = Seq(\n      (READ, someUser, Right(false)),\n      (PUT, someUser, Right(false)),\n      (DELETE, someUser, Right(false)),\n      (ACTIVATE, someUser, Right(false)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(false)), // not allowed to read referenced package\n      (PUT, guestUser, Right(true)), // can update\n      (DELETE, guestUser, Right(true)), // and delete the binding however\n      (ACTIVATE, guestUser, Right(false)),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new PackageCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(guestUser.namespace.name.toPath, PACKAGES, Some(binding.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n\n    // simulate package deletion for which binding was once entitled\n    deletePackage(provider.docid)\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new PackageCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(guestUser.namespace.name.toPath, PACKAGES, Some(binding.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"not allow guest access to public binding of package\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    // simulate entitlement change on package for which binding was once entitled\n    val provider = WhiskPackage(someUser.namespace.name.toPath, MakeName.next(), None, publish = true)\n    val binding = WhiskPackage(guestUser.namespace.name.toPath, MakeName.next(), provider.bind, publish = true)\n    put(entityStore, provider)\n    put(entityStore, binding)\n\n    val paths = Seq(\n      (READ, someUser, Right(false)), // cannot access a public binding\n      (PUT, someUser, Right(false)),\n      (DELETE, someUser, Right(false)),\n      (ACTIVATE, someUser, Right(false)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(true)), // can read\n      (PUT, guestUser, Right(true)), // can update\n      (DELETE, guestUser, Right(true)), // and delete the binding\n      (ACTIVATE, guestUser, Right(false)),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new PackageCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(guestUser.namespace.name.toPath, PACKAGES, Some(binding.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"allow guest access to binding of public package\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val provider = WhiskPackage(someUser.namespace.name.toPath, MakeName.next(), None, publish = true)\n    val binding = WhiskPackage(guestUser.namespace.name.toPath, MakeName.next(), provider.bind)\n    put(entityStore, provider)\n    put(entityStore, binding)\n\n    val paths = Seq(\n      (READ, someUser, Right(false)),\n      (PUT, someUser, Right(false)),\n      (DELETE, someUser, Right(false)),\n      (ACTIVATE, someUser, Right(false)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(true)), // can read package + binding\n      (PUT, guestUser, Right(true)), // can update\n      (DELETE, guestUser, Right(true)), // and delete the binding\n      (ACTIVATE, guestUser, Right(false)),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new PackageCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(guestUser.namespace.name.toPath, PACKAGES, Some(binding.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  behavior of \"Action Collection\"\n\n  it should \"only allow read access for listing action collection\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val paths = Seq(\n      (READ, someUser, Right(true)),\n      (PUT, someUser, Right(false)),\n      (DELETE, someUser, Right(false)),\n      (ACTIVATE, someUser, Right(false)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(false)),\n      (PUT, guestUser, Right(false)),\n      (DELETE, guestUser, Right(false)),\n      (ACTIVATE, guestUser, Right(false)),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new ActionCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          // any user can list any namespace packages\n          // (because this performs a db view lookup which is later filtered)\n          Resource(someUser.namespace.name.toPath, ACTIONS, None))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"allow guest access to read or activate an action in a package if package is public\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val paths = Seq(\n      (READ, someUser, Right(true)),\n      (PUT, someUser, Right(true)),\n      (DELETE, someUser, Right(true)),\n      (ACTIVATE, someUser, Right(true)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(true)),\n      (PUT, guestUser, Right(false)),\n      (DELETE, guestUser, Right(false)),\n      (ACTIVATE, guestUser, Right(true)),\n      (REJECT, guestUser, Right(false)))\n\n    val provider = WhiskPackage(someUser.namespace.name.toPath, MakeName.next(), None, publish = true)\n    val action = WhiskAction(provider.fullPath, MakeName.next(), jsDefault(\"\"))\n    put(entityStore, provider)\n    put(entityStore, action)\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new ActionCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(action.namespace, ACTIONS, Some(action.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"reject guest access to read or activate an action in a package if package is private\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val provider = WhiskPackage(someUser.namespace.name.toPath, MakeName.next(), None, publish = false)\n    val action = WhiskAction(provider.fullPath, MakeName.next(), jsDefault(\"\"))\n    put(entityStore, provider)\n    put(entityStore, action)\n\n    val resourceName = provider.fullyQualifiedName(false).asString\n\n    val paths = Seq(\n      (READ, someUser, Right(true)),\n      (PUT, someUser, Right(true)),\n      (DELETE, someUser, Right(true)),\n      (ACTIVATE, someUser, Right(true)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceName)))),\n      (PUT, guestUser, Right(false)),\n      (DELETE, guestUser, Right(false)),\n      (ACTIVATE, guestUser, Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceName)))),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new ActionCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(action.namespace, ACTIONS, Some(action.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"allow guest access to read or activate an action in a package binding if package is public\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val provider = WhiskPackage(someUser.namespace.name.toPath, MakeName.next(), None, publish = true)\n    val binding = WhiskPackage(guestUser.namespace.name.toPath, MakeName.next(), provider.bind)\n    val action = WhiskAction(binding.fullPath, MakeName.next(), jsDefault(\"\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    put(entityStore, action)\n\n    val resourceName = binding.fullyQualifiedName(false).asString\n\n    val paths = Seq(\n      (READ, someUser, Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceName)))),\n      (PUT, someUser, Right(false)),\n      (DELETE, someUser, Right(false)),\n      (ACTIVATE, someUser, Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceName)))),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(true)),\n      (PUT, guestUser, Right(true)),\n      (DELETE, guestUser, Right(true)),\n      (ACTIVATE, guestUser, Right(true)),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new ActionCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(action.namespace, ACTIONS, Some(action.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"reject guest access to read or activate an action in a package binding if package is private\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val provider = WhiskPackage(someUser.namespace.name.toPath, MakeName.next(), None, publish = false)\n    val binding = WhiskPackage(guestUser.namespace.name.toPath, MakeName.next(), provider.bind)\n    val action = WhiskAction(binding.fullPath, MakeName.next(), jsDefault(\"\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    put(entityStore, action)\n\n    val resourceName = binding.fullyQualifiedName(false).asString\n\n    val paths = Seq(\n      (READ, someUser, Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceName)))),\n      (PUT, someUser, Right(false)),\n      (DELETE, someUser, Right(false)),\n      (ACTIVATE, someUser, Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceName)))),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceName)))),\n      (PUT, guestUser, Right(true)),\n      (DELETE, guestUser, Right(true)),\n      (ACTIVATE, guestUser, Left(RejectRequest(Forbidden, Messages.notAuthorizedtoAccessResource(resourceName)))),\n      (REJECT, guestUser, Right(false)))\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new ActionCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(action.namespace, ACTIONS, Some(action.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"reject guest access to read or activate an action in default package\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n\n    val paths = Seq(\n      (READ, someUser, Right(true)),\n      (PUT, someUser, Right(true)),\n      (DELETE, someUser, Right(true)),\n      (ACTIVATE, someUser, Right(true)),\n      (REJECT, someUser, Right(false)),\n      (READ, guestUser, Right(false)),\n      (PUT, guestUser, Right(false)),\n      (DELETE, guestUser, Right(false)),\n      (ACTIVATE, guestUser, Right(false)),\n      (REJECT, guestUser, Right(false)))\n\n    val action = WhiskAction(someUser.namespace.name.toPath, MakeName.next(), jsDefault(\"\"))\n    put(entityStore, action)\n\n    paths foreach {\n      case (priv, who, expected) =>\n        val check = new ActionCollection(entityStore).implicitRights(\n          who,\n          Set(who.namespace.name.asString),\n          priv,\n          Resource(action.namespace, ACTIONS, Some(action.name.asString)))\n        Await.ready(check, requestTimeout).eitherValue.get shouldBe expected\n    }\n  }\n\n  it should \"restrict access to disallowed action kinds for a subject\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(allowedKinds)))\n\n    disallowedKinds.foreach(k => {\n      val ex = intercept[RejectRequest] {\n        Await.result(ep.check(subject, Some(getExec(k))), 1.seconds)\n      }\n\n      ex.code shouldBe Forbidden\n      ex.message.get.error shouldBe Messages.notAuthorizedtoActionKind(k)\n    })\n  }\n\n  it should \"allow access to whitelisted action kinds for a subject\" in {\n    implicit val tid = transid()\n    implicit val ep = entitlementProvider\n    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(allowedKinds)))\n\n    Await.result(ep.check(subject, None), 1.seconds)\n    allowedKinds.foreach(k => Await.result(ep.check(subject, Some(getExec(k))), 1.seconds))\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/FPCEntitlementTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.controller.RejectRequest\nimport org.apache.openwhisk.core.entitlement.{EntitlementProvider, FPCEntitlementProvider, Privilege, Resource}\nimport org.apache.openwhisk.core.entitlement.Privilege.{ACTIVATE, DELETE, PUT, READ, REJECT}\nimport org.apache.openwhisk.core.entity.{EntityName, EntityPath, FullyQualifiedEntityName}\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancer\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass FPCEntitlementProviderTests extends ControllerTestCommon with ScalaFutures with MockFactory {\n\n  implicit val transactionId = TransactionId.testing\n\n  it should \"get throttle flag from loadBalancer\" in {\n    val someUser = WhiskAuthHelpers.newIdentity()\n    val action = FullyQualifiedEntityName(EntityPath(\"testns\"), EntityName(\"action\"))\n    val loadBalancer = mock[LoadBalancer]\n    (loadBalancer.clusterSize _).expects().returning(1).anyNumberOfTimes()\n    (loadBalancer\n      .checkThrottle(_: EntityPath, _: String))\n      .expects(someUser.namespace.name.toPath, action.fullPath.asString)\n      .returning(true)\n    val resources = Set(Resource(action.path, ACTIONS, Some(action.name.name)))\n\n    val entitlementProvider: EntitlementProvider = new FPCEntitlementProvider(whiskConfig, loadBalancer, instance)\n    entitlementProvider.checkThrottles(someUser, ACTIVATE, resources).failed.futureValue shouldBe a[RejectRequest]\n\n    Seq[Privilege](READ, PUT, DELETE, REJECT).foreach(OP => {\n      noException shouldBe thrownBy(entitlementProvider.checkThrottles(someUser, OP, resources).futureValue)\n    })\n\n    val action2 = FullyQualifiedEntityName(EntityPath(\"testns2\"), EntityName(\"action2\"))\n    val resources2 = Set(Resource(action2.path, ACTIONS, Some(action2.name.name)))\n    (loadBalancer\n      .checkThrottle(_: EntityPath, _: String))\n      .expects(someUser.namespace.name.toPath, action2.fullPath.asString)\n      .returning(false)\n    noException shouldBe thrownBy(entitlementProvider.checkThrottles(someUser, ACTIVATE, resources2).futureValue)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/KindRestrictorTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport common.StreamLogging\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.core.entitlement.KindRestrictor\nimport org.apache.openwhisk.core.entity._\n\n/**\n * Tests authorization handler which guards resources.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n */\n@RunWith(classOf[JUnitRunner])\nclass KindRestrictorTests extends AnyFlatSpec with Matchers with StreamLogging {\n\n  behavior of \"Kind Restrictor\"\n\n  val allowedKinds = Set(\"nodejs:20\", \"python\")\n  val disallowedKinds = Set(\"golang\", \"blackbox\")\n  val allKinds = allowedKinds ++ disallowedKinds\n\n  it should \"grant subject access to all kinds when no limits exist and no white list is defined\" in {\n    val subject = WhiskAuthHelpers.newIdentity()\n    val kr = KindRestrictor()\n    allKinds.foreach(k => kr.check(subject, k) shouldBe true)\n  }\n\n  it should \"grant subject access to any kinds if limit is the empty set\" in {\n    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(Set.empty)))\n    val kr = KindRestrictor()\n    allKinds.foreach(k => kr.check(subject, k) shouldBe true)\n  }\n\n  it should \"grant subject access to any kinds if white list is the empty set\" in {\n    val subject = WhiskAuthHelpers.newIdentity()\n    val kr = KindRestrictor(Set[String]())\n    allKinds.foreach(k => kr.check(subject, k) shouldBe true)\n  }\n\n  it should \"grant subject access only to subject-limited kinds\" in {\n    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(allowedKinds)))\n    val kr = KindRestrictor()\n    allowedKinds.foreach(k => kr.check(subject, k) shouldBe true)\n    disallowedKinds.foreach(k => kr.check(subject, k) shouldBe false)\n  }\n\n  it should \"grant subject access to white listed kinds when no limits exist\" in {\n    val subject = WhiskAuthHelpers.newIdentity()\n    val kr = KindRestrictor(allowedKinds)\n    allowedKinds.foreach(k => kr.check(subject, k) shouldBe true)\n    disallowedKinds.foreach(k => kr.check(subject, k) shouldBe false)\n  }\n\n  it should \"grant subject access both explicitly limited kinds and default whitelisted kinds\" in {\n    val explicitKind = allowedKinds.head\n    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(Set(explicitKind))))\n    val kr = KindRestrictor(allowedKinds.tail)\n    allKinds.foreach(k => kr.check(subject, k) shouldBe allowedKinds.contains(k))\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/LimitsApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.{BadRequest, MethodNotAllowed, OK}\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.openwhisk.core.controller.WhiskLimitsApi\nimport org.apache.openwhisk.core.entity.{\n  EntityPath,\n  IntraConcurrencyLimit,\n  LogLimit,\n  MemoryLimit,\n  TimeLimit,\n  UserLimits\n}\nimport org.apache.openwhisk.core.entity.size._\n\nimport scala.concurrent.duration._\n\n/**\n * Tests Packages API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass LimitsApiTests extends ControllerTestCommon with WhiskLimitsApi {\n\n  /** Limits API tests */\n  behavior of \"Limits API\"\n\n  // test namespace limit configurations\n  val testInvokesPerMinute = 100\n  val testConcurrent = 200\n  val testFiresPerMinute = 300\n  val testAllowedKinds = Set(\"java:8\")\n  val testStoreActivations = false\n\n  val testMemoryMin = MemoryLimit(150.MB)\n  val testMemoryMax = MemoryLimit(200.MB)\n  val testLogMin = LogLimit(3.MB)\n  val testLogMax = LogLimit(6.MB)\n  val testDurationMax = TimeLimit(20.seconds)\n  val testDurationMin = TimeLimit(10.seconds)\n  val testConcurrencyMax = IntraConcurrencyLimit(20)\n  val testConcurrencyMin = IntraConcurrencyLimit(10)\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val credsWithSetLimits = WhiskAuthHelpers\n    .newIdentity()\n    .copy(\n      limits = UserLimits(\n        Some(testInvokesPerMinute),\n        Some(testConcurrent),\n        Some(testFiresPerMinute),\n        Some(testAllowedKinds),\n        Some(testStoreActivations),\n        minActionMemory = Some(testMemoryMin),\n        maxActionMemory = Some(testMemoryMax),\n        minActionLogs = Some(testLogMin),\n        maxActionLogs = Some(testLogMax),\n        maxActionTimeout = Some(testDurationMax),\n        minActionTimeout = Some(testDurationMin),\n        maxActionConcurrency = Some(testConcurrencyMax),\n        minActionConcurrency = Some(testConcurrencyMin)))\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n\n  //// GET /limits\n  it should \"list default system limits if no namespace limits are set\" in {\n    implicit val tid = transid()\n    Seq(\"\", \"/\").foreach { p =>\n      Get(collectionPath + p) ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        responseAs[UserLimits].invocationsPerMinute shouldBe Some(whiskConfig.actionInvokePerMinuteLimit.toInt)\n        responseAs[UserLimits].concurrentInvocations shouldBe Some(whiskConfig.actionInvokeConcurrentLimit.toInt)\n        responseAs[UserLimits].firesPerMinute shouldBe Some(whiskConfig.triggerFirePerMinuteLimit.toInt)\n        responseAs[UserLimits].maxActionInstances shouldBe Some(whiskConfig.actionInvokeConcurrentLimit.toInt)\n\n        responseAs[UserLimits].allowedKinds shouldBe None\n        responseAs[UserLimits].storeActivations shouldBe None\n\n        // provide default action limits\n        responseAs[UserLimits].minActionMemory.get.megabytes shouldBe MemoryLimit.MIN_MEMORY_DEFAULT.toMB\n        responseAs[UserLimits].maxActionMemory.get.megabytes shouldBe MemoryLimit.MAX_MEMORY_DEFAULT.toMB\n        responseAs[UserLimits].minActionLogs.get.megabytes shouldBe LogLimit.MIN_LOGSIZE_DEFAULT.toMB\n        responseAs[UserLimits].maxActionLogs.get.megabytes shouldBe LogLimit.MAX_LOGSIZE_DEFAULT.toMB\n        responseAs[UserLimits].minActionTimeout.get.duration shouldBe TimeLimit.MIN_DURATION_DEFAULT\n        responseAs[UserLimits].maxActionTimeout.get.duration shouldBe TimeLimit.MAX_DURATION_DEFAULT\n        responseAs[UserLimits].minActionConcurrency.get.maxConcurrent shouldBe IntraConcurrencyLimit.MIN_CONCURRENT_DEFAULT\n        responseAs[UserLimits].maxActionConcurrency.get.maxConcurrent shouldBe IntraConcurrencyLimit.MAX_CONCURRENT_DEFAULT\n      }\n    }\n  }\n\n  it should \"list set limits if limits have been set for the namespace\" in {\n    implicit val tid = transid()\n    Seq(\"\", \"/\").foreach { p =>\n      Get(collectionPath + p) ~> Route.seal(routes(credsWithSetLimits)) ~> check {\n        status should be(OK)\n        responseAs[UserLimits].invocationsPerMinute shouldBe Some(testInvokesPerMinute)\n        responseAs[UserLimits].concurrentInvocations shouldBe Some(testConcurrent)\n        responseAs[UserLimits].firesPerMinute shouldBe Some(testFiresPerMinute)\n        responseAs[UserLimits].allowedKinds shouldBe Some(testAllowedKinds)\n        responseAs[UserLimits].storeActivations shouldBe Some(testStoreActivations)\n        responseAs[UserLimits].maxActionInstances shouldBe Some(testConcurrent)\n\n        // provide action limits for namespace\n        responseAs[UserLimits].minActionMemory.get.megabytes shouldBe testMemoryMin.megabytes\n        responseAs[UserLimits].maxActionMemory.get.megabytes shouldBe testMemoryMax.megabytes\n        responseAs[UserLimits].minActionLogs.get.megabytes shouldBe testLogMin.megabytes\n        responseAs[UserLimits].maxActionLogs.get.megabytes shouldBe testLogMax.megabytes\n        responseAs[UserLimits].minActionTimeout.get.duration shouldBe testDurationMin.duration\n        responseAs[UserLimits].maxActionTimeout.get.duration shouldBe testDurationMax.duration\n        responseAs[UserLimits].minActionConcurrency.get.maxConcurrent shouldBe testConcurrencyMin.maxConcurrent\n        responseAs[UserLimits].maxActionConcurrency.get.maxConcurrent shouldBe testConcurrencyMax.maxConcurrent\n      }\n    }\n  }\n\n  it should \"reject requests for unsupported methods\" in {\n    implicit val tid = transid()\n    Seq(Put, Post, Delete).foreach { m =>\n      m(collectionPath) ~> Route.seal(routes(creds)) ~> check {\n        status should be(MethodNotAllowed)\n      }\n    }\n  }\n\n  it should \"reject all methods for entity level request\" in {\n    implicit val tid = transid()\n    Seq(Put, Post, Delete).foreach { m =>\n      m(s\"$collectionPath/limitsEntity\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(MethodNotAllowed)\n      }\n    }\n\n    Seq(Get).foreach { m =>\n      m(s\"$collectionPath/limitsEntity\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(BadRequest)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/NamespacesApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller\nimport org.apache.pekko.http.scaladsl.server.Route\n\nimport spray.json.DefaultJsonProtocol._\n\nimport org.apache.openwhisk.core.controller.WhiskNamespacesApi\nimport org.apache.openwhisk.core.entity.EntityPath\n\n/**\n * Tests Namespaces API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass NamespacesApiTests extends ControllerTestCommon with WhiskNamespacesApi {\n\n  /** Triggers API tests */\n  behavior of \"Namespaces API\"\n\n  val collectionPath = s\"/${collection.path}\"\n  val creds = WhiskAuthHelpers.newIdentity()\n  val namespace = EntityPath(creds.subject.asString)\n\n  it should \"list namespaces for subject\" in {\n    implicit val tid = transid()\n    Seq(\"\", \"/\").foreach { p =>\n      Get(collectionPath + p) ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val ns = responseAs[List[EntityPath]]\n        ns should be(List(EntityPath(creds.subject.asString)))\n      }\n    }\n  }\n\n  it should \"reject request for unsupported method\" in {\n    implicit val tid = transid()\n    Seq(Get, Put, Post, Delete).foreach { m =>\n      m(s\"$collectionPath/${creds.subject}\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(NotFound)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/PackageActionsApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport scala.concurrent.duration.DurationInt\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.server.Route\n\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport org.apache.openwhisk.core.controller.WhiskActionsApi\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entitlement.Resource\nimport org.apache.openwhisk.core.entitlement.Privilege._\nimport scala.concurrent.Await\nimport scala.language.postfixOps\nimport org.apache.openwhisk.http.ErrorResponse\nimport org.apache.openwhisk.http.Messages\n\n/**\n * Tests Packages API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass PackageActionsApiTests extends ControllerTestCommon with WhiskActionsApi {\n\n  /** Package Actions API tests */\n  behavior of \"Package Actions API\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n  def aname() = MakeName.next(\"package_action_tests\")\n\n  //// GET /actions/package/\n  it should \"list all actions in package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val actions = (1 to 2).map { _ =>\n      WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    }\n    put(entityStore, provider)\n    actions foreach { put(entityStore, _) }\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath/${provider.name}/\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[List[JsObject]]\n        actions.length should be(response.length)\n        actions forall { a =>\n          response contains a.summaryAsJson\n        } should be(true)\n      }\n    }\n  }\n\n  it should \"list all actions in package binding\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    val actions = (1 to 2).map { _ =>\n      WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    }\n    put(entityStore, provider)\n    put(entityStore, reference)\n    actions foreach { put(entityStore, _) }\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath/${reference.name}/\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[List[JsObject]]\n        actions.length should be(response.length)\n        actions forall { a =>\n          response contains a.summaryAsJson\n        } should be(true)\n      }\n    }\n  }\n\n  it should \"include action in package when listing all actions\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None)\n    val action1 = WhiskAction(namespace, aname(), jsDefault(\"??\"), Parameters(), ActionLimits())\n    val action2 = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    put(entityStore, provider)\n    put(entityStore, action1)\n    put(entityStore, action2)\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[List[JsObject]]\n        response.length should be(2)\n        response contains action1.summaryAsJson should be(true)\n        response contains action2.summaryAsJson should be(true)\n      }\n    }\n  }\n\n  it should \"reject ambiguous list actions in package without trailing slash\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None)\n    put(entityStore, provider)\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(Conflict)\n      }\n    }\n  }\n\n  it should \"reject invalid verb on get package actions\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None)\n    put(entityStore, provider)\n    Delete(s\"$collectionPath/${provider.name}/\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  //// PUT /actions/package/name\n  it should \"put action in package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = WhiskActionPut(Some(action.exec))\n    put(entityStore, provider)\n    Put(s\"$collectionPath/${provider.name}/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response should be(\n        WhiskAction(\n          action.namespace,\n          action.name,\n          action.exec,\n          action.parameters,\n          action.limits,\n          action.version,\n          action.publish,\n          action.annotations ++ systemAnnotations(NODEJS),\n          updated = response.updated))\n    }\n  }\n\n  it should \"reject put action in package that does not exist\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = WhiskActionPut(Some(action.exec))\n    Put(s\"$collectionPath/${provider.name}/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"reject put action in package binding where package doesn't exist\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None, publish = true)\n    val binding = WhiskPackage(namespace, aname(), provider.bind)\n    val content = WhiskActionPut(Some(jsDefault(\"??\")))\n    put(entityStore, binding)\n    Put(s\"$collectionPath/${binding.name}/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n  it should \"reject put action in package binding\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None, publish = true)\n    val binding = WhiskPackage(namespace, aname(), provider.bind)\n    val content = WhiskActionPut(Some(jsDefault(\"??\")))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    Put(s\"$collectionPath/${binding.name}/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"reject put action in package owned by different subject\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(EntityPath(Subject().asString), aname(), publish = true)\n    val content = WhiskActionPut(Some(jsDefault(\"??\")))\n    put(entityStore, provider)\n    Put(s\"/${provider.namespace}/${collection.path}/${provider.name}/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  //// DEL /actions/package/name\n  it should \"delete action in package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    put(entityStore, provider)\n    put(entityStore, action)\n\n    // it should \"reject delete action in package owned by different subject\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Delete(s\"/${provider.namespace}/${collection.path}/${provider.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Delete(s\"$collectionPath/${provider.name}/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response should be(action)\n    }\n  }\n\n  it should \"reject delete action in package that does not exist\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    put(entityStore, action)\n    Delete(s\"$collectionPath/${provider.name}/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"reject delete non-existent action in package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    put(entityStore, provider)\n    Delete(s\"$collectionPath/${provider.name}/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"reject delete action in package binding where package doesn't exist\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None, publish = true)\n    val binding = WhiskPackage(namespace, aname(), provider.bind)\n    val content = WhiskActionPut(Some(jsDefault(\"??\")))\n    put(entityStore, binding)\n    Delete(s\"$collectionPath/${binding.name}/${aname()}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"reject delete action in package binding\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None, publish = true)\n    val binding = WhiskPackage(namespace, aname(), provider.bind)\n    val content = WhiskActionPut(Some(jsDefault(\"??\")))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    Delete(s\"$collectionPath/${binding.name}/${aname()}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"reject delete action in package owned by different subject\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(EntityPath(Subject().asString), aname(), publish = true)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    put(entityStore, provider)\n    put(entityStore, action)\n    Delete(s\"/${provider.namespace}/${collection.path}/${provider.name}/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  //// GET /actions/package/name\n  it should \"get action in package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = true)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"), Parameters(\"a\", \"A\"))\n    put(entityStore, provider)\n    put(entityStore, action)\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath/${provider.name}/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[WhiskAction]\n        response should be(action inherit provider.parameters)\n      }\n    }\n  }\n\n  it should \"get action in package binding with public package\" in {\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, publish = true)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    put(entityStore, action)\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath/${binding.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n        status should be(OK)\n        val response = responseAs[WhiskAction]\n        response should be(action inherit (provider.parameters ++ binding.parameters))\n      }\n    }\n  }\n\n  it should \"get action in package binding with public package with overriding parameters\" in {\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = true)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"), Parameters(\"a\", \"A\") ++ Parameters(\"b\", \"b\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    put(entityStore, action)\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath/${binding.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n        status should be(OK)\n        val response = responseAs[WhiskAction]\n        response should be(action inherit (provider.parameters ++ binding.parameters))\n      }\n    }\n  }\n\n  // NOTE: does not work because entitlement model does not allow for an explicit\n  // check on either one or both of the binding and package\n  ignore should \"get action in package binding with explicit entitlement grant\" in {\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = false)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"), Parameters(\"a\", \"A\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    put(entityStore, action)\n    val pkgaccess = Resource(provider.namespace, PACKAGES, Some(provider.name.asString))\n    Await.result(entitlementProvider.grant(auser, READ, pkgaccess), 1 second)\n    Get(s\"$collectionPath/${binding.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response should be(action inherit (provider.parameters ++ binding.parameters))\n    }\n  }\n\n  it should \"reject get action in package that does not exist\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    put(entityStore, action)\n    Get(s\"$collectionPath/${provider.name}/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"reject get non-existent action in package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    put(entityStore, provider)\n    Get(s\"$collectionPath/${provider.name}/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"reject get action in package binding that does not exist\" in {\n    implicit val tid = transid()\n    val name = aname()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = true)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"), Parameters(\"a\", \"A\"))\n    put(entityStore, provider)\n    put(entityStore, action)\n    Get(s\"$collectionPath/${binding.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"reject get action in package binding with package that does not exist\" in {\n    implicit val tid = transid()\n    val name = aname()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = true)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"), Parameters(\"a\", \"A\"))\n    put(entityStore, binding)\n    put(entityStore, action)\n    Get(s\"$collectionPath/${binding.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden) // do not leak that package does not exist\n    }\n  }\n\n  it should \"reject get non-existing action in package binding\" in {\n    implicit val tid = transid()\n    val name = aname()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = true)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"), Parameters(\"a\", \"A\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    Get(s\"$collectionPath/${binding.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"reject get action in package binding with private package\" in {\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = false)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"), Parameters(\"a\", \"A\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    put(entityStore, action)\n    Get(s\"$collectionPath/${binding.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  //// POST /actions/name\n  it should \"allow owner to invoke an action in package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = JsObject(\"xxx\" -> \"yyy\".toJson)\n    put(entityStore, provider)\n    put(entityStore, action)\n    Post(s\"$collectionPath/${provider.name}/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n    }\n  }\n\n  it should \"allow non-owner to invoke an action in public package\" in {\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), publish = true)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = JsObject(\"xxx\" -> \"yyy\".toJson)\n    put(entityStore, provider)\n    put(entityStore, action)\n    Post(s\"/$namespace/${collection.path}/${provider.name}/${action.name}\", content) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n    }\n  }\n\n  it should \"invoke action in package binding with public package\" in {\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), publish = true)\n    val reference = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = JsObject(\"x\" -> \"x\".toJson, \"z\" -> \"Z\".toJson)\n    put(entityStore, provider)\n    put(entityStore, reference)\n    put(entityStore, action)\n    Post(s\"$collectionPath/${reference.name}/${action.name}\", content) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n    }\n  }\n\n  // NOTE: does not work because entitlement model does not allow for an explicit\n  // check on either one or both of the binding and package\n  ignore should \"invoke action in package binding with explicit entitlement grant even if package is not public\" in {\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), publish = false)\n    val reference = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = JsObject(\"x\" -> \"x\".toJson, \"z\" -> \"Z\".toJson)\n    put(entityStore, provider)\n    put(entityStore, reference)\n    put(entityStore, action)\n    val pkgaccess = Resource(provider.namespace, PACKAGES, Some(provider.name.asString))\n    Await.result(entitlementProvider.grant(auser, ACTIVATE, pkgaccess), 1 second)\n    Post(s\"$collectionPath/${reference.name}/${action.name}\", content) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      response.fields(\"activationId\") should not be None\n    }\n  }\n\n  it should \"reject non-owner invoking an action in private package\" in {\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), publish = false)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = JsObject(\"xxx\" -> \"yyy\".toJson)\n    put(entityStore, provider)\n    put(entityStore, action)\n    Post(s\"/$namespace/${collection.path}/${provider.name}/${action.name}\", content) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"reject invoking an action in package that does not exist\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), publish = false)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = JsObject(\"xxx\" -> \"yyy\".toJson)\n    put(entityStore, action)\n    Post(s\"$collectionPath/${provider.name}/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"reject invoking a non-existent action in package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), publish = false)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = JsObject(\"xxx\" -> \"yyy\".toJson)\n    put(entityStore, action)\n    Post(s\"$collectionPath/${provider.name}/${action.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"reject invoke action in package binding with private package\" in {\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), publish = false)\n    val reference = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val content = JsObject(\"x\" -> \"x\".toJson, \"z\" -> \"Z\".toJson)\n    put(entityStore, provider)\n    put(entityStore, reference)\n    put(entityStore, action)\n    Post(s\"$collectionPath/${reference.name}/${action.name}\", content) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"report proper error when provider record is corrupted on delete\" in {\n    implicit val tid = transid()\n    val provider = BadEntity(namespace, aname())\n    val entity = BadEntity(provider.namespace.addPath(provider.name), aname())\n    put(entityStore, provider)\n    put(entityStore, entity)\n    Delete(s\"$collectionPath/${provider.name}/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when record is corrupted on delete\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val entity = BadEntity(provider.fullPath, aname())\n    put(entityStore, provider, false)\n    val entityToDelete = put(entityStore, entity, false)\n\n    Delete(s\"$collectionPath/${provider.name}/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      deletePackage(provider.docid)\n      delete(entityStore, entityToDelete)\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when provider record is corrupted on get\" in {\n    implicit val tid = transid()\n    val provider = BadEntity(namespace, aname())\n    val entity = BadEntity(provider.namespace.addPath(provider.name), aname())\n    put(entityStore, provider)\n    put(entityStore, entity)\n\n    Get(s\"$collectionPath/${provider.name}/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/${provider.namespace}/${collection.path}/${provider.name}/${entity.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n      responseAs[ErrorResponse].error shouldBe Messages.notAuthorizedtoAccessResource(s\"$namespace/${provider.name}\")\n    }\n  }\n\n  it should \"report proper error when record is corrupted on get\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val entity = BadEntity(provider.fullPath, aname())\n    put(entityStore, provider)\n    put(entityStore, entity)\n\n    Get(s\"$collectionPath/${provider.name}/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when provider record is corrupted on put\" in {\n    implicit val tid = transid()\n    val provider = BadEntity(namespace, aname())\n    val entity = BadEntity(provider.namespace.addPath(provider.name), aname())\n    put(entityStore, provider)\n    put(entityStore, entity)\n\n    val content = WhiskActionPut()\n    Put(s\"$collectionPath/${provider.name}/${entity.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when record is corrupted on put\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val entity = BadEntity(provider.fullPath, aname())\n    put(entityStore, provider)\n    put(entityStore, entity)\n\n    val content = WhiskActionPut()\n    Put(s\"$collectionPath/${provider.name}/${entity.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  var testExecuteOnly = false\n  override def executeOnly = testExecuteOnly\n\n  it should (\"allow access to get of action in binding of shared package when config option is disabled\") in {\n    testExecuteOnly = false\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = true)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"), Parameters(\"a\", \"A\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    put(entityStore, action)\n    Get(s\"$collectionPath/${binding.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(OK)\n    }\n  }\n\n  it should (\"deny access to get of action in binding of shared package when config option is enabled\") in {\n    testExecuteOnly = true\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = true)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"), Parameters(\"a\", \"A\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    put(entityStore, action)\n    Get(s\"$collectionPath/${binding.name}/${action.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/PackagesApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.core.controller.WhiskPackagesApi\nimport org.apache.openwhisk.core.entitlement.Collection\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.{ErrorResponse, Messages}\n\nimport scala.language.postfixOps\n\n/**\n * Tests Packages API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass PackagesApiTests extends ControllerTestCommon with WhiskPackagesApi {\n\n  /** Packages API tests */\n  behavior of \"Packages API\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n  def aname() = MakeName.next(\"packages_tests\")\n  val parametersLimit = Parameters.sizeLimit\n\n  private def bindingAnnotation(binding: Binding) = {\n    Parameters(WhiskPackage.bindingFieldName, Binding.serdes.write(binding))\n  }\n\n  def checkCount(path: String = collectionPath, expected: Long, user: Identity = creds) = {\n    implicit val tid = transid()\n    withClue(s\"count did not match\") {\n      org.apache.openwhisk.utils.retry {\n        Get(s\"$path?count=true\") ~> Route.seal(routes(user)) ~> check {\n          status should be(OK)\n          responseAs[JsObject].fields(collection.path).convertTo[Long] shouldBe (expected)\n        }\n      }\n    }\n  }\n\n  //// GET /packages\n  it should \"list all packages/references\" in {\n    implicit val tid = transid()\n    // create packages and package bindings, and confirm API lists all of them\n    val providers = (1 to 4).map { i =>\n      if (i % 2 == 0) {\n        WhiskPackage(namespace, aname(), None)\n      } else {\n        val binding = Some(Binding(namespace.root, aname()))\n        WhiskPackage(namespace, aname(), binding)\n      }\n    }.toList\n    providers foreach { put(entityStore, _) }\n    waitOnView(entityStore, WhiskPackage, namespace, providers.length)\n\n    checkCount(expected = providers.length)\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[List[JsObject]]\n        providers.length should be(response.length)\n        response should contain theSameElementsAs providers.map(_.summaryAsJson)\n      }\n    }\n\n    {\n      val path = s\"/$namespace/${collection.path}\"\n      val auser = WhiskAuthHelpers.newIdentity()\n      checkCount(path, 0, auser)\n      Get(path) ~> Route.seal(routes(auser)) ~> check {\n        val response = responseAs[List[JsObject]]\n        response should be(List.empty) // cannot list packages that are private in another namespace\n      }\n    }\n  }\n\n  it should \"list all public packages in explicit namespace excluding bindings\" in {\n    implicit val tid = transid()\n    // create packages and package bindings, set some public and confirm API lists only public packages\n    val namespaces = Seq(namespace, EntityPath(aname().toString), EntityPath(aname().toString))\n    val providers = Seq(\n      WhiskPackage(namespaces(0), aname(), None, publish = true),\n      WhiskPackage(namespaces(1), aname(), None, publish = true),\n      WhiskPackage(namespaces(2), aname(), None, publish = true))\n    val references = Seq(\n      WhiskPackage(namespaces(0), aname(), providers(0).bind, publish = true),\n      WhiskPackage(namespaces(0), aname(), providers(0).bind, publish = false),\n      WhiskPackage(namespaces(0), aname(), providers(1).bind, publish = true),\n      WhiskPackage(namespaces(0), aname(), providers(1).bind, publish = false))\n    (providers ++ references) foreach { put(entityStore, _) }\n    waitOnView(entityStore, WhiskPackage, namespaces(1), 1)\n    waitOnView(entityStore, WhiskPackage, namespaces(2), 1)\n    waitOnView(entityStore, WhiskPackage, namespaces(0), 1 + 4)\n\n    {\n      val expected = providers.filter(_.namespace == namespace) ++ references\n      checkCount(expected = expected.length)\n      Get(s\"$collectionPath\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[List[JsObject]]\n        response should have size expected.size\n        response should contain theSameElementsAs expected.map(_.summaryAsJson)\n      }\n    }\n\n    {\n      val path = s\"/$namespace/${collection.path}\"\n      val auser = WhiskAuthHelpers.newIdentity()\n      val expected = providers.filter(p => p.namespace == namespace && p.publish) ++\n        references.filter(p => p.publish && p.binding == None)\n\n      checkCount(path, expected.length, auser)\n      Get(path) ~> Route.seal(routes(auser)) ~> check {\n        status should be(OK)\n        val response = responseAs[List[JsObject]]\n        response should have size expected.size\n        response should contain theSameElementsAs expected.map(_.summaryAsJson)\n      }\n    }\n  }\n\n  it should \"reject list when limit is greater than maximum allowed value\" in {\n    implicit val tid = transid()\n    val exceededMaxLimit = Collection.MAX_LIST_LIMIT + 1\n    val response = Get(s\"$collectionPath?limit=$exceededMaxLimit\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listLimitOutOfRange(Collection.PACKAGES, exceededMaxLimit, Collection.MAX_LIST_LIMIT)\n      }\n    }\n  }\n\n  it should \"reject list when limit is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?limit=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.PACKAGES, notAnInteger)\n      }\n    }\n  }\n\n  it should \"reject list when skip is negative\" in {\n    implicit val tid = transid()\n    val negativeSkip = -1\n    val response = Get(s\"$collectionPath?skip=$negativeSkip\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listSkipOutOfRange(Collection.PACKAGES, negativeSkip)\n      }\n    }\n  }\n\n  it should \"reject list when skip is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?skip=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.PACKAGES, notAnInteger)\n      }\n    }\n  }\n\n  ignore should \"list all public packages excluding bindings\" in {\n    implicit val tid = transid()\n    // create packages and package bindings, set some public and confirm API lists only public packages\n    val namespaces = Seq(namespace, EntityPath(aname().toString), EntityPath(aname().toString))\n    val providers = Seq(\n      WhiskPackage(namespaces(0), aname(), None, publish = false),\n      WhiskPackage(namespaces(1), aname(), None, publish = true),\n      WhiskPackage(namespaces(2), aname(), None, publish = true))\n    val references = Seq(\n      WhiskPackage(namespaces(0), aname(), providers(0).bind, publish = true),\n      WhiskPackage(namespaces(0), aname(), providers(0).bind, publish = false),\n      WhiskPackage(namespaces(0), aname(), providers(1).bind, publish = true),\n      WhiskPackage(namespaces(0), aname(), providers(1).bind, publish = false))\n    (providers ++ references) foreach { put(entityStore, _) }\n    waitOnView(entityStore, WhiskPackage, namespaces(1), 1)\n    waitOnView(entityStore, WhiskPackage, namespaces(2), 1)\n    waitOnView(entityStore, WhiskPackage, namespaces(0), 1 + 4)\n    Get(s\"$collectionPath?public=true\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[JsObject]]\n      val expected = providers filter { _.publish }\n      response.length should be >= (expected.length)\n      expected forall { p =>\n        (response contains p.summaryAsJson) && p.binding == None\n      } should be(true)\n    }\n  }\n\n  // ?public disabled\n  ignore should \"list all public packages including ones with same name but in different namespaces\" in {\n    implicit val tid = transid()\n    // create packages and package bindings, set some public and confirm API lists only public packages\n    val namespaces = Seq(namespace, EntityPath(aname().toString), EntityPath(aname().toString))\n    val pkgname = aname()\n    val providers = Seq(\n      WhiskPackage(namespaces(0), pkgname, None, publish = false),\n      WhiskPackage(namespaces(1), pkgname, None, publish = true),\n      WhiskPackage(namespaces(2), pkgname, None, publish = true))\n    providers foreach { put(entityStore, _) }\n    waitOnView(entityStore, WhiskPackage, namespaces(0), 1)\n    waitOnView(entityStore, WhiskPackage, namespaces(1), 1)\n    waitOnView(entityStore, WhiskPackage, namespaces(2), 1)\n    Get(s\"$collectionPath?public=true\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[JsObject]]\n      val expected = providers filter { _.publish }\n      response.length should be >= (expected.length)\n      expected forall { p =>\n        (response contains p.summaryAsJson) && p.binding == None\n      } should be(true)\n    }\n  }\n\n  // confirm ?public disabled\n  it should \"ignore ?public on list all packages\" in {\n    implicit val tid = transid()\n    Get(s\"$collectionPath?public=true\") ~> Route.seal(routes(creds)) ~> check {\n      implicit val tid = transid()\n      // create packages and package bindings, set some public and confirm API lists only public packages\n      val namespaces = Seq(namespace, EntityPath(aname().toString), EntityPath(aname().toString))\n      val pkgname = aname()\n      val providers = Seq(\n        WhiskPackage(namespaces(0), pkgname, None, publish = true),\n        WhiskPackage(namespaces(1), pkgname, None, publish = true),\n        WhiskPackage(namespaces(2), pkgname, None, publish = true))\n      providers foreach { put(entityStore, _) }\n      waitOnView(entityStore, WhiskPackage, namespaces(0), 1)\n      waitOnView(entityStore, WhiskPackage, namespaces(1), 1)\n      waitOnView(entityStore, WhiskPackage, namespaces(2), 1)\n      val expected = providers filter (_.namespace == creds.namespace.name.toPath)\n\n      Get(s\"$collectionPath?public=true\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[List[JsObject]]\n        response.length should be >= (expected.length)\n        expected forall { p =>\n          (response contains p.summaryAsJson) && p.binding == None\n        } should be(true)\n      }\n    }\n  }\n\n  // ?public disabled\n  ignore should \"reject list all public packages with invalid parameters\" in {\n    implicit val tid = transid()\n    Get(s\"$collectionPath?public=true&docs=true\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  //// GET /packages/name\n  it should \"get package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None)\n    put(entityStore, provider)\n    Get(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskPackageWithActions]\n      response should be(provider.withActions())\n    }\n  }\n\n  it should \"get package with updated field\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None)\n    put(entityStore, provider)\n\n    // `updated` field should be compared with a document in DB\n    val pkg = get(entityStore, provider.docid, WhiskPackage)\n\n    Get(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskPackageWithActions]\n      response should be(provider.copy(updated = pkg.updated).withActions())\n    }\n  }\n\n  it should \"get package reference for private package in same namespace\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"a\", \"A\") ++ Parameters(\"b\", \"B\"))\n    val reference = WhiskPackage(namespace, aname(), provider.bind, Parameters(\"b\", \"b\") ++ Parameters(\"c\", \"C\"))\n    put(entityStore, provider)\n    put(entityStore, reference)\n    Get(s\"$collectionPath/${reference.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskPackageWithActions]\n      response should be(reference.inherit(provider.parameters).withActions())\n      // this is redundant in case the precedence orders on inherit are changed incorrectly\n      response.wp.parameters should be(Parameters(\"a\", \"A\") ++ Parameters(\"b\", \"b\") ++ Parameters(\"c\", \"C\"))\n    }\n  }\n\n  it should \"not get package reference for a private package in other namespace\" in {\n    implicit val tid = transid()\n    val privateCreds = WhiskAuthHelpers.newIdentity()\n    val privateNamespace = EntityPath(privateCreds.subject.asString)\n\n    val provider = WhiskPackage(privateNamespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    put(entityStore, provider)\n    put(entityStore, reference)\n    Get(s\"$collectionPath/${reference.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"get package with its actions and feeds\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.namespace.addPath(provider.name), aname(), jsDefault(\"??\"))\n    val feed = WhiskAction(\n      provider.namespace.addPath(provider.name),\n      aname(),\n      jsDefault(\"??\"),\n      annotations = Parameters(Parameters.Feed, \"true\"))\n    put(entityStore, provider)\n    put(entityStore, action)\n    put(entityStore, feed)\n\n    // it should \"reject get private package from other subject\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}/${provider.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Get(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskPackageWithActions]\n      response should be(provider withActions (List(action, feed)))\n    }\n  }\n\n  it should \"get package reference with its actions and feeds\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    val action = WhiskAction(provider.namespace.addPath(provider.name), aname(), jsDefault(\"??\"))\n    val feed = WhiskAction(\n      provider.namespace.addPath(provider.name),\n      aname(),\n      jsDefault(\"??\"),\n      annotations = Parameters(Parameters.Feed, \"true\"))\n\n    put(entityStore, provider)\n    put(entityStore, reference)\n    put(entityStore, action)\n    put(entityStore, feed)\n\n    waitOnView(entityStore, WhiskAction, provider.fullPath, 2)\n\n    // it should \"reject get package reference from other subject\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}/${reference.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Get(s\"$collectionPath/${reference.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskPackageWithActions]\n      response should be(reference withActions List(action, feed))\n    }\n  }\n\n  it should \"not get package reference with its actions and feeds from private package\" in {\n    implicit val tid = transid()\n    val privateCreds = WhiskAuthHelpers.newIdentity()\n    val privateNamespace = EntityPath(privateCreds.subject.asString)\n    val provider = WhiskPackage(privateNamespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    val action = WhiskAction(provider.namespace.addPath(provider.name), aname(), jsDefault(\"??\"))\n    val feed = WhiskAction(\n      provider.namespace.addPath(provider.name),\n      aname(),\n      jsDefault(\"??\"),\n      annotations = Parameters(Parameters.Feed, \"true\"))\n    put(entityStore, provider)\n    put(entityStore, reference)\n    put(entityStore, action)\n    put(entityStore, feed)\n\n    // it should \"reject get package reference from other subject\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}/${reference.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Get(s\"$collectionPath/${reference.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  //// PUT /packages/name\n  it should \"create package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname(), None, annotations = Parameters(\"a\", \"b\"))\n    // binding annotation should be removed\n    val someBindingAnnotation = Parameters(WhiskPackage.bindingFieldName, \"???\")\n    val content = WhiskPackagePut(annotations = Some(someBindingAnnotation ++ Parameters(\"a\", \"b\")))\n    Put(s\"$collectionPath/${provider.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deletePackage(provider.docid)\n      status should be(OK)\n      val response = responseAs[WhiskPackage]\n      checkWhiskEntityResponse(response, provider)\n    }\n  }\n\n  it should \"reject create/update package when package name is reserved\" in {\n    implicit val tid = transid()\n    Set(true, false) foreach { overwrite =>\n      RESERVED_NAMES foreach { reservedName =>\n        val provider = WhiskPackage(namespace, EntityName(reservedName), None)\n        val content = WhiskPackagePut()\n        Put(s\"$collectionPath/${provider.name}?overwrite=$overwrite\", content) ~> Route.seal(routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.packageNameIsReserved(reservedName)\n        }\n      }\n    }\n  }\n\n  it should \"not allow package update of pre-existing package with a reserved\" in {\n    implicit val tid = transid()\n    RESERVED_NAMES foreach { reservedName =>\n      val provider = WhiskPackage(namespace, EntityName(reservedName), None)\n      put(entityStore, provider)\n      val content = WhiskPackagePut()\n      Put(s\"$collectionPath/${provider.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n        status should be(BadRequest)\n        responseAs[ErrorResponse].error shouldBe Messages.packageNameIsReserved(reservedName)\n      }\n    }\n  }\n\n  it should \"allow package get/delete for pre-existing package with a reserved name\" in {\n    implicit val tid = transid()\n    RESERVED_NAMES foreach { reservedName =>\n      val provider = WhiskPackage(namespace, EntityName(reservedName), None)\n      put(entityStore, provider, garbageCollect = false)\n      val content = WhiskPackagePut()\n      Get(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        responseAs[WhiskPackage] shouldBe provider\n      }\n      Delete(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n      }\n    }\n  }\n\n  it should \"create package reference with explicit namespace\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val reference = WhiskPackage(\n      namespace,\n      aname(),\n      provider.bind,\n      annotations = bindingAnnotation(provider.bind.get) ++ Parameters(\"a\", \"b\"))\n    // binding annotation should be removed and set by controller\n    val someBindingAnnotation = Parameters(WhiskPackage.bindingFieldName, \"???\")\n    val content = WhiskPackagePut(reference.binding, annotations = Some(someBindingAnnotation ++ Parameters(\"a\", \"b\")))\n    put(entityStore, provider)\n\n    // it should \"reject create package reference in some other namespace\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Put(s\"/$namespace/${collection.path}/${reference.name}\", content) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Put(s\"/$namespace/${collection.path}/${reference.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deletePackage(reference.docid)\n      status should be(OK)\n      val response = responseAs[WhiskPackage]\n      checkWhiskEntityResponse(response, reference)\n    }\n  }\n\n  it should \"not create package reference from private package in another namespace\" in {\n    implicit val tid = transid()\n    val privateCreds = WhiskAuthHelpers.newIdentity()\n    val privateNamespace = EntityPath(privateCreds.subject.asString)\n\n    val provider = WhiskPackage(privateNamespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    // binding annotation should be removed and set by controller\n    val content = WhiskPackagePut(reference.binding)\n    put(entityStore, provider)\n\n    Put(s\"/$namespace/${collection.path}/${reference.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"create package reference with implicit namespace\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val reference = WhiskPackage(namespace, aname(), Some(Binding(EntityPath.DEFAULT.root, provider.name)))\n    val content = WhiskPackagePut(reference.binding)\n    put(entityStore, provider)\n    Put(s\"$collectionPath/${reference.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deletePackage(reference.docid)\n      status should be(OK)\n      val response = responseAs[WhiskPackage]\n      checkWhiskEntityResponse(\n        response,\n        WhiskPackage(\n          reference.namespace,\n          reference.name,\n          provider.bind,\n          annotations = bindingAnnotation(provider.bind.get)))\n    }\n  }\n\n  it should \"reject create package reference when referencing non-existent package in same namespace\" in {\n    implicit val tid = transid()\n    val binding = Some(Binding(namespace.root, aname()))\n    val content = WhiskPackagePut(binding)\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error should include(Messages.bindingDoesNotExist)\n    }\n  }\n\n  it should \"reject create package reference when referencing non-existent package in another namespace\" in {\n    implicit val tid = transid()\n    val privateCreds = WhiskAuthHelpers.newIdentity()\n    val privateNamespace = EntityPath(privateCreds.subject.asString)\n\n    val binding = Some(Binding(privateNamespace.root, aname()))\n    val content = WhiskPackagePut(binding)\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"reject create package reference when referencing a non-package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    val content = WhiskPackagePut(Some(Binding(reference.namespace.root, reference.name)))\n    put(entityStore, provider)\n    put(entityStore, reference)\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error should include(Messages.bindingCannotReferenceBinding)\n    }\n  }\n\n  it should \"reject create package reference when annotations are too big\" in {\n    implicit val tid = transid()\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val annotations = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val content = s\"\"\"{\"annotations\":$annotations}\"\"\".parseJson.asJsObject\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.annotationsFieldName, annotations.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"reject create package reference when parameters are too big\" in {\n    implicit val tid = transid()\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val parameters = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val content = s\"\"\"{\"parameters\":$parameters}\"\"\".parseJson.asJsObject\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.paramsFieldName, parameters.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"reject update package reference when parameters are too big\" in {\n    implicit val tid = transid()\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val parameters = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val provider = WhiskPackage(namespace, aname())\n    val content = s\"\"\"{\"parameters\":$parameters}\"\"\".parseJson.asJsObject\n    put(entityStore, provider)\n    Put(s\"$collectionPath/${aname()}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.paramsFieldName, parameters.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"update package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val content = WhiskPackagePut(publish = Some(true))\n    put(entityStore, provider)\n\n    // it should \"reject update package owned by different user\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Put(s\"/$namespace/${collection.path}/${provider.name}?overwrite=true\", content) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Put(s\"$collectionPath/${provider.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deletePackage(provider.docid)\n      val response = responseAs[WhiskPackage]\n      checkWhiskEntityResponse(\n        response,\n        WhiskPackage(namespace, provider.name, None, version = provider.version.upPatch, publish = true))\n    }\n  }\n\n  it should \"update package reference\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind, annotations = bindingAnnotation(provider.bind.get))\n    // create a bogus binding annotation which should be replaced by the PUT\n    val someBindingAnnotation = Some(Parameters(WhiskPackage.bindingFieldName, \"???\") ++ Parameters(\"a\", \"b\"))\n    val content = WhiskPackagePut(publish = Some(true), annotations = someBindingAnnotation)\n    put(entityStore, provider)\n    put(entityStore, reference)\n\n    // it should \"reject update package reference owned by different user\"\n    val auser = WhiskAuthHelpers.newIdentity()\n    Put(s\"/$namespace/${collection.path}/${reference.name}?overwrite=true\", content) ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Put(s\"$collectionPath/${reference.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deletePackage(reference.docid)\n      status should be(OK)\n      val response = responseAs[WhiskPackage]\n      checkWhiskEntityResponse(\n        response,\n        WhiskPackage(\n          reference.namespace,\n          reference.name,\n          reference.binding,\n          version = reference.version.upPatch,\n          publish = true,\n          annotations = reference.annotations ++ Parameters(\"a\", \"b\")))\n    }\n  }\n\n  it should \"reject update package with binding\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val content = WhiskPackagePut(provider.bind)\n    put(entityStore, provider)\n    Put(s\"$collectionPath/${provider.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Conflict)\n      responseAs[ErrorResponse].error should include(Messages.packageCannotBecomeBinding)\n    }\n  }\n\n  it should \"reject update package reference when new binding refers to non-existent package in same namespace\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    val content = WhiskPackagePut(reference.binding)\n    put(entityStore, reference)\n    Put(s\"$collectionPath/${reference.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error should include(Messages.bindingDoesNotExist)\n    }\n  }\n\n  it should \"reject update package reference when new binding refers to non-existent package in another namespace\" in {\n    implicit val tid = transid()\n    val privateCreds = WhiskAuthHelpers.newIdentity()\n    val privateNamespace = EntityPath(privateCreds.subject.asString)\n\n    val provider = WhiskPackage(privateNamespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    val content = WhiskPackagePut(reference.binding)\n    put(entityStore, reference)\n    Put(s\"$collectionPath/${reference.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"reject update package reference when new binding refers to itself\" in {\n    implicit val tid = transid()\n    // create package and valid reference binding to it\n    val provider = WhiskPackage(namespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    put(entityStore, provider)\n    put(entityStore, reference)\n    // manipulate package reference such that it attempts to bind to itself\n    val content = WhiskPackagePut(Some(Binding(namespace.root, reference.name)))\n    Put(s\"$collectionPath/${reference.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error should include(Messages.bindingCannotReferenceBinding)\n    }\n    // verify that the reference is still pointing to the original provider\n    Get(s\"$collectionPath/${reference.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskPackage]\n      response should be(reference)\n      response.binding should be(provider.bind)\n    }\n  }\n\n  it should \"reject update package reference when new binding refers to private package in another namespace\" in {\n    implicit val tid = transid()\n    val privateCreds = WhiskAuthHelpers.newIdentity()\n    val privateNamespace = EntityPath(privateCreds.subject.asString)\n\n    val provider = WhiskPackage(privateNamespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    val content = WhiskPackagePut(reference.binding)\n    put(entityStore, provider)\n    put(entityStore, reference)\n    Put(s\"$collectionPath/${reference.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  //// DEL /packages/name\n  it should \"delete package\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    put(entityStore, provider)\n\n    // it should \"reject deleting package owned by different user\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}/${provider.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Delete(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskPackage]\n      response should be(provider)\n    }\n  }\n\n  it should \"delete package reference regardless of package existence\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    put(entityStore, reference)\n\n    // it should \"reject deleting package reference owned by different user\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}/${reference.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n    Delete(s\"$collectionPath/${reference.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskPackage]\n      response should be(reference)\n    }\n  }\n\n  it should \"delete package and its actions if force flag is set to true\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.namespace.addPath(provider.name), aname(), jsDefault(\"??\"))\n    put(entityStore, provider)\n    put(entityStore, action)\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response.fields(\"actions\").asInstanceOf[JsArray].elements.length should be(1)\n      }\n    }\n\n    Delete(s\"$collectionPath/${provider.name}?force=true\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskPackage]\n      response should be(provider)\n    }\n  }\n\n  it should \"reject delete non-empty package if force flag is not set\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    val action = WhiskAction(provider.namespace.addPath(provider.name), aname(), jsDefault(\"??\"))\n    put(entityStore, provider)\n    put(entityStore, action)\n    org.apache.openwhisk.utils.retry {\n      Get(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response.fields(\"actions\").asInstanceOf[JsArray].elements.length should be(1)\n      }\n    }\n\n    val exceptionString = \"Package not empty (contains 1 entity). Set force param or delete package contents.\"\n    Delete(s\"$collectionPath/${provider.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Conflict)\n      val response = responseAs[ErrorResponse]\n      response.error should include(exceptionString)\n      response.code.id should not be empty\n    }\n  }\n\n  //// invalid resource\n  it should \"reject invalid resource\" in {\n    implicit val tid = transid()\n    val provider = WhiskPackage(namespace, aname())\n    put(entityStore, provider)\n    Get(s\"$collectionPath/${provider.name}/bar\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"return empty list for invalid namespace\" in {\n    implicit val tid = transid()\n    val path = s\"/whisk.systsdf/${collection.path}\"\n    Get(path) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      responseAs[List[JsObject]] should be(List.empty)\n    }\n  }\n\n  it should \"reject bind to non-package\" in {\n    implicit val tid = transid()\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val reference = WhiskPackage(namespace, aname(), Some(Binding(action.namespace.root, action.name)))\n    val content = WhiskPackagePut(reference.binding)\n\n    put(entityStore, action)\n\n    Put(s\"$collectionPath/${reference.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Conflict)\n      responseAs[ErrorResponse].error should include(Messages.requestedBindingIsNotValid)\n    }\n  }\n\n  it should \"report proper error when record is corrupted on delete\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    Delete(s\"$collectionPath/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when record is corrupted on get\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    Get(s\"$collectionPath/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n    }\n  }\n\n  it should \"report proper error when record is corrupted on put\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    val content = WhiskPackagePut()\n    Put(s\"$collectionPath/${entity.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  var testExecuteOnly = false\n  override def executeOnly = testExecuteOnly\n\n  it should (\"allow access to get of shared package binding when config option is disabled\") in {\n    testExecuteOnly = false\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = true)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    Get(s\"/$namespace/${collection.path}/${provider.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(OK)\n    }\n  }\n\n  it should (\"allow access to get of shared package when config option is disabled\") in {\n    testExecuteOnly = false\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, publish = true)\n    put(entityStore, provider)\n\n    Get(s\"/$namespace/${collection.path}/${provider.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(OK)\n    }\n  }\n\n  it should (\"deny access to get of shared package binding when config option is enabled\") in {\n    testExecuteOnly = true\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, Parameters(\"p\", \"P\"), publish = true)\n    val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters(\"b\", \"B\"))\n    put(entityStore, provider)\n    put(entityStore, binding)\n    Get(s\"/$namespace/${collection.path}/${provider.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n\n  }\n\n  it should (\"deny access to get of shared package when config option is enabled\") in {\n    testExecuteOnly = true\n    implicit val tid = transid()\n    val auser = WhiskAuthHelpers.newIdentity()\n    val provider = WhiskPackage(namespace, aname(), None, publish = true)\n    put(entityStore, provider)\n\n    Get(s\"/$namespace/${collection.path}/${provider.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/RateThrottleTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport scala.concurrent.duration.DurationInt\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.StreamLogging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entitlement._\nimport org.apache.openwhisk.core.entity.UserLimits\n\n/**\n * Tests rate throttle.\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n */\n@RunWith(classOf[JUnitRunner])\nclass RateThrottleTests extends AnyFlatSpec with Matchers with StreamLogging {\n\n  implicit val transid = TransactionId.testing\n  val subject = WhiskAuthHelpers.newIdentity()\n\n  behavior of \"Rate Throttle\"\n\n  it should \"throttle when rate exceeds allowed threshold\" in {\n    new RateThrottler(\"test\", _ => 0).check(subject).ok shouldBe false\n    val rt = new RateThrottler(\"test\", _ => 1)\n    rt.check(subject).ok shouldBe true\n    rt.check(subject).ok shouldBe false\n    rt.check(subject).ok shouldBe false\n    Thread.sleep(1.minute.toMillis)\n    rt.check(subject).ok shouldBe true\n  }\n\n  it should \"check against an alternative limit if passed in\" in {\n    val withLimits = subject.copy(limits = UserLimits(invocationsPerMinute = Some(5)))\n    val rt = new RateThrottler(\"test\", u => u.limits.invocationsPerMinute.getOrElse(1))\n    rt.check(withLimits).ok shouldBe true // 1\n    rt.check(withLimits).ok shouldBe true // 2\n    rt.check(withLimits).ok shouldBe true // 3\n    rt.check(withLimits).ok shouldBe true // 4\n    rt.check(withLimits).ok shouldBe true // 5\n    rt.check(withLimits).ok shouldBe false\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/RespondWithHeadersTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\nimport org.apache.pekko.http.scaladsl.server.Route\n\nimport org.apache.openwhisk.core.controller.RespondWithHeaders\n\n/**\n * Tests the API in general\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n * These tests differ from the more specific tests in that they make calls to the\n * outermost routes of the controller.\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass RespondWithHeadersTests extends ControllerTestCommon with RespondWithHeaders {\n\n  behavior of \"General API\"\n\n  val routes = {\n    pathPrefix(\"api\" / \"v1\") {\n      sendCorsHeaders {\n        path(\"one\") {\n          complete(OK)\n        } ~ path(\"two\") {\n          complete(OK)\n        } ~ options {\n          complete(OK)\n        } ~ reject\n      }\n    } ~ pathPrefix(\"other\") {\n      complete(OK)\n    }\n  }\n\n  it should \"respond to options\" in {\n    Options(\"/api/v1\") ~> Route.seal(routes) ~> check {\n      headers should contain allOf (allowOrigin, allowHeaders)\n    }\n  }\n\n  it should \"respond to options on every route under /api/v1\" in {\n    Options(\"/api/v1/one\") ~> Route.seal(routes) ~> check {\n      headers should contain allOf (allowOrigin, allowHeaders)\n    }\n    Options(\"/api/v1/two\") ~> Route.seal(routes) ~> check {\n      headers should contain allOf (allowOrigin, allowHeaders)\n    }\n  }\n\n  it should \"respond to options even on bogus routes under /api/v1\" in {\n    Options(\"/api/v1/bogus\") ~> Route.seal(routes) ~> check {\n      headers should contain allOf (allowOrigin, allowHeaders)\n    }\n  }\n\n  it should \"not respond to options on routes before /api/v1\" in {\n    Options(\"/api\") ~> Route.seal(routes) ~> check {\n      status shouldBe NotFound\n    }\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/RulesApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport java.time.Instant\n\nimport scala.language.postfixOps\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.core.controller.WhiskRulesApi\nimport org.apache.openwhisk.core.entitlement.Collection\nimport org.apache.openwhisk.core.entity.{WhiskRuleResponse, _}\nimport org.apache.openwhisk.core.entity.test.OldWhiskTrigger\nimport org.apache.openwhisk.http.ErrorResponse\n\nimport scala.language.postfixOps\nimport org.apache.openwhisk.core.entity.test.OldWhiskRule\nimport org.apache.openwhisk.http.Messages\n\n/**\n * Tests Rules API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass RulesApiTests extends ControllerTestCommon with WhiskRulesApi {\n\n  /** Rules API tests */\n  behavior of \"Rules API\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val namespace = EntityPath(creds.subject.asString)\n  def aname() = MakeName.next(\"rules_tests\")\n  def afullname(namespace: EntityPath, name: String) = FullyQualifiedEntityName(namespace, EntityName(name))\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n  val activeStatus = s\"\"\"{\"status\":\"${Status.ACTIVE}\"}\"\"\".parseJson.asJsObject\n  val inactiveStatus = s\"\"\"{\"status\":\"${Status.INACTIVE}\"}\"\"\".parseJson.asJsObject\n  val parametersLimit = Parameters.sizeLimit\n  val dummyInstant = Instant.now()\n\n  def checkResponse(response: WhiskRuleResponse, expected: WhiskRuleResponse) =\n    // ignore `updated` field because another test covers it\n    response should be(expected copy (updated = response.updated))\n\n  //// GET /rules\n  it should \"list rules by default/explicit namespace\" in {\n    implicit val tid = transid()\n\n    val rules = (1 to 2).map { i =>\n      WhiskRule(namespace, aname(), afullname(namespace, \"bogus trigger\"), afullname(namespace, \"bogus action\"))\n    }.toList\n    rules foreach { put(entityStore, _) }\n    waitOnView(entityStore, WhiskRule, namespace, 2, includeDocs = true)\n    Get(s\"$collectionPath\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[JsObject]]\n      rules.length should be(response.length)\n      response should contain theSameElementsAs rules.map(_.toJson)\n    }\n\n    // it should \"list rules with explicit namespace owned by subject\" in {\n    Get(s\"/$namespace/${collection.path}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[JsObject]]\n      rules.length should be(response.length)\n      response should contain theSameElementsAs rules.map(_.toJson)\n    }\n\n    // it should \"reject list rules with explicit namespace not owned by subject\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"reject list when limit is greater than maximum allowed value\" in {\n    implicit val tid = transid()\n    val exceededMaxLimit = Collection.MAX_LIST_LIMIT + 1\n    val response = Get(s\"$collectionPath?limit=$exceededMaxLimit\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listLimitOutOfRange(Collection.RULES, exceededMaxLimit, Collection.MAX_LIST_LIMIT)\n      }\n    }\n  }\n\n  it should \"reject list when limit is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?limit=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.RULES, notAnInteger)\n      }\n    }\n  }\n\n  it should \"reject list when skip is negative\" in {\n    implicit val tid = transid()\n    val negativeSkip = -1\n    val response = Get(s\"$collectionPath?skip=$negativeSkip\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listSkipOutOfRange(Collection.RULES, negativeSkip)\n      }\n    }\n  }\n\n  it should \"reject list when skip is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?skip=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.RULES, notAnInteger)\n      }\n    }\n  }\n\n  //?docs disabled\n  ignore should \"list rules by default namespace with full docs\" in {\n    implicit val tid = transid()\n\n    val rules = (1 to 2).map { i =>\n      WhiskRule(namespace, aname(), afullname(namespace, \"bogus trigger\"), afullname(namespace, \"bogus action\"))\n    }.toList\n    rules foreach { put(entityStore, _) }\n    waitOnView(entityStore, WhiskRule, namespace, 2, includeDocs = true)\n    Get(s\"$collectionPath?docs=true\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[WhiskRule]]\n      rules.length should be(response.length)\n      response should contain theSameElementsAs rules.map(_.toJson)\n    }\n  }\n\n  //// GET /rule/name\n  it should \"get rule by name in default/explicit namespace\" in {\n    implicit val tid = transid()\n\n    val rule =\n      WhiskRule(namespace, aname(), afullname(namespace, \"bogus trigger\"), afullname(namespace, \"bogus action\"))\n    put(entityStore, rule)\n    Get(s\"$collectionPath/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.INACTIVE))\n    }\n\n    // it should \"get trigger by name in explicit namespace owned by subject\" in\n    Get(s\"/$namespace/${collection.path}/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.INACTIVE))\n    }\n\n    // it should \"reject get trigger by name in explicit namespace not owned by subject\" in\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}/${rule.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"reject get of non existent rule\" in {\n    implicit val tid = transid()\n\n    Get(s\"$collectionPath/xxx\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"get rule with active state in trigger\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(\n      namespace,\n      EntityName(\"get_active_rule\"),\n      afullname(namespace, \"get_active_rule trigger\"),\n      afullname(namespace, \"an action\"))\n    val trigger = WhiskTrigger(rule.trigger.path, rule.trigger.name, rules = Some {\n      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))\n    })\n\n    put(entityStore, trigger)\n    put(entityStore, rule)\n\n    Get(s\"$collectionPath/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.ACTIVE))\n    }\n  }\n\n  it should \"get rule with updated field\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(\n      namespace,\n      EntityName(\"get_active_rule\"),\n      afullname(namespace, \"get_active_rule trigger\"),\n      afullname(namespace, \"an action\"))\n    val trigger = WhiskTrigger(rule.trigger.path, rule.trigger.name, rules = Some {\n      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))\n    })\n\n    put(entityStore, trigger)\n    put(entityStore, rule)\n\n    // `updated` field should be compared with a document in DB\n    val r = get(entityStore, rule.docid, WhiskRule)\n\n    Get(s\"$collectionPath/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      response should be(rule.withStatus(Status.ACTIVE) copy (updated = r.updated))\n    }\n  }\n\n  it should \"get rule with no rule-entries in trigger\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, EntityName(\"get_rule_with_empty_trigger trigger\"))\n    val rule = WhiskRule(\n      namespace,\n      EntityName(\"get_rule_with_empty_trigger\"),\n      trigger.fullyQualifiedName(false),\n      afullname(namespace, \"an action\"))\n\n    put(entityStore, trigger)\n    put(entityStore, rule)\n\n    Get(s\"$collectionPath/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.INACTIVE))\n    }\n  }\n\n  it should \"report Conflict if the name was of a different type\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, aname())\n\n    put(entityStore, trigger)\n\n    Get(s\"/$namespace/${collection.path}/${trigger.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Conflict)\n    }\n  }\n\n  // DEL /rules/name\n  it should \"not reject delete rule in state active\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(\n      namespace,\n      EntityName(\"reject_delete_rule_active\"),\n      FullyQualifiedEntityName(namespace, aname()),\n      afullname(namespace, \"an action\"))\n    val trigger = WhiskTrigger(namespace, rule.trigger.name, rules = Some {\n      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))\n    })\n\n    put(entityStore, trigger)\n    put(entityStore, rule)\n\n    Delete(s\"$collectionPath/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      deleteTrigger(trigger.docid)\n\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.INACTIVE))\n    }\n  }\n\n  it should \"delete rule in state inactive\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(\n      namespace,\n      aname(),\n      FullyQualifiedEntityName(namespace, aname()),\n      FullyQualifiedEntityName(namespace, aname()))\n    val triggerLink = ReducedRule(rule.action, Status.INACTIVE)\n    val trigger = WhiskTrigger(\n      rule.trigger.path,\n      rule.trigger.name,\n      rules = Some(Map(rule.fullyQualifiedName(false) -> triggerLink)))\n\n    put(entityStore, trigger, false)\n    put(entityStore, rule)\n\n    Delete(s\"$collectionPath/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n\n      status should be(OK)\n      t.rules.get.get(rule.fullyQualifiedName(false)) shouldBe None\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.INACTIVE))\n    }\n  }\n\n  it should \"delete rule in state inactive even if the trigger has been deleted\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(\n      namespace,\n      EntityName(\"delete_rule_inactive\"),\n      afullname(namespace, \"a trigger\"),\n      afullname(namespace, \"an action\"))\n\n    put(entityStore, rule)\n\n    Delete(s\"$collectionPath/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.INACTIVE))\n    }\n  }\n\n  it should \"delete rule in state inactive even if the trigger has no reference to the rule\" in {\n    implicit val tid = transid()\n\n    val rule =\n      WhiskRule(namespace, aname(), FullyQualifiedEntityName(namespace, aname()), afullname(namespace, \"an action\"))\n    val trigger = WhiskTrigger(rule.trigger.path, rule.trigger.name)\n\n    put(entityStore, trigger, false)\n    put(entityStore, rule)\n\n    Delete(s\"$collectionPath/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      deleteTrigger(trigger.docid)\n\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.INACTIVE))\n    }\n  }\n\n  //// PUT /rules/name\n  it should \"create rule\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(\n      namespace,\n      aname(),\n      FullyQualifiedEntityName(namespace, aname()),\n      FullyQualifiedEntityName(namespace, aname()))\n    val trigger = WhiskTrigger(rule.trigger.path, rule.trigger.name)\n    val action = WhiskAction(rule.action.path, rule.action.name, jsDefault(\"??\"))\n    val content = WhiskRulePut(Some(rule.trigger), Some(rule.action))\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n\n    Put(s\"$collectionPath/${rule.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.ACTIVE))\n      t.rules.get(rule.fullyQualifiedName(false)) shouldBe ReducedRule(action.fullyQualifiedName(false), Status.ACTIVE)\n    }\n  }\n\n  it should \"create rule without fully qualifying name\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(\n      namespace,\n      aname(),\n      FullyQualifiedEntityName(namespace, aname()),\n      FullyQualifiedEntityName(namespace, aname()))\n    val trigger = WhiskTrigger(rule.trigger.path, rule.trigger.name)\n    val action = WhiskAction(rule.action.path, rule.action.name, jsDefault(\"??\"))\n    val content = JsObject(\n      \"trigger\" -> JsString(s\"/_/${trigger.name.asString}\"),\n      \"action\" -> JsString(s\"/_/${action.name.asString}\"))\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n\n    Put(s\"$collectionPath/${rule.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.ACTIVE))\n      t.rules.get(rule.fullyQualifiedName(false)) shouldBe ReducedRule(action.fullyQualifiedName(false), Status.ACTIVE)\n    }\n  }\n\n  it should \"reject create rule without namespace in referenced entities\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(\n      namespace,\n      aname(),\n      FullyQualifiedEntityName(namespace, aname()),\n      FullyQualifiedEntityName(namespace, aname()))\n    val trigger = WhiskTrigger(rule.trigger.path, rule.trigger.name)\n    val action = WhiskAction(rule.action.path, rule.action.name, jsDefault(\"??\"))\n    val contentT =\n      JsObject(\"trigger\" -> trigger.name.toJson, \"action\" -> action.fullyQualifiedName(false).toDocId.toJson)\n    val contentA =\n      JsObject(\"action\" -> action.name.toJson, \"trigger\" -> trigger.fullyQualifiedName(false).toDocId.toJson)\n\n    Put(s\"$collectionPath/${rule.name}\", contentT) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] shouldBe s\"The request content was malformed:\\nrequirement failed: ${Messages.malformedFullyQualifiedEntityName}\"\n    }\n\n    Put(s\"$collectionPath/${rule.name}\", contentA) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] shouldBe s\"The request content was malformed:\\nrequirement failed: ${Messages.malformedFullyQualifiedEntityName}\"\n    }\n  }\n\n  it should \"create rule with an action in a package\" in {\n    implicit val tid = transid()\n\n    val provider = WhiskPackage(namespace, aname(), publish = true)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val trigger = WhiskTrigger(namespace, aname())\n    val rule = WhiskRule(namespace, aname(), trigger.fullyQualifiedName(false), action.fullyQualifiedName(false))\n    val content = WhiskRulePut(Some(rule.trigger), Some(rule.action))\n\n    put(entityStore, provider)\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n\n    Put(s\"$collectionPath/${rule.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.ACTIVE))\n      t.rules.get(rule.fullyQualifiedName(false)) shouldBe ReducedRule(action.fullyQualifiedName(false), Status.ACTIVE)\n    }\n  }\n\n  it should \"create rule with an action in a binding\" in {\n    implicit val tid = transid()\n\n    val provider = WhiskPackage(namespace, aname(), publish = true)\n    val reference = WhiskPackage(namespace, aname(), provider.bind)\n    val action = WhiskAction(provider.fullPath, aname(), jsDefault(\"??\"))\n    val trigger = WhiskTrigger(namespace, aname())\n    val actionReference = reference.binding.map(b => b.namespace.addPath(b.name)).get\n    val rule = WhiskRule(\n      namespace,\n      aname(),\n      trigger.fullyQualifiedName(false),\n      FullyQualifiedEntityName(actionReference, action.name))\n    val content = WhiskRulePut(Some(rule.trigger), Some(rule.action))\n\n    put(entityStore, provider)\n    put(entityStore, reference)\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n\n    Put(s\"$collectionPath/${rule.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.ACTIVE))\n      t.rules.get(rule.fullyQualifiedName(false)) shouldBe ReducedRule(action.fullyQualifiedName(false), Status.ACTIVE)\n    }\n  }\n\n  it should \"reject create rule with annotations which are too big\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, aname())\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (Parameters.sizeLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val annotations = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val content =\n      s\"\"\"{\"trigger\":\"${trigger.name}\",\"action\":\"${action.name}\",\"annotations\":$annotations}\"\"\".parseJson.asJsObject\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.annotationsFieldName, annotations.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"reject update rule with annotations which are too big\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, aname())\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val rule = WhiskRule(namespace, aname(), trigger.fullyQualifiedName(false), action.fullyQualifiedName(false))\n\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (Parameters.sizeLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val annotations = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val content =\n      s\"\"\"{\"trigger\":\"${trigger.name}\",\"action\":\"${action.name}\",\"annotations\":$annotations}\"\"\".parseJson.asJsObject\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n    put(entityStore, rule)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.annotationsFieldName, annotations.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"reject rule if action does not exist\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, aname())\n    val content = WhiskRulePut(Some(trigger.fullyQualifiedName(false)), Some(afullname(namespace, \"bogus action\")))\n\n    put(entityStore, trigger)\n\n    Put(s\"$collectionPath/xxx\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] === s\"${content.action.get.qualifiedNameWithLeadingSlash} does not exist\"\n    }\n  }\n\n  it should \"reject rule if trigger does not exist\" in {\n    implicit val tid = transid()\n\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val content = WhiskRulePut(Some(afullname(namespace, \"bogus trigger\")), Some(action.fullyQualifiedName(false)))\n\n    put(entityStore, action)\n\n    Put(s\"$collectionPath/xxx\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] === s\"${content.trigger.get.qualifiedNameWithLeadingSlash} does not exist\"\n    }\n  }\n\n  it should \"reject rule if neither action or trigger do not exist\" in {\n    implicit val tid = transid()\n\n    val content = WhiskRulePut(Some(afullname(namespace, \"bogus trigger\")), Some(afullname(namespace, \"bogus action\")))\n\n    Put(s\"$collectionPath/xxx\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String].contains(\"does not exist\") should be(true)\n    }\n  }\n\n  it should \"update rule with new trigger and action at once\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, aname())\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val rule =\n      WhiskRule(namespace, aname(), afullname(namespace, \"bogus trigger\"), afullname(namespace, \"bogus action\"))\n    val content = WhiskRulePut(Some(trigger.fullyQualifiedName(false)), Some(action.fullyQualifiedName(false)))\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n    put(entityStore, rule, false)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n\n      t.rules.get(rule.fullyQualifiedName(false)).action should be(action.fullyQualifiedName(false))\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(\n        response,\n        WhiskRuleResponse(\n          namespace,\n          rule.name,\n          Status.INACTIVE,\n          trigger.fullyQualifiedName(false),\n          action.fullyQualifiedName(false),\n          version = SemVer().upPatch,\n          updated = dummyInstant))\n    }\n  }\n\n  it should \"update rule with a new action while passing the same trigger\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, aname())\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val rule = WhiskRule(namespace, aname(), trigger.fullyQualifiedName(false), afullname(namespace, \"bogus action\"))\n    val content = WhiskRulePut(Some(trigger.fullyQualifiedName(false)), Some(action.fullyQualifiedName(false)))\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n    put(entityStore, rule, false)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      t.rules.get(rule.fullyQualifiedName(false)).action should be(action.fullyQualifiedName(false))\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(\n        response,\n        WhiskRuleResponse(\n          namespace,\n          rule.name,\n          Status.INACTIVE,\n          trigger.fullyQualifiedName(false),\n          action.fullyQualifiedName(false),\n          version = SemVer().upPatch,\n          updated = dummyInstant))\n    }\n  }\n\n  it should \"update rule with just a new action\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, aname())\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val rule = WhiskRule(namespace, aname(), trigger.fullyQualifiedName(false), afullname(namespace, \"bogus action\"))\n    val content = WhiskRulePut(action = Some(action.fullyQualifiedName(false)))\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n    put(entityStore, rule, false)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      t.rules.get(rule.fullyQualifiedName(false)).action should be(action.fullyQualifiedName(false))\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(\n        response,\n        WhiskRuleResponse(\n          namespace,\n          rule.name,\n          Status.INACTIVE,\n          trigger.fullyQualifiedName(false),\n          action.fullyQualifiedName(false),\n          version = SemVer().upPatch,\n          updated = dummyInstant))\n    }\n  }\n\n  it should \"update rule with just a new trigger\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, aname())\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val rule = WhiskRule(namespace, aname(), trigger.fullyQualifiedName(false), action.fullyQualifiedName(false))\n    val content = WhiskRulePut(trigger = Some(trigger.fullyQualifiedName(false)))\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n    put(entityStore, rule, false)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      t.rules.get.get(rule.fullyQualifiedName(false)) shouldBe a[Some[_]]\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(\n        response,\n        WhiskRuleResponse(\n          namespace,\n          rule.name,\n          Status.INACTIVE,\n          trigger.fullyQualifiedName(false),\n          action.fullyQualifiedName(false),\n          version = SemVer().upPatch,\n          updated = dummyInstant))\n    }\n  }\n\n  it should \"update rule when no new content is provided\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname())\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val rule = WhiskRule(namespace, aname(), trigger.fullyQualifiedName(false), action.fullyQualifiedName(false))\n    val content = WhiskRulePut(None, None, None, None, None)\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n    put(entityStore, rule, false)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteTrigger(trigger.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(\n        response,\n        WhiskRuleResponse(\n          namespace,\n          rule.name,\n          Status.INACTIVE,\n          trigger.fullyQualifiedName(false),\n          action.fullyQualifiedName(false),\n          version = SemVer().upPatch,\n          updated = dummyInstant))\n    }\n  }\n\n  it should \"reject update rule if trigger does not exist\" in {\n    implicit val tid = transid()\n\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, \"bogus trigger\"), action.fullyQualifiedName(false))\n    val content = WhiskRulePut(action = Some(action.fullyQualifiedName(false)))\n\n    put(entityStore, action)\n    put(entityStore, rule)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] === s\"${rule.trigger.qualifiedNameWithLeadingSlash} does not exist\"\n    }\n  }\n\n  it should \"reject update rule if action does not exist\" in {\n    implicit val tid = transid()\n\n    val trigger = WhiskTrigger(namespace, aname())\n    val rule = WhiskRule(namespace, aname(), trigger.fullyQualifiedName(false), afullname(namespace, \"bogus action\"))\n    val content = WhiskRulePut(trigger = Some(trigger.fullyQualifiedName(false)))\n\n    put(entityStore, trigger)\n    put(entityStore, rule)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] === s\"${rule.action.qualifiedNameWithLeadingSlash} does not exist\"\n    }\n  }\n\n  it should \"reject update rule if neither trigger or action exist\" in {\n    implicit val tid = transid()\n    val rule =\n      WhiskRule(namespace, aname(), afullname(namespace, \"bogus trigger\"), afullname(namespace, \"bogus action\"))\n    val content = WhiskRulePut(None, None, None, None, None)\n\n    put(entityStore, rule)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should {\n        include(s\"${rule.action.qualifiedNameWithLeadingSlash} does not exist\") or\n          include(s\"${rule.trigger.qualifiedNameWithLeadingSlash} does not exist\")\n      }\n    }\n  }\n\n  it should \"not reject update rule in state active\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, \"a trigger\"), afullname(namespace, \"an action\"))\n    val trigger = WhiskTrigger(namespace, rule.trigger.name, rules = Some {\n      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))\n    })\n    val action = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    val content = WhiskRulePut(Some(trigger.fullyQualifiedName(false)), Some(action.fullyQualifiedName(false)))\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n    put(entityStore, rule, false)\n\n    Put(s\"$collectionPath/${rule.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      t.rules.get(rule.fullyQualifiedName(false)).action should be(action.fullyQualifiedName(false))\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(\n        response,\n        WhiskRuleResponse(\n          namespace,\n          rule.name,\n          Status.ACTIVE,\n          trigger.fullyQualifiedName(false),\n          action.fullyQualifiedName(false),\n          version = SemVer().upPatch,\n          updated = dummyInstant))\n    }\n  }\n\n  //// POST /rules/name\n  it should \"do nothing to disable already disabled rule\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, \"a trigger\"), afullname(namespace, \"an action\"))\n\n    put(entityStore, rule)\n\n    Post(s\"$collectionPath/${rule.name}\", inactiveStatus) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n    }\n  }\n\n  it should \"do nothing to enable already enabled rule\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, \"an action\"))\n    val trigger = WhiskTrigger(namespace, rule.trigger.name, rules = Some {\n      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))\n    })\n\n    put(entityStore, trigger)\n    put(entityStore, rule)\n\n    Post(s\"$collectionPath/${rule.name}\", activeStatus) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n    }\n  }\n\n  it should \"reject post with status undefined\" in {\n    implicit val tid = transid()\n\n    Post(s\"$collectionPath/xyz\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"reject post with invalid status\" in {\n    implicit val tid = transid()\n\n    val badStatus = s\"\"\"{\"status\":\"xxx\"}\"\"\".parseJson.asJsObject\n\n    Post(s\"$collectionPath/xyz\", badStatus) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"activate rule\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, \"an action\"))\n    val trigger = WhiskTrigger(namespace, rule.trigger.name, rules = Some {\n      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.INACTIVE))\n    })\n\n    put(entityStore, trigger, false)\n    put(entityStore, rule)\n\n    Post(s\"$collectionPath/${rule.name}\", activeStatus) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n\n      status should be(OK)\n\n      t.rules.get(rule.fullyQualifiedName(false)).status should be(Status.ACTIVE)\n    }\n  }\n\n  it should \"activate rule without rule in trigger\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, \"an action\"))\n    val trigger = WhiskTrigger(namespace, rule.trigger.name)\n\n    put(entityStore, trigger, false)\n    put(entityStore, rule)\n\n    Post(s\"$collectionPath/${rule.name}\", activeStatus) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n\n      status should be(OK)\n      t.rules.get(rule.fullyQualifiedName(false)).status should be(Status.ACTIVE)\n    }\n  }\n\n  it should \"reject rule activation, if the trigger is absent\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, \"an action\"))\n\n    put(entityStore, rule)\n\n    Post(s\"$collectionPath/${rule.name}\", activeStatus) ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  it should \"deactivate rule\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, \"an action\"))\n    val trigger = WhiskTrigger(namespace, rule.trigger.name, rules = Some {\n      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))\n    })\n\n    put(entityStore, trigger, false)\n    put(entityStore, rule)\n\n    Post(s\"$collectionPath/${rule.name}\", inactiveStatus) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n\n      status should be(OK)\n      t.rules.get(rule.fullyQualifiedName(false)).status should be(Status.INACTIVE)\n    }\n  }\n\n  // invalid resource\n  it should \"reject invalid resource\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, \"a trigger\"), afullname(namespace, \"an action\"))\n\n    put(entityStore, rule)\n\n    Get(s\"$collectionPath/${rule.name}/bar\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  // migration path\n  it should \"handle a rule with the old schema gracefully\" in {\n    implicit val tid = transid()\n\n    val rule = OldWhiskRule(namespace, aname(), EntityName(\"a trigger\"), EntityName(\"an action\"), Status.ACTIVE)\n\n    put(entityStore, rule)\n\n    Get(s\"$collectionPath/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.toWhiskRule.withStatus(Status.INACTIVE))\n    }\n  }\n\n  it should \"create rule even if the attached trigger has the old schema\" in {\n    implicit val tid = transid()\n\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, aname().name))\n    val trigger = OldWhiskTrigger(namespace, rule.trigger.name)\n\n    val action = WhiskAction(namespace, rule.action.name, jsDefault(\"??\"))\n    val content = WhiskRulePut(Some(rule.trigger), Some(rule.action))\n\n    put(entityStore, trigger, false)\n    put(entityStore, action)\n\n    Put(s\"$collectionPath/${rule.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n      deleteRule(rule.docid)\n\n      status should be(OK)\n      t.rules.get(rule.fullyQualifiedName(false)) shouldBe ReducedRule(action.fullyQualifiedName(false), Status.ACTIVE)\n      val response = responseAs[WhiskRuleResponse]\n      checkResponse(response, rule.withStatus(Status.ACTIVE))\n    }\n  }\n\n  it should \"activate rule even if it is still in the old schema\" in {\n    implicit val tid = transid()\n\n    val ruleNameQualified = FullyQualifiedEntityName(namespace, aname())\n    val triggerName = aname()\n    val actionName = FullyQualifiedEntityName(namespace, aname())\n    val rule = OldWhiskRule(namespace, ruleNameQualified.name, triggerName, actionName.name, Status.ACTIVE)\n    val trigger = WhiskTrigger(\n      namespace,\n      triggerName,\n      rules = Some(Map(ruleNameQualified -> ReducedRule(actionName, Status.INACTIVE))))\n\n    put(entityStore, trigger, false)\n    put(entityStore, rule)\n\n    Post(s\"$collectionPath/${rule.name}\", activeStatus) ~> Route.seal(routes(creds)) ~> check {\n      val t = get(entityStore, trigger.docid, WhiskTrigger)\n      deleteTrigger(t.docid)\n\n      status should be(OK)\n\n      t.rules.get(ruleNameQualified).status should be(Status.ACTIVE)\n    }\n  }\n\n  it should \"report proper error when record is corrupted on delete\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    Delete(s\"$collectionPath/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when record is corrupted on get\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    Get(s\"$collectionPath/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when record is corrupted on put\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    val content = WhiskRulePut()\n    Put(s\"$collectionPath/${entity.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when action record is corrupted on put\" in {\n    implicit val tid = transid()\n    val tentity = BadEntity(namespace, aname())\n    val aentity = BadEntity(namespace, aname())\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, aname().name))\n    val trigger = WhiskTrigger(namespace, rule.trigger.name)\n    val action = WhiskAction(namespace, rule.action.name, jsDefault(\"??\"))\n\n    val contenta = WhiskRulePut(Some(tentity.fullyQualifiedName(false)), Some(aentity.fullyQualifiedName(false)))\n    val contentb = WhiskRulePut(Some(trigger.fullyQualifiedName(false)), Some(aentity.fullyQualifiedName(false)))\n    val contentc = WhiskRulePut(Some(tentity.fullyQualifiedName(false)), Some(action.fullyQualifiedName(false)))\n\n    put(entityStore, tentity)\n    put(entityStore, aentity)\n    put(entityStore, trigger)\n    put(entityStore, action)\n\n    Put(s\"$collectionPath/${aname()}\", contenta) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n\n    Put(s\"$collectionPath/${aname()}\", contentb) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n\n    Put(s\"$collectionPath/${aname()}\", contentc) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/SequenceApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport java.time.Instant\n\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.openwhisk.core.controller.WhiskActionsApi\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.Messages.sequenceComponentNotFound\nimport org.apache.openwhisk.http.{ErrorResponse, Messages}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.concurrent.duration.DurationInt\nimport scala.language.postfixOps\n\n/**\n * Tests Sequence API - stand-alone tests that require only the controller to be up\n */\n@RunWith(classOf[JUnitRunner])\nclass SequenceApiTests extends ControllerTestCommon with WhiskActionsApi {\n\n  behavior of \"Sequence API\"\n\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n  val creds = WhiskAuthHelpers.newIdentity()\n  val namespace = EntityPath(creds.subject.asString)\n  val defaultNamespace = EntityPath.DEFAULT\n\n  def aname() = MakeName.next(\"sequence_tests\")\n\n  val allowedActionDuration = 120 seconds\n\n  it should \"partially invoke a sequence with missing component and produce component missing error\" in {\n    implicit val tid = transid()\n    val seqName = s\"${aname()}_seq\"\n    val compName1 = s\"${aname()}_comp1\"\n    val compName2 = s\"${aname()}_comp2\"\n    val comp1Activation = WhiskActivation(\n      namespace,\n      EntityName(compName1),\n      creds.subject,\n      activationIdFactory.make(),\n      start = Instant.now,\n      end = Instant.now)\n\n    putSimpleSequenceInDB(seqName, namespace, Vector(compName1, compName2))\n    deleteAction(DocId(s\"$namespace/$compName2\"))\n    loadBalancer.whiskActivationStub = Some((1.milliseconds, Right(comp1Activation)))\n\n    Post(s\"$collectionPath/$seqName?blocking=true\") ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(DocId(s\"$namespace/$seqName\"))\n      deleteAction(DocId(s\"$namespace/$compName1\"))\n      status should be(BadGateway)\n      val response = responseAs[JsObject]\n      response.fields(\"response\") shouldBe ActivationResponse.applicationError(sequenceComponentNotFound).toExtendedJson\n      val logs = response.fields(\"logs\").convertTo[JsArray]\n      logs.elements.size shouldBe 1\n      logs.elements.head shouldBe comp1Activation.activationId.toJson\n    }\n  }\n\n  it should \"reject creation of sequence with more actions than allowed limit\" in {\n    implicit val tid = transid()\n    val seqName = EntityName(s\"${aname()}_toomanyactions\")\n    // put the component action in the entity store so it's found\n    val component = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    put(entityStore, component)\n    // create exec sequence that will violate max length\n    val limit = whiskConfig.actionSequenceLimit.toInt + 1 // one more than allowed\n    val components = for (i <- 1 to limit) yield stringToFullyQualifiedName(component.docid.asString)\n    val content = WhiskActionPut(Some(sequence(components.toVector)))\n\n    // create an action sequence\n    Put(s\"$collectionPath/${seqName.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceIsTooLong\n    }\n  }\n\n  it should \"reject creation of sequence with no component specified\" in {\n    implicit val tid = transid()\n    val seqName = s\"${aname()}_no_component\"\n    // create exec sequence with no component\n    val content = WhiskActionPut(Some(sequence(Vector.empty)))\n\n    // create an action sequence\n    Put(s\"$collectionPath/$seqName\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceNoComponent\n    }\n  }\n\n  it should \"reject update of sequence with no component specified\" in {\n    implicit val tid = transid()\n    val seqName = s\"${aname()}_update_no_component\"\n    // install fake sequence in db to be able to do update\n    val components = Vector(\"a\", \"b\")\n    putSimpleSequenceInDB(seqName, namespace, components)\n    // update sequence with no component\n    val updateContent = WhiskActionPut(Some(sequence(Vector.empty)))\n\n    // create an action sequence\n    Put(s\"$collectionPath/$seqName?overwrite=true\", updateContent) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceNoComponent\n    }\n  }\n\n  it should \"reject creation of sequence with non-existent action\" in {\n    implicit val tid = transid()\n    val seqName = EntityName(s\"${aname()}_componentnotfound\")\n    val bogus = s\"${aname()}_bogus\"\n    val bogusAction = s\"/$namespace/$bogus\"\n    val components = Vector(bogusAction).map(stringToFullyQualifiedName(_))\n    val content = WhiskActionPut(Some(sequence(components)))\n\n    // create an action sequence\n    Put(s\"$collectionPath/${seqName.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceComponentNotFound\n    }\n  }\n\n  it should \"reject create sequence that points to itself\" in {\n    implicit val tid = transid()\n    val seqName = s\"${aname()}_cyclic\"\n    val sSeq = makeSimpleSequence(seqName, namespace, Vector(seqName), false)\n\n    // create an action sequence\n    val content = WhiskActionPut(Some(sSeq.exec))\n    Put(s\"$collectionPath/$seqName\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceIsCyclic\n    }\n  }\n\n  it should \"reject create sequence that points to itself with many components\" in {\n    implicit val tid = transid()\n\n    // put the action in the entity store so it's found\n    val component = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    put(entityStore, component)\n\n    val seqName = s\"${aname()}_cyclic\"\n    val sSeq =\n      makeSimpleSequence(seqName, namespace, Vector(component.name.asString, seqName, component.name.asString), false)\n\n    // create an action sequence\n    val content = WhiskActionPut(Some(sSeq.exec))\n    Put(s\"$collectionPath/$seqName\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceIsCyclic\n    }\n  }\n\n  it should \"reject update of sequence with cycle\" in {\n    implicit val tid = transid()\n    val seqName = EntityName(s\"${aname()}_cycle\")\n    // put the component action in the entity store so it's found\n    val component = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    put(entityStore, component)\n    // create valid exec sequence initially\n    val components = for (i <- 1 to 2) yield stringToFullyQualifiedName(component.docid.asString)\n    val content = WhiskActionPut(Some(sequence(components.toVector)))\n\n    // create a valid action sequence first\n    Put(s\"$collectionPath/${seqName.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n    }\n\n    // now create exec sequence with a self-reference\n    val seqNameWithNamespace = stringToFullyQualifiedName(s\"/$namespace/${seqName.name}\")\n    val updatedSeq = components.updated(1, seqNameWithNamespace)\n    val updatedContent = WhiskActionPut(Some(sequence(updatedSeq.toVector)))\n\n    // update the sequence\n    Put(s\"$collectionPath/${seqName.name}?overwrite=true\", updatedContent) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(DocId(s\"$namespace/${seqName.name}\"))\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceIsCyclic\n    }\n  }\n\n  it should \"allow creation of sequence provided the number of actions is <= than allowed limit\" in {\n    implicit val tid = transid()\n    val seqName = EntityName(s\"${aname()}_normal\")\n    // put the component action in the entity store so it's found\n    val component = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    put(entityStore, component)\n    // create valid exec sequence\n    val limit = whiskConfig.actionSequenceLimit.toInt\n    val components = for (i <- 1 to limit) yield stringToFullyQualifiedName(component.docid.asString)\n    val content = WhiskActionPut(Some(sequence(components.toVector)))\n\n    // create an action sequence\n    Put(s\"$collectionPath/${seqName.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(DocId(s\"$namespace/${seqName.name}\"))\n      status should be(OK)\n    }\n  }\n\n  it should \"allow creation of sequence with actions with package bindings\" in {\n    implicit val tid = transid()\n    val seqName = EntityName(s\"${aname()}_withbindings\")\n\n    // create the package\n    val pkg = s\"${aname()}_pkg\"\n    val wp = WhiskPackage(namespace, EntityName(pkg), None, publish = true)\n    put(entityStore, wp)\n\n    // create binding to wp\n    val pkgWithBinding = s\"${aname()}_pkgbinding\"\n    val wpBinding = WhiskPackage(namespace, EntityName(pkgWithBinding), wp.bind)\n    put(entityStore, wpBinding)\n\n    // put the action in the entity store so it exists\n    val actionName = s\"${aname()}_action\"\n    val namespaceWithPkg = s\"/$namespace/$pkg\"\n    val action = WhiskAction(EntityPath(namespaceWithPkg), EntityName(actionName), jsDefault(\"??\"))\n    put(entityStore, action)\n\n    // create sequence that refers to action with binding\n    val components = Vector(s\"/$defaultNamespace/$pkgWithBinding/$actionName\").map(stringToFullyQualifiedName(_))\n    val content = WhiskActionPut(Some(sequence(components)))\n\n    // create an action sequence\n    Put(s\"$collectionPath/${seqName.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(DocId(s\"$namespace/${seqName.name}\"))\n      status should be(OK)\n      val response = responseAs[String]\n    }\n  }\n\n  it should \"reject update of sequence with cycle through bindings\" in {\n    implicit val tid = transid()\n    val seqName = EntityName(s\"${aname()}_cycle_binding\")\n\n    // put the action in the entity store so it's found\n    val component = WhiskAction(namespace, aname(), jsDefault(\"??\"))\n    put(entityStore, component)\n    val components = for (i <- 1 to 2) yield stringToFullyQualifiedName(component.docid.asString)\n\n    // create package\n    val pkg = s\"${aname()}_pkg\"\n    val wp = WhiskPackage(namespace, EntityName(pkg), None, publish = true)\n    put(entityStore, wp)\n\n    // create an action sequence\n    val namespaceWithPkg = EntityPath(s\"/$namespace/$pkg\")\n    val content = WhiskActionPut(Some(sequence(components.toVector)))\n    Put(s\"$collectionPath/$pkg/${seqName.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n    }\n\n    // create binding\n    val pkgWithBinding = s\"${aname()}_pkgbinding\"\n    val wpBinding = WhiskPackage(namespace, EntityName(pkgWithBinding), wp.bind)\n    put(entityStore, wpBinding)\n\n    // now update the sequence to refer to itself through the binding\n    val seqNameWithBinding = stringToFullyQualifiedName(s\"/$namespace/$pkgWithBinding/${seqName.name}\")\n    val updatedSeq = components.updated(1, seqNameWithBinding)\n    val updatedContent = WhiskActionPut(Some(sequence(updatedSeq.toVector)))\n\n    // update the sequence\n    Put(s\"$collectionPath/$pkg/${seqName.name}?overwrite=true\", updatedContent) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(DocId(s\"$namespace/$pkg/${seqName.name}\"))\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceIsCyclic\n    }\n  }\n\n  it should \"reject creation of a sequence with components that don't have at least namespace and action name\" in {\n    implicit val tid = transid()\n    val content = JsObject(\"exec\" -> JsObject(\"kind\" -> Exec.SEQUENCE.toJson, \"components\" -> Vector(\"a\", \"b\").toJson))\n\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      // the content will fail to deserialize on the route directive,\n      // and without a custom rejection, the response will be a string\n      responseAs[String] shouldBe s\"The request content was malformed:\\nrequirement failed: ${Messages.malformedFullyQualifiedEntityName}\"\n    }\n  }\n\n  it should \"reject create or update of a sequence with no components\" in {\n    implicit val tid = transid()\n    val content = JsObject(\"exec\" -> JsObject(\"kind\" -> Exec.SEQUENCE.toJson))\n    Seq(true, false).foreach { overwrite =>\n      Put(s\"$collectionPath/${aname()}?overwrite=$overwrite\", content) ~> Route.seal(routes(creds)) ~> check {\n        status should be(BadRequest)\n        // the content will fail to deserialize on the route directive,\n        // and without a custom rejection, the response will be a string\n        responseAs[String] shouldBe \"The request content was malformed:\\n'components' must be defined for sequence kind\"\n      }\n    }\n  }\n\n  it should \"reject update of a sequence with components that don't have at least namespace and action name\" in {\n    implicit val tid = transid()\n    val content = JsObject(\"exec\" -> JsObject(\"kind\" -> Exec.SEQUENCE.toJson, \"components\" -> Vector(\"a\", \"b\").toJson))\n\n    // update an action sequence\n    Put(s\"$collectionPath/${aname()}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      // the content will fail to deserialize on the route directive,\n      // and without a custom rejection, the response will be a string\n      responseAs[String] shouldBe s\"The request content was malformed:\\nrequirement failed: ${Messages.malformedFullyQualifiedEntityName}\"\n    }\n  }\n\n  it should \"create a sequence of type s -> (x, x) where x is a sequence and correctly count the atomic actions\" in {\n    implicit val tid = transid()\n    val actionCnt = 2\n\n    // make sequence x and install it in db\n    val xSeqName = s\"${aname()}_x\"\n    val components = for (i <- 1 to actionCnt) yield s\"${aname()}_p\"\n    putSimpleSequenceInDB(xSeqName, namespace, components.toVector)\n\n    // create an action sequence s\n    val sSeqName = s\"${aname()}_s\"\n    val sSeq = makeSimpleSequence(sSeqName, namespace, Vector(xSeqName, xSeqName), false) // x is installed in the db already\n    val content = WhiskActionPut(Some(sSeq.exec))\n\n    Console.withOut(stream) {\n      Put(s\"$collectionPath/$sSeqName\", content) ~> Route.seal(routes(creds)) ~> check {\n        deleteAction(sSeq.docid)\n        status should be(OK)\n        logContains(s\"atomic action count ${2 * actionCnt}\")(stream)\n      }\n    }\n  }\n\n  /**\n   * Tests the following sequence:\n   * y -> a\n   * x -> b, z\n   * s -> a, x, y\n   *\n   * Update z -> s should not work\n   * Update s -> a, s, b should not work\n   * Update z -> y should work (no cycle) act cnt 1\n   * Update s -> a, x, y, a, b should work (no cycle) act cnt 6\n   */\n  it should \"create a complex sequence, allow updates with no cycle and reject updates with cycle\" in {\n    val limit = whiskConfig.actionSequenceLimit.toInt\n    assert(whiskConfig.actionSequenceLimit.toInt >= 6)\n    implicit val tid = transid()\n    val actionCnt = 4\n    val aAct = s\"${aname()}_a\"\n    val yAct = s\"${aname()}_y\"\n    val yComp = Vector(aAct)\n    // make seq y and store it in the db\n    putSimpleSequenceInDB(yAct, namespace, yComp)\n    val bAct = s\"${aname()}_b\"\n    val zAct = s\"${aname()}_z\"\n    val xAct = s\"${aname()}_x\"\n    val xComp = Vector(bAct, zAct)\n    // make sequence x and install it in db\n    putSimpleSequenceInDB(xAct, namespace, xComp)\n    val sAct = s\"${aname()}_s\"\n    val sSeq = makeSimpleSequence(sAct, namespace, Vector(aAct, xAct, yAct), false) // a, x, y  in the db already\n    // create an action sequence s\n    val content = WhiskActionPut(Some(sSeq.exec))\n\n    stream.reset()\n    Console.withOut(stream) {\n      Put(s\"$collectionPath/$sAct\", content) ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n      }\n      logContains(\"atomic action count 4\")(stream)\n    }\n\n    // update action z to point to s --- should be rejected\n    val zUpdate = makeSimpleSequence(zAct, namespace, Vector(sAct), false) // s in the db already\n    val zUpdateContent = WhiskActionPut(Some(zUpdate.exec))\n    Put(s\"$collectionPath/$zAct?overwrite=true\", zUpdateContent) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceIsCyclic\n    }\n\n    // update action s to point to a, s, b --- should be rejected\n    val sUpdate = makeSimpleSequence(sAct, namespace, Vector(aAct, sAct, bAct), false) // s in the db already\n    val sUpdateContent = WhiskActionPut(Some(sUpdate.exec))\n    Put(s\"$collectionPath/$sAct?overwrite=true\", sUpdateContent) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[ErrorResponse].error shouldBe Messages.sequenceIsCyclic\n    }\n\n    // update action z to point to y\n    val zSeq = makeSimpleSequence(zAct, namespace, Vector(yAct), false) // y  in the db already\n    val updateContent = WhiskActionPut(Some(zSeq.exec))\n    stream.reset()\n    Console.withOut(stream) {\n      Put(s\"$collectionPath/$zAct?overwrite=true\", updateContent) ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n      }\n      logContains(\"atomic action count 1\")(stream)\n    }\n    // update sequence s to s -> a, x, y, a, b\n    val newS = makeSimpleSequence(sAct, namespace, Vector(aAct, xAct, yAct, aAct, bAct), false) // a, x, y, b  in the db already\n    val newSContent = WhiskActionPut(Some(newS.exec))\n    stream.reset()\n    Console.withOut(stream) {\n      Put(s\"${collectionPath}/$sAct?overwrite=true\", newSContent) ~> Route.seal(routes(creds)) ~> check {\n        deleteAction(sSeq.docid)\n        deleteAction(zSeq.docid)\n        status should be(OK)\n      }\n      logContains(\"atomic action count 6\")(stream)\n    }\n  }\n\n  private def logContains(w: String)(implicit stream: java.io.ByteArrayOutputStream): Boolean = {\n    org.apache.openwhisk.utils.retry({\n      val log = stream.toString()\n      val result = log.contains(w)\n      assert(result) // throws exception required to retry\n      result\n    }, 10, Some(100 milliseconds))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/SwaggerRoutesTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.model.Uri\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\nimport org.apache.openwhisk.core.controller.SwaggerDocs\n\n/**\n * Tests swagger routes.\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n\n@RunWith(classOf[JUnitRunner])\nclass SwaggerRoutesTests extends ControllerTestCommon with BeforeAndAfterEach {\n\n  behavior of \"Swagger routes\"\n\n  it should \"server docs\" in {\n    implicit val tid = transid()\n    val swagger = new SwaggerDocs(Uri.Path.Empty, \"infoswagger.json\")\n    Get(\"/docs\") ~> Route.seal(swagger.swaggerRoutes) ~> check {\n      status shouldBe PermanentRedirect\n      header(\"location\").get.value shouldBe \"docs/index.html?url=/api-docs\"\n    }\n\n    Get(\"/api-docs\") ~> Route.seal(swagger.swaggerRoutes) ~> check {\n      status shouldBe OK\n      responseAs[JsObject].fields(\"swagger\") shouldBe JsString(\"2.0\")\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/TriggersApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport java.time.Instant\n\nimport scala.concurrent.duration.DurationInt\nimport scala.language.postfixOps\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.model.headers.RawHeader\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\nimport org.apache.openwhisk.core.controller.WhiskTriggersApi\nimport org.apache.openwhisk.core.entitlement.Collection\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.WhiskRule\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.test.OldWhiskTrigger\nimport org.apache.openwhisk.http.ErrorResponse\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.core.database.UserContext\n\n/**\n * Tests Trigger API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n@RunWith(classOf[JUnitRunner])\nclass TriggersApiTests extends ControllerTestCommon with WhiskTriggersApi {\n\n  /** Triggers API tests */\n  behavior of \"Triggers API\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val context = UserContext(creds)\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n  def aname() = MakeName.next(\"triggers_tests\")\n  def afullname(namespace: EntityPath, name: String) = FullyQualifiedEntityName(namespace, EntityName(name))\n  val parametersLimit = Parameters.sizeLimit\n  val systemPayloadLimit = ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT\n  val dummyInstant = Instant.now()\n\n  //// GET /triggers\n  it should \"list triggers by default/explicit namespace\" in {\n    implicit val tid = transid()\n    val triggers = (1 to 2).map { i =>\n      WhiskTrigger(namespace, aname(), Parameters(\"x\", \"b\"))\n    }.toList\n    triggers foreach { put(entityStore, _) }\n    waitOnView(entityStore, WhiskTrigger, namespace, 2)\n    Get(s\"$collectionPath\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[JsObject]]\n      triggers.length should be(response.length)\n      response should contain theSameElementsAs triggers.map(_.summaryAsJson)\n    }\n\n    // it should \"list triggers with explicit namespace owned by subject\" in {\n    Get(s\"/$namespace/${collection.path}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[JsObject]]\n      triggers.length should be(response.length)\n      response should contain theSameElementsAs triggers.map(_.summaryAsJson)\n    }\n\n    // it should \"reject list triggers with explicit namespace not owned by subject\" in {\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"reject list when limit is greater than maximum allowed value\" in {\n    implicit val tid = transid()\n    val exceededMaxLimit = Collection.MAX_LIST_LIMIT + 1\n    val response = Get(s\"$collectionPath?limit=$exceededMaxLimit\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listLimitOutOfRange(Collection.TRIGGERS, exceededMaxLimit, Collection.MAX_LIST_LIMIT)\n      }\n    }\n  }\n\n  it should \"reject list when limit is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?limit=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.TRIGGERS, notAnInteger)\n      }\n    }\n  }\n\n  it should \"reject list when skip is negative\" in {\n    implicit val tid = transid()\n    val negativeSkip = -1\n    val response = Get(s\"$collectionPath?skip=$negativeSkip\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.listSkipOutOfRange(Collection.TRIGGERS, negativeSkip)\n      }\n    }\n  }\n\n  it should \"reject list when skip is not an integer\" in {\n    implicit val tid = transid()\n    val notAnInteger = \"string\"\n    val response = Get(s\"$collectionPath?skip=$notAnInteger\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n      responseAs[String] should include {\n        Messages.argumentNotInteger(Collection.TRIGGERS, notAnInteger)\n      }\n    }\n  }\n\n  // ?docs disabled\n  ignore should \"list triggers by default namespace with full docs\" in {\n    implicit val tid = transid()\n    val triggers = (1 to 2).map { i =>\n      WhiskTrigger(namespace, aname(), Parameters(\"x\", \"b\"))\n    }.toList\n    triggers foreach { put(entityStore, _) }\n    waitOnView(entityStore, WhiskTrigger, namespace, 2)\n    Get(s\"$collectionPath?docs=true\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[WhiskTrigger]]\n      triggers.length should be(response.length)\n      response should contain theSameElementsAs triggers.map(_.summaryAsJson)\n    }\n  }\n\n  //// GET /triggers/name\n  it should \"get trigger by name in default/explicit namespace\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname(), Parameters(\"x\", \"b\"))\n    put(entityStore, trigger)\n    Get(s\"$collectionPath/${trigger.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskTrigger]\n      response should be(trigger)\n    }\n\n    // it should \"get trigger by name in explicit namespace owned by subject\" in\n    Get(s\"/$namespace/${collection.path}/${trigger.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskTrigger]\n      response should be(trigger)\n    }\n\n    // it should \"reject get trigger by name in explicit namespace not owned by subject\" in\n    val auser = WhiskAuthHelpers.newIdentity()\n    Get(s\"/$namespace/${collection.path}/${trigger.name}\") ~> Route.seal(routes(auser)) ~> check {\n      status should be(Forbidden)\n    }\n  }\n\n  it should \"get trigger with updated field\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname(), Parameters(\"x\", \"b\"))\n    put(entityStore, trigger)\n\n    // `updated` field should be compared with a document in DB\n    val t = get(entityStore, trigger.docid, WhiskTrigger)\n\n    Get(s\"$collectionPath/${trigger.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskTrigger]\n      response should be(trigger copy (updated = t.updated))\n    }\n  }\n\n  it should \"report Conflict if the name was of a different type\" in {\n    implicit val tid = transid()\n    val rule = WhiskRule(\n      namespace,\n      aname(),\n      FullyQualifiedEntityName(namespace, aname()),\n      FullyQualifiedEntityName(namespace, aname()))\n    put(entityStore, rule)\n    Get(s\"/$namespace/${collection.path}/${rule.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(Conflict)\n    }\n  }\n\n  //// DEL /triggers/name\n  it should \"delete trigger by name\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname(), Parameters(\"x\", \"b\"))\n    put(entityStore, trigger)\n    Delete(s\"$collectionPath/${trigger.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskTrigger]\n      response should be(trigger)\n    }\n  }\n\n  //// PUT /triggers/name\n  it should \"put should accept request with missing optional properties\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname())\n    val content = WhiskTriggerPut()\n    Put(s\"$collectionPath/${trigger.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteTrigger(trigger.docid)\n      status should be(OK)\n      val response = responseAs[WhiskTrigger]\n      checkWhiskEntityResponse(response, trigger.withoutRules)\n    }\n  }\n\n  it should \"put should accept request with valid feed parameter\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname(), annotations = Parameters(Parameters.Feed, \"xyz\"))\n    val content = WhiskTriggerPut(annotations = Some(trigger.annotations))\n    Put(s\"$collectionPath/${trigger.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteTrigger(trigger.docid)\n      status should be(OK)\n      val response = responseAs[WhiskTrigger]\n      checkWhiskEntityResponse(response, trigger.withoutRules)\n    }\n  }\n\n  it should \"put should reject request with undefined feed parameter\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname(), annotations = Parameters(Parameters.Feed, \"\"))\n    val content = WhiskTriggerPut(annotations = Some(trigger.annotations))\n    Put(s\"$collectionPath/${trigger.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"put should reject request with bad feed parameters\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname(), annotations = Parameters(Parameters.Feed, \"a,b\"))\n    val content = WhiskTriggerPut(annotations = Some(trigger.annotations))\n    Put(s\"$collectionPath/${trigger.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(BadRequest)\n    }\n  }\n\n  it should \"reject activation with entity which is too big\" in {\n    implicit val tid = transid()\n    val code = \"a\" * (systemPayloadLimit.toBytes.toInt + 1)\n    val content = s\"\"\"{\"a\":\"$code\"}\"\"\".stripMargin\n    Post(s\"$collectionPath/${aname()}\", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(fieldDescriptionForSizeError, (content.length).B, systemPayloadLimit.toBytes.B))\n      }\n    }\n  }\n\n  it should \"reject create with parameters which are too big\" in {\n    implicit val tid = transid()\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val parameters = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val content = s\"\"\"{\"parameters\":$parameters}\"\"\".parseJson.asJsObject\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.paramsFieldName, parameters.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"reject create with annotations which are too big\" in {\n    implicit val tid = transid()\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val annotations = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val content = s\"\"\"{\"annotations\":$annotations}\"\"\".parseJson.asJsObject\n    Put(s\"$collectionPath/${aname()}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.annotationsFieldName, annotations.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"reject update with parameters which are too big\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname())\n    val keys: List[Long] =\n      List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)\n    val parameters = keys map { key =>\n      Parameters(key.toString, \"a\" * 10)\n    } reduce (_ ++ _)\n    val content = s\"\"\"{\"parameters\":$parameters}\"\"\".parseJson.asJsObject\n    put(entityStore, trigger)\n    Put(s\"$collectionPath/${trigger.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(ContentTooLarge)\n      responseAs[String] should include {\n        Messages.entityTooBig(SizeError(WhiskEntity.paramsFieldName, parameters.size, Parameters.sizeLimit))\n      }\n    }\n  }\n\n  it should \"put should accept update request with missing optional properties\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname(), Parameters(\"x\", \"b\"))\n    val content = WhiskTriggerPut()\n    put(entityStore, trigger)\n    Put(s\"$collectionPath/${trigger.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteTrigger(trigger.docid)\n      status should be(OK)\n      val response = responseAs[WhiskTrigger]\n      checkWhiskEntityResponse(\n        response,\n        WhiskTrigger(\n          trigger.namespace,\n          trigger.name,\n          trigger.parameters,\n          version = trigger.version.upPatch,\n          updated = dummyInstant).withoutRules)\n    }\n  }\n\n  it should \"put should reject update request for trigger with existing feed\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname(), annotations = Parameters(Parameters.Feed, \"xyz\"))\n    val content = WhiskTriggerPut(annotations = Some(trigger.annotations))\n    put(entityStore, trigger)\n    Put(s\"$collectionPath/${trigger.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteTrigger(trigger.docid)\n      status should be(OK)\n    }\n  }\n\n  it should \"put should reject update request for trigger with new feed\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname())\n    val content = WhiskTriggerPut(annotations = Some(Parameters(Parameters.Feed, \"xyz\")))\n    put(entityStore, trigger)\n    Put(s\"$collectionPath/${trigger.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteTrigger(trigger.docid)\n      status should be(OK)\n    }\n  }\n\n  //// POST /triggers/name\n  it should \"fire a trigger\" in {\n    implicit val tid = transid()\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, \"bogus action\"))\n    val trigger = WhiskTrigger(namespace, rule.trigger.name, rules = Some {\n      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))\n    })\n    val content = JsObject(\"xxx\" -> \"yyy\".toJson)\n    put(entityStore, trigger)\n    put(entityStore, rule)\n    Post(s\"$collectionPath/${trigger.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(Accepted)\n      val response = responseAs[JsObject]\n      val JsString(id) = response.fields(\"activationId\")\n      val activationId = ActivationId.parse(id).get\n      response.fields(\"activationId\") should not be None\n      headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n\n      val activationDoc = DocId(WhiskEntity.qualifiedName(namespace, activationId))\n      org.apache.openwhisk.utils.retry({\n        println(s\"trying to obtain async activation doc: '${activationDoc}'\")\n\n        val activation = getActivation(ActivationId(activationDoc.asString), context)\n        deleteActivation(ActivationId(activationDoc.asString), context)\n        activation.end should be(Instant.EPOCH)\n        activation.response.result should be(Some(content))\n      }, 30, Some(1.second))\n    }\n  }\n\n  it should \"fire a trigger without args\" in {\n    implicit val tid = transid()\n    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, \"bogus action\"))\n    val trigger = WhiskTrigger(namespace, rule.trigger.name, Parameters(\"x\", \"b\"), rules = Some {\n      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))\n    })\n    put(entityStore, trigger)\n    put(entityStore, rule)\n    Post(s\"$collectionPath/${trigger.name}\") ~> Route.seal(routes(creds)) ~> check {\n      val response = responseAs[JsObject]\n      val JsString(id) = response.fields(\"activationId\")\n      val activationId = ActivationId.parse(id).get\n      val activationDoc = DocId(WhiskEntity.qualifiedName(namespace, activationId))\n      org.apache.openwhisk.utils.retry({\n        println(s\"trying to delete async activation doc: '${activationDoc}'\")\n        deleteActivation(ActivationId(activationDoc.asString), context)\n        response.fields(\"activationId\") should not be None\n        headers should contain(RawHeader(ActivationIdHeader, response.fields(\"activationId\").convertTo[String]))\n      }, 30, Some(1.second))\n    }\n  }\n\n  it should \"not fire a trigger without a rule\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname())\n    put(entityStore, trigger)\n    Post(s\"$collectionPath/${trigger.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status shouldBe NoContent\n    }\n  }\n\n  //// invalid resource\n  it should \"reject invalid resource\" in {\n    implicit val tid = transid()\n    val trigger = WhiskTrigger(namespace, aname())\n    put(entityStore, trigger)\n    Get(s\"$collectionPath/${trigger.name}/bar\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(NotFound)\n    }\n  }\n\n  // migration path\n  it should \"be able to handle a trigger as of the old schema\" in {\n    implicit val tid = transid()\n    val trigger = OldWhiskTrigger(namespace, aname())\n    put(entityStore, trigger)\n    Get(s\"$collectionPath/${trigger.name}\") ~> Route.seal(routes(creds)) ~> check {\n      val response = responseAs[WhiskTrigger]\n      status should be(OK)\n      checkWhiskEntityResponse(response, trigger.toWhiskTrigger)\n    }\n  }\n\n  it should \"report proper error when record is corrupted on delete\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    Delete(s\"$collectionPath/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when record is corrupted on get\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    Get(s\"$collectionPath/${entity.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n\n  it should \"report proper error when record is corrupted on put\" in {\n    implicit val tid = transid()\n    val entity = BadEntity(namespace, aname())\n    put(entityStore, entity)\n\n    val content = WhiskTriggerPut()\n    Put(s\"$collectionPath/${entity.name}\", content) ~> Route.seal(routes(creds)) ~> check {\n      status should be(InternalServerError)\n      responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/WebActionsApiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport java.time.Instant\nimport java.util.Base64\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration.FiniteDuration\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.FormData\nimport org.apache.pekko.http.scaladsl.model.HttpEntity\nimport org.apache.pekko.http.scaladsl.model.MediaTypes\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.model.HttpCharsets\nimport org.apache.pekko.http.scaladsl.model.HttpHeader\nimport org.apache.pekko.http.scaladsl.model.HttpResponse\nimport org.apache.pekko.http.scaladsl.model.Uri.Query\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.model.HttpMethods\nimport org.apache.pekko.http.scaladsl.model.headers.{`Access-Control-Request-Headers`, `Content-Type`, RawHeader}\nimport org.apache.pekko.http.scaladsl.model.ContentTypes\nimport org.apache.pekko.http.scaladsl.model.ContentType\nimport org.apache.pekko.http.scaladsl.model.MediaType\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.controller._\nimport org.apache.openwhisk.core.entitlement.EntitlementProvider\nimport org.apache.openwhisk.core.entitlement.Privilege\nimport org.apache.openwhisk.core.entitlement.Resource\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.loadBalancer.LoadBalancer\nimport org.apache.openwhisk.http.ErrorResponse\nimport org.apache.openwhisk.http.Messages\n\nimport scala.collection.immutable.Set\n\n/**\n * Tests web actions API.\n *\n * Unit tests of the controller service as a standalone component.\n * These tests exercise a fresh instance of the service object in memory -- these\n * tests do NOT communication with a whisk deployment.\n *\n * @Idioglossia\n * \"using Specification DSL to write unit tests, as in should, must, not, be\"\n * \"using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>\"\n */\n\n@RunWith(classOf[JUnitRunner])\nclass WebActionsApiCommonTests extends AnyFlatSpec with Matchers {\n  \"extension splitter\" should \"split action name and extension\" in {\n    Seq(\".http\", \".json\", \".text\", \".html\", \".svg\").foreach { ext =>\n      Seq(s\"t$ext\", s\"tt$ext\", s\"t.wxyz$ext\", s\"tt.wxyz$ext\").foreach { s =>\n        Seq(true, false).foreach { enforce =>\n          val (n, e) = WhiskWebActionsApi.mediaTranscoderForName(s, enforce)\n          val i = s.lastIndexOf(\".\")\n          n shouldBe s.substring(0, i)\n          e.get.extension shouldBe ext\n        }\n      }\n    }\n\n    Seq(s\"t\", \"tt\", \"abcde\", \"abcdef\", \"t.wxyz\").foreach { s =>\n      val (n, e) = WhiskWebActionsApi.mediaTranscoderForName(s, false)\n      n shouldBe s\n      e.get.extension shouldBe \".http\"\n    }\n\n    Seq(s\"t\", \"tt\", \"abcde\", \"abcdef\", \"t.wxyz\").foreach { s =>\n      val (n, e) = WhiskWebActionsApi.mediaTranscoderForName(s, true)\n      n shouldBe s\n      e shouldBe empty\n    }\n  }\n}\n\n@RunWith(classOf[JUnitRunner])\nclass WebActionsApiTests extends AnyFlatSpec with Matchers with WebActionsApiBaseTests {\n  override lazy val webInvokePathSegments = Seq(\"web\")\n  override lazy val webApiDirectives = new WebApiDirectives()\n\n  \"properties\" should \"match verion\" in {\n    webApiDirectives.method shouldBe \"__ow_method\"\n    webApiDirectives.headers shouldBe \"__ow_headers\"\n    webApiDirectives.path shouldBe \"__ow_path\"\n    webApiDirectives.namespace shouldBe \"__ow_user\"\n    webApiDirectives.query shouldBe \"__ow_query\"\n    webApiDirectives.body shouldBe \"__ow_body\"\n    webApiDirectives.statusCode shouldBe \"statusCode\"\n    webApiDirectives.enforceExtension shouldBe false\n    webApiDirectives.reservedProperties shouldBe {\n      Set(\"__ow_method\", \"__ow_headers\", \"__ow_path\", \"__ow_user\", \"__ow_query\", \"__ow_body\")\n    }\n  }\n}\n\ntrait WebActionsApiBaseTests extends ControllerTestCommon with BeforeAndAfterEach with WhiskWebActionsApi {\n\n  val systemPayloadLimit = ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT\n  val namespacePayloadLimit = systemPayloadLimit - 100.KB\n\n  val uuid = UUID()\n  val systemId = Subject()\n  val systemKey = BasicAuthenticationAuthKey(uuid, Secret())\n  val systemIdentity =\n    Future.successful(\n      Identity(\n        systemId,\n        Namespace(EntityName(systemId.asString), uuid),\n        systemKey,\n        rights = Privilege.ALL,\n        limits = UserLimits(maxPayloadSize = Option(namespacePayloadLimit))))\n  val namespace = EntityPath(systemId.asString)\n  val proxyNamespace = namespace.addPath(EntityName(\"proxy\"))\n  override lazy val entitlementProvider = new TestingEntitlementProvider(whiskConfig, loadBalancer)\n  protected val testRoutePath = webInvokePathSegments.mkString(\"/\", \"/\", \"\")\n  def aname() = MakeName.next(\"web_action_tests\")\n\n  behavior of \"Web actions API\"\n\n  var failActivation = 0 // toggle to cause action to fail\n  var failThrottleForSubject: Option[Subject] = None // toggle to cause throttle to fail for subject\n  var failCheckEntitlement = false // toggle to cause entitlement to fail\n  var actionResult: Option[JsObject] = None\n  var testParametersInInvokeAction = true // toggle to test parameter in invokeAction\n  var requireAuthenticationKey = \"example-web-action-api-key\"\n  var invocationCount = 0\n  var invocationsAllowed = 0\n\n  lazy val testFixturesToGc = {\n    implicit val tid = transid()\n    Seq(\n      stubPackage,\n      stubAction(namespace, EntityName(\"export_c\")),\n      stubAction(proxyNamespace, EntityName(\"export_c\")),\n      stubAction(proxyNamespace, EntityName(\"raw_export_c\"))).map { f =>\n      put(entityStore, f, garbageCollect = false)\n    }\n  }\n\n  override def beforeAll() = {\n    testFixturesToGc.foreach(f => ())\n  }\n\n  override def beforeEach() = {\n    invocationCount = 0\n    invocationsAllowed = 0\n  }\n\n  override def afterEach() = {\n    failActivation = 0\n    failThrottleForSubject = None\n    failCheckEntitlement = false\n    actionResult = None\n    testParametersInInvokeAction = true\n    assert(invocationsAllowed == invocationCount, \"allowed invoke count did not match actual\")\n    cleanup()\n  }\n\n  override def afterAll() = {\n    implicit val tid = transid()\n    testFixturesToGc.foreach(delete(entityStore, _))\n  }\n\n  val allowedMethodsWithEntity = {\n    val nonModifierMethods = Seq(Get, Options)\n    val modifierMethods = Seq(Post, Put, Delete, Patch)\n    modifierMethods ++ nonModifierMethods\n  }\n\n  val allowedMethods = {\n    allowedMethodsWithEntity ++ Seq(Head)\n  }\n\n  val pngSample = \"iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAA/klEQVQYGWNgAAEHBxaG//+ZQMyyn581Pfas+cRQnf1LfF\" +\n    \"Ljf+62smUgcUbt0FA2Zh7drf/ffMy9vLn3RurrW9e5hCU11i2azfD4zu1/DHz8TAy/foUxsXBrFzHzC7r8+M9S1vn1qxQT07dDjL\" +\n    \"9fdemrqKxlYGT6z8AIMo6hgeUfA0PUvy9fGFh5GWK3z7vNxSWt++jX99+8SoyiGQwsW38w8PJEM7x5v5SJ8f+/xv8MDAzffv9hev\" +\n    \"fkWjiXBGMpMx+j2awovjcMjFztDO8+7GF49LkbZDCDeXLTWnZO7qDfn1/+5jbw/8pjYWS4wZLztXnuEuYTk2M+MzIw/AcA36Vewa\" +\n    \"D6fzsAAAAASUVORK5CYII=\"\n\n  // there is only one package that is predefined 'proxy'\n  val stubPackage = WhiskPackage(\n    EntityPath(systemId.asString),\n    EntityName(\"proxy\"),\n    parameters = Parameters(\"x\", JsString(\"X\")) ++ Parameters(\"z\", JsString(\"z\")))\n\n  val packages = Seq(stubPackage)\n\n  val defaultActionParameters = {\n    Parameters(\"y\", JsString(\"Y\")) ++ Parameters(\"z\", JsString(\"Z\")) ++ Parameters(\"empty\", JsNull)\n  }\n\n  // action names that start with 'export_' will automatically have an web-export annotation added by the test harness\n  protected def stubAction(namespace: EntityPath,\n                           name: EntityName,\n                           customOptions: Boolean = true,\n                           requireAuthentication: Boolean = false,\n                           requireAuthenticationAsBoolean: Boolean = true) = {\n\n    val annotations = Parameters(Annotations.FinalParamsAnnotationName, JsTrue)\n    WhiskAction(\n      namespace,\n      name,\n      jsDefault(\"??\"),\n      defaultActionParameters,\n      annotations = {\n        if (name.asString.startsWith(\"export_\")) {\n          annotations ++\n            Parameters(\"web-export\", JsTrue) ++ {\n            if (requireAuthentication) {\n              Parameters(\n                \"require-whisk-auth\",\n                (if (requireAuthenticationAsBoolean) JsTrue else JsString(requireAuthenticationKey)))\n            } else Parameters()\n          } ++ {\n            if (customOptions) {\n              Parameters(\"web-custom-options\", JsTrue)\n            } else Parameters()\n          }\n        } else if (name.asString.startsWith(\"raw_export_\")) {\n          annotations ++\n            Parameters(\"web-export\", JsTrue) ++\n            Parameters(\"raw-http\", JsTrue) ++ {\n            if (requireAuthentication) {\n              Parameters(\n                \"require-whisk-auth\",\n                (if (requireAuthenticationAsBoolean) JsTrue else JsString(requireAuthenticationKey)))\n            } else Parameters()\n          } ++ {\n            if (customOptions) {\n              Parameters(\"web-custom-options\", JsTrue)\n            } else Parameters()\n          }\n        } else annotations\n      })\n  }\n\n  // there is only one identity defined for the fully qualified name of the web action: 'systemId'\n  override protected def getIdentity(namespace: EntityName)(implicit transid: TransactionId): Future[Identity] = {\n    if (namespace.asString == systemId.asString) {\n      systemIdentity\n    } else {\n      logging.info(this, s\"namespace has no identity\")\n      Future.failed(RejectRequest(BadRequest))\n    }\n  }\n\n  override protected[controller] def invokeAction(\n    user: Identity,\n    action: WhiskActionMetaData,\n    payload: Option[JsValue],\n    waitForResponse: Option[FiniteDuration],\n    cause: Option[ActivationId])(implicit transid: TransactionId): Future[Either[ActivationId, WhiskActivation]] = {\n    invocationCount = invocationCount + 1\n\n    if (failActivation == 0) {\n      // construct a result stub that includes:\n      // 1. the package name for the action (to confirm that this resolved to systemId)\n      // 2. the action name (to confirm that this resolved to the expected action)\n      // 3. the payload received by the action which consists of the action.params + payload\n      val content = payload match {\n        case Some(JsObject(fields)) => action.parameters.merge(Some(JsObject(fields)))\n        case _                      => Some(action.parameters.toJsObject)\n      }\n      val result = actionResult getOrElse JsObject(\n        \"pkg\" -> action.namespace.toJson,\n        \"action\" -> action.name.toJson,\n        \"content\" -> content.get)\n\n      val activation = WhiskActivation(\n        action.namespace,\n        action.name,\n        user.subject,\n        ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now,\n        response = {\n          actionResult.flatMap { r =>\n            r.fields.get(\"application_error\").map { e =>\n              ActivationResponse.applicationError(e)\n            } orElse r.fields.get(\"developer_error\").map { e =>\n              ActivationResponse.developerError(e, None)\n            } orElse r.fields.get(\"whisk_error\").map { e =>\n              ActivationResponse.whiskError(e)\n            } orElse None // for clarity\n          } getOrElse ActivationResponse.success(Some(result))\n        })\n\n      // check that action parameters were merged with package\n      // all actions have default parameters (see stubAction)\n      if (testParametersInInvokeAction) {\n        if (!action.namespace.defaultPackage) {\n          action.parameters shouldBe (stubPackage.parameters ++ defaultActionParameters)\n        } else {\n          action.parameters shouldBe defaultActionParameters\n        }\n        action.parameters.get(\"z\") shouldBe defaultActionParameters.get(\"z\")\n      }\n\n      Future.successful(Right(activation))\n    } else if (failActivation == 1) {\n      Future.successful(Left(ActivationId.generate()))\n    } else {\n      Future.failed(new IllegalStateException(\"bad activation\"))\n    }\n  }\n\n  def metaPayload(method: String,\n                  params: JsObject,\n                  identity: Option[Identity],\n                  path: String = \"\",\n                  body: Option[JsObject] = None,\n                  pkgName: String = null,\n                  headers: List[HttpHeader] = List.empty) = {\n    val packageActionParams = Option(pkgName)\n      .filter(_ != null)\n      .flatMap(n => packages.find(_.name == EntityName(n)))\n      .map(_.parameters)\n      .getOrElse(Parameters())\n\n    (packageActionParams ++ defaultActionParameters).merge {\n      Some {\n        JsObject(\n          params.fields ++\n            body.map(_.fields).getOrElse(Map.empty) ++\n            Context(webApiDirectives, HttpMethods.getForKey(method.toUpperCase).get, headers, path, Query.Empty)\n              .metadata(identity))\n      }\n    }.get\n  }\n\n  def confirmErrorWithTid(error: JsObject, message: Option[String] = None) = {\n    error.fields.size shouldBe 2\n    error.fields.get(\"error\") shouldBe defined\n    message.foreach { m =>\n      error.fields.get(\"error\").get shouldBe JsString(m)\n    }\n    error.fields.get(\"code\") shouldBe defined\n    error.fields.get(\"code\").get shouldBe an[JsString]\n  }\n\n  Seq(None, Some(WhiskAuthHelpers.newIdentity())).foreach { creds =>\n    it should s\"not match invalid routes (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      // none of these should match a route\n      Seq(\"a\", \"a/b\", \"/a\", s\"$systemId/c\", s\"$systemId/export_c\").foreach { path =>\n        allowedMethods.foreach { m =>\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(NotFound)\n          }\n        }\n      }\n    }\n\n    it should s\"reject requests when Identity, package or action lookup fail or missing annotation (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      put(entityStore, stubAction(namespace, EntityName(\"c\")))\n\n      // the first of these fails in the identity lookup,\n      // the second in the package lookup (does not exist),\n      // the third fails the annotation check (no web-export annotation because name doesn't start with export_c)\n      // the fourth fails the action lookup\n      Seq(\"guest/proxy/c\", s\"$systemId/doesnotexist/c\", s\"$systemId/default/c\", s\"$systemId/proxy/export_fail\")\n        .foreach { path =>\n          allowedMethods.foreach { m =>\n            m(s\"$testRoutePath/${path}.json\") ~> Route.seal(routes(creds)) ~> check {\n              status should be(NotFound)\n            }\n\n            m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n              if (webApiDirectives.enforceExtension) {\n                status should be(NotAcceptable)\n                confirmErrorWithTid(\n                  responseAs[JsObject],\n                  Some(Messages.contentTypeExtensionNotSupported(WhiskWebActionsApi.allowedExtensions)))\n              } else {\n                status should be(NotFound)\n              }\n            }\n          }\n        }\n    }\n\n    it should s\"reject requests when whisk authentication is required but none given (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val entityName = MakeName.next(\"export\")\n      val action =\n        stubAction(\n          proxyNamespace,\n          entityName,\n          customOptions = false,\n          requireAuthentication = true,\n          requireAuthenticationAsBoolean = true)\n      val path = action.fullyQualifiedName(false)\n      put(entityStore, action)\n\n      allowedMethods.foreach { m =>\n        m(s\"$testRoutePath/${path}.json\") ~> Route.seal(routes(creds)) ~> check {\n          if (m === Options) {\n            status should be(OK) // options response is always present regardless of auth\n            header(\"Access-Control-Allow-Origin\").get.toString shouldBe \"Access-Control-Allow-Origin: *\"\n            header(\"Access-Control-Allow-Methods\").get.toString shouldBe \"Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH\"\n            header(\"Access-Control-Request-Headers\") shouldBe empty\n          } else if (creds.isEmpty) {\n            status should be(Unauthorized) // if user is not authenticated, reject all requests\n          } else {\n            invocationsAllowed += 1\n            status should be(OK)\n            val response = responseAs[JsObject]\n            response shouldBe JsObject(\n              \"pkg\" -> s\"$systemId/proxy\".toJson,\n              \"action\" -> entityName.asString.toJson,\n              \"content\" -> metaPayload(m.method.name.toLowerCase, JsObject.empty, creds, pkgName = \"proxy\"))\n            response\n              .fields(\"content\")\n              .asJsObject\n              .fields(webApiDirectives.namespace) shouldBe creds.get.namespace.name.toJson\n          }\n        }\n      }\n    }\n\n    it should s\"reject requests when x-authentication is required but none given (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val entityName = MakeName.next(\"export\")\n      val action =\n        stubAction(\n          proxyNamespace,\n          entityName,\n          customOptions = false,\n          requireAuthentication = true,\n          requireAuthenticationAsBoolean = false)\n      val path = action.fullyQualifiedName(false)\n      put(entityStore, action)\n\n      allowedMethods.foreach { m =>\n        // web action require-whisk-auth is set, but the header X-Require-Whisk-Auth value does not match\n        m(s\"$testRoutePath/${path}.json\") ~> addHeader(\n          WhiskAction.requireWhiskAuthHeader,\n          requireAuthenticationKey + \"-bad\") ~> Route\n          .seal(routes(creds)) ~> check {\n          if (m == Options) {\n            status should be(OK) // options should always respond\n            header(\"Access-Control-Allow-Origin\").get.toString shouldBe \"Access-Control-Allow-Origin: *\"\n            header(\"Access-Control-Allow-Methods\").get.toString shouldBe \"Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH\"\n            header(\"Access-Control-Request-Headers\") shouldBe empty\n          } else {\n            status should be(Unauthorized)\n          }\n        }\n\n        // web action require-whisk-auth is set, but the header X-Require-Whisk-Auth value is not set\n        m(s\"$testRoutePath/${path}.json\") ~> Route.seal(routes(creds)) ~> check {\n          if (m == Options) {\n            status should be(OK) // options should always respond\n            header(\"Access-Control-Allow-Origin\").get.toString shouldBe \"Access-Control-Allow-Origin: *\"\n            header(\"Access-Control-Allow-Methods\").get.toString shouldBe \"Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH\"\n            header(\"Access-Control-Request-Headers\") shouldBe empty\n          } else {\n            status should be(Unauthorized)\n          }\n        }\n\n        m(s\"$testRoutePath/${path}.json\") ~> addHeader(WhiskAction.requireWhiskAuthHeader, requireAuthenticationKey) ~> Route\n          .seal(routes(creds)) ~> check {\n          if (m == Options) {\n            status should be(OK) // options should always respond\n            header(\"Access-Control-Allow-Origin\").get.toString shouldBe \"Access-Control-Allow-Origin: *\"\n            header(\"Access-Control-Allow-Methods\").get.toString shouldBe \"Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH\"\n            header(\"Access-Control-Request-Headers\") shouldBe empty\n          } else {\n            invocationsAllowed += 1\n            status should be(OK)\n            val response = responseAs[JsObject]\n            response shouldBe JsObject(\n              \"pkg\" -> s\"$systemId/proxy\".toJson,\n              \"action\" -> entityName.asString.toJson,\n              \"content\" -> metaPayload(\n                m.method.name.toLowerCase,\n                JsObject.empty,\n                creds,\n                pkgName = \"proxy\",\n                headers = List(RawHeader(WhiskAction.requireWhiskAuthHeader, requireAuthenticationKey))))\n            if (creds.isDefined) {\n              response\n                .fields(\"content\")\n                .asJsObject\n                .fields(webApiDirectives.namespace) shouldBe creds.get.namespace.name.toJson\n            }\n          }\n        }\n      }\n    }\n\n    it should s\"invoke action that times out and provide a code (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n      failActivation = 1\n\n      allowedMethods.foreach { m =>\n        invocationsAllowed += 1\n\n        m(s\"$testRoutePath/$systemId/proxy/export_c.json\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(Accepted)\n          val response = responseAs[JsObject]\n          confirmErrorWithTid(response, Some(\"Response not yet ready.\"))\n        }\n      }\n    }\n\n    it should s\"invoke action that errors and respond with error and code (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n      failActivation = 2\n\n      allowedMethods.foreach { m =>\n        invocationsAllowed += 1\n\n        m(s\"$testRoutePath/$systemId/proxy/export_c.json\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(InternalServerError)\n          val response = responseAs[JsObject]\n          confirmErrorWithTid(response)\n        }\n      }\n    }\n\n    it should s\"invoke action and merge query parameters (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.json?a=b&c=d\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[JsObject]\n            response shouldBe JsObject(\n              \"pkg\" -> s\"$systemId/proxy\".toJson,\n              \"action\" -> \"export_c\".toJson,\n              \"content\" -> metaPayload(\n                m.method.name.toLowerCase,\n                Map(\"a\" -> \"b\", \"c\" -> \"d\").toJson.asJsObject,\n                creds,\n                pkgName = \"proxy\"))\n          }\n        }\n      }\n    }\n\n    it should s\"invoke action and merge body parameters (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      // both of these should produce full result objects (trailing slash is ok)\n      Seq(s\"$systemId/proxy/export_c.json\", s\"$systemId/proxy/export_c.json/\").foreach { path =>\n        allowedMethodsWithEntity.foreach { m =>\n          val content = JsObject(\"extra\" -> \"read all about it\".toJson, \"yummy\" -> true.toJson)\n          val p = if (path.endsWith(\"/\")) \"/\" else \"\"\n          invocationsAllowed += 1\n          m(s\"$testRoutePath/$path\", content) ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[JsObject]\n            response shouldBe JsObject(\n              \"pkg\" -> s\"$systemId/proxy\".toJson,\n              \"action\" -> \"export_c\".toJson,\n              \"content\" -> metaPayload(\n                m.method.name.toLowerCase,\n                JsObject.empty,\n                creds,\n                body = Some(content),\n                path = p,\n                pkgName = \"proxy\",\n                headers = List(`Content-Type`(ContentTypes.`application/json`))))\n          }\n        }\n      }\n    }\n\n    it should s\"invoke action which receives an empty entity (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(\"\", JsArray.empty.compactPrint, JsObject.empty.compactPrint, JsNull.compactPrint).foreach { arg =>\n        Seq(s\"$systemId/proxy/export_c.json\").foreach { path =>\n          allowedMethodsWithEntity.foreach { m =>\n            invocationsAllowed += 1\n            m(s\"$testRoutePath/$path\", HttpEntity(ContentTypes.`application/json`, arg)) ~> Route.seal(routes(creds)) ~> check {\n              status should be(OK)\n              val response = responseAs[JsObject]\n              response shouldBe JsObject(\n                \"pkg\" -> s\"$systemId/proxy\".toJson,\n                \"action\" -> \"export_c\".toJson,\n                \"content\" -> metaPayload(\n                  m.method.name.toLowerCase,\n                  if (arg.nonEmpty && arg != \"{}\") JsObject(webApiDirectives.body -> arg.parseJson) else JsObject.empty,\n                  creds,\n                  pkgName = \"proxy\",\n                  headers = List(`Content-Type`(ContentTypes.`application/json`))))\n            }\n          }\n        }\n      }\n    }\n\n    it should s\"invoke action and merge query and body parameters (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.json?a=b&c=d\").foreach { path =>\n        allowedMethodsWithEntity.foreach { m =>\n          val content = JsObject(\"extra\" -> \"read all about it\".toJson, \"yummy\" -> true.toJson)\n          invocationsAllowed += 1\n\n          m(s\"$testRoutePath/$path\", content) ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[JsObject]\n            response shouldBe JsObject(\n              \"pkg\" -> s\"$systemId/proxy\".toJson,\n              \"action\" -> \"export_c\".toJson,\n              \"content\" -> metaPayload(\n                m.method.name.toLowerCase,\n                Map(\"a\" -> \"b\", \"c\" -> \"d\").toJson.asJsObject,\n                creds,\n                body = Some(content),\n                pkgName = \"proxy\",\n                headers = List(`Content-Type`(ContentTypes.`application/json`))))\n          }\n        }\n      }\n    }\n\n    it should s\"invoke action in default package (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/default/export_c.json\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[JsObject]\n            response shouldBe JsObject(\n              \"pkg\" -> s\"$systemId\".toJson,\n              \"action\" -> \"export_c\".toJson,\n              \"content\" -> metaPayload(m.method.name.toLowerCase, JsObject.empty, creds))\n          }\n        }\n      }\n    }\n\n    it should s\"invoke action in a binding of private package (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val provider = WhiskPackage(EntityPath(systemId.asString), aname(), None, stubPackage.parameters)\n      val reference = WhiskPackage(EntityPath(systemId.asString), aname(), provider.bind)\n      val action = stubAction(provider.fullPath, EntityName(\"export_c\"))\n\n      put(entityStore, provider)\n      put(entityStore, reference)\n      put(entityStore, action)\n\n      Seq(s\"$systemId/${reference.name}/export_c.json\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n          }\n        }\n      }\n    }\n\n    it should s\"invoke action in a binding of public package (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val provider = WhiskPackage(EntityPath(\"guest\"), aname(), None, stubPackage.parameters, publish = true)\n      val reference = WhiskPackage(EntityPath(systemId.asString), aname(), provider.bind)\n      val action = stubAction(provider.fullPath, EntityName(\"export_c\"))\n\n      put(entityStore, provider)\n      put(entityStore, reference)\n      put(entityStore, action)\n\n      Seq(s\"$systemId/${reference.name}/export_c.json\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n          }\n        }\n      }\n    }\n\n    it should s\"invoke action relative to a binding where the action doesn't exist (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val provider = WhiskPackage(EntityPath(\"guest\"), aname(), None, stubPackage.parameters, publish = true)\n      val reference = WhiskPackage(EntityPath(systemId.asString), aname(), provider.bind)\n\n      put(entityStore, provider)\n      put(entityStore, reference)\n      // action is not created\n\n      Seq(s\"$systemId/${reference.name}/export_c.json\").foreach { path =>\n        allowedMethods.foreach { m =>\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(NotFound)\n          }\n        }\n      }\n    }\n\n    it should s\"invoke action in non-existing binding (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val provider = WhiskPackage(EntityPath(\"guest\"), aname(), None, stubPackage.parameters, publish = true)\n      val action = stubAction(provider.fullPath, EntityName(\"export_c\"))\n      val reference = WhiskPackage(EntityPath(systemId.asString), aname(), provider.bind)\n\n      put(entityStore, provider)\n      put(entityStore, action)\n      // reference is not created\n\n      Seq(s\"$systemId/${reference.name}/export_c.json\").foreach { path =>\n        allowedMethods.foreach { m =>\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(NotFound)\n          }\n        }\n      }\n    }\n\n    it should s\"not inherit annotations of package binding (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val provider = WhiskPackage(EntityPath(\"guest\"), aname(), None, stubPackage.parameters, publish = true)\n      val reference = WhiskPackage(\n        EntityPath(systemId.asString),\n        aname(),\n        provider.bind,\n        annotations = Parameters(\"web-export\", JsFalse))\n      val action = stubAction(provider.fullPath, EntityName(\"export_c\"))\n\n      put(entityStore, provider)\n      put(entityStore, reference)\n      put(entityStore, action)\n\n      Seq(s\"$systemId/${reference.name}/export_c.json\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n          }\n        }\n      }\n    }\n\n    it should s\"reject request that tries to override final parameters of action in package binding (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val provider = WhiskPackage(EntityPath(\"guest\"), aname(), None, publish = true)\n      val reference = WhiskPackage(EntityPath(systemId.asString), aname(), provider.bind, stubPackage.parameters)\n      val action = stubAction(provider.fullPath, EntityName(\"export_c\"))\n\n      put(entityStore, provider)\n      put(entityStore, reference)\n      put(entityStore, action)\n\n      val contentX = JsObject(\"x\" -> \"overridden\".toJson)\n      val contentZ = JsObject(\"z\" -> \"overridden\".toJson)\n\n      allowedMethodsWithEntity.foreach { m =>\n        invocationsAllowed += 1\n\n        m(s\"$testRoutePath/$systemId/${reference.name}/export_c.json?x=overridden\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n        }\n\n        m(s\"$testRoutePath/$systemId/${reference.name}/export_c.json?y=overridden\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n        }\n\n        m(s\"$testRoutePath/$systemId/${reference.name}/export_c.json\", contentX) ~> Route.seal(routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n        }\n\n        m(s\"$testRoutePath/$systemId/${reference.name}/export_c.json?y=overridden\", contentZ) ~> Route.seal(\n          routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n        }\n\n        m(s\"$testRoutePath/$systemId/${reference.name}/export_c.json?empty=overridden\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val response = responseAs[JsObject]\n          response shouldBe JsObject(\n            \"pkg\" -> s\"guest/${provider.name}\".toJson,\n            \"action\" -> \"export_c\".toJson,\n            \"content\" -> metaPayload(\n              m.method.name.toLowerCase,\n              Map(\"empty\" -> \"overridden\").toJson.asJsObject,\n              creds,\n              pkgName = \"proxy\"))\n        }\n      }\n    }\n\n    it should s\"match precedence order for merging parameters (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      testParametersInInvokeAction = false\n\n      val provider = WhiskPackage(\n        EntityPath(\"guest\"),\n        aname(),\n        None,\n        Parameters(\"a\", JsString(\"A\")) ++ Parameters(\"b\", JsString(\"b\")),\n        publish = true)\n      val reference = WhiskPackage(\n        EntityPath(systemId.asString),\n        aname(),\n        provider.bind,\n        Parameters(\"a\", JsString(\"a\")) ++ Parameters(\"c\", JsString(\"c\")))\n\n      // stub action has defaultActionParameters\n      val action = stubAction(provider.fullPath, EntityName(\"export_c\"))\n\n      put(entityStore, provider)\n      put(entityStore, reference)\n      put(entityStore, action)\n\n      Seq(s\"$systemId/${reference.name}/export_c.json\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[JsObject]\n\n            response shouldBe JsObject(\n              \"pkg\" -> s\"guest/${provider.name}\".toJson,\n              \"action\" -> \"export_c\".toJson,\n              \"content\" -> metaPayload(\n                m.method.name.toLowerCase,\n                Map(\"a\" -> \"a\", \"b\" -> \"b\", \"c\" -> \"c\").toJson.asJsObject,\n                creds))\n          }\n        }\n      }\n    }\n\n    it should s\"pass the unmatched segment to the action (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.json/content\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[JsObject].fields(\"content\")\n            response shouldBe metaPayload(\n              m.method.name.toLowerCase,\n              JsObject.empty,\n              creds,\n              path = \"/content\",\n              pkgName = \"proxy\")\n          }\n        }\n      }\n    }\n\n    it should s\"respond with error when expected text property does not exist (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.text\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(NotFound)\n            confirmErrorWithTid(responseAs[JsObject], Some(Messages.propertyNotFound))\n            // ensure that error message is pretty printed as { error, code }\n            responseAs[String].linesIterator should have size 4\n          }\n        }\n      }\n    }\n\n    it should s\"use action status code and headers to terminate an http response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          actionResult = Some(\n            JsObject(\n              \"headers\" -> JsObject(\"location\" -> \"http://openwhisk.org\".toJson),\n              webApiDirectives.statusCode -> Found.intValue.toJson))\n          invocationsAllowed += 1\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(Found)\n            header(\"location\").get.toString shouldBe \"location: http://openwhisk.org\"\n          }\n        }\n      }\n    }\n\n    it should s\"use non-standard action status code to terminate an http response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          actionResult = Some(JsObject(webApiDirectives.statusCode -> JsNumber(444)))\n          invocationsAllowed += 1\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(StatusCodes.custom(444, \"\"))\n          }\n        }\n      }\n    }\n\n    it should s\"use default field projection for extension (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(\n            JsObject(\n              \"headers\" -> JsObject(\"location\" -> \"http://openwhisk.org\".toJson),\n              webApiDirectives.statusCode -> Found.intValue.toJson))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(Found)\n            header(\"location\").get.toString shouldBe \"location: http://openwhisk.org\"\n          }\n        }\n      }\n\n      Seq(s\"$systemId/proxy/export_c.text\").foreach { path =>\n        allowedMethods.foreach { m =>\n          val text = \"default text\"\n          invocationsAllowed += 1\n          actionResult = Some(JsObject(\"text\" -> JsString(text)))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            contentType shouldBe MediaTypes.`text/plain`.withCharset(HttpCharsets.`UTF-8`)\n            val response = responseAs[String]\n            response shouldBe text\n          }\n        }\n      }\n\n      Seq(s\"$systemId/proxy/export_c.json\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(JsObject(\"foobar\" -> JsString(\"foobar\")))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[JsObject]\n            response shouldBe actionResult.get\n\n            // ensure response is pretty printed\n            responseAs[String] shouldBe {\n              \"\"\"{\n                |  \"foobar\": \"foobar\"\n                |}\"\"\".stripMargin\n            }\n          }\n        }\n      }\n\n      Seq(s\"$systemId/proxy/export_c.html\").foreach { path =>\n        allowedMethods.foreach { m =>\n          val html = \"<html>hi</htlml>\"\n          invocationsAllowed += 1\n          actionResult = Some(JsObject(\"html\" -> JsString(html)))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            contentType shouldBe MediaTypes.`text/html`.withCharset(HttpCharsets.`UTF-8`)\n            val response = responseAs[String]\n            response shouldBe html\n          }\n        }\n      }\n\n      Seq(s\"$systemId/proxy/export_c.svg\").foreach { path =>\n        allowedMethods.foreach { m =>\n          val svg = \"\"\"<svg><circle cx=\"3\" cy=\"3\" r=\"3\" fill=\"blue\"/></svg>\"\"\"\n          invocationsAllowed += 1\n          actionResult = Some(JsObject(\"svg\" -> JsString(svg)))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            //contentType shouldBe MediaTypes.`image/svg+xml`.withCharset(HttpCharsets.`UTF-8`)\n            val response = responseAs[String]\n            response shouldBe svg\n          }\n        }\n      }\n    }\n\n    it should s\"handle http web action and provide defaults (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      def confirmEmptyResponse() = {\n        status should be(NoContent)\n        response.entity shouldBe HttpEntity.Empty\n        withClue(headers) {\n          headers.length shouldBe 1\n          headers.exists(_.is(ActivationIdHeader)) should be(true)\n        }\n      }\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        Set(JsObject.empty, JsObject(\"body\" -> \"\".toJson), JsObject(\"body\" -> JsNull)).foreach { bodyResult =>\n          allowedMethods.foreach { m =>\n            invocationsAllowed += 2\n            actionResult = Some(bodyResult)\n\n            m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n              withClue(s\"failed for: $bodyResult\") {\n                confirmEmptyResponse()\n              }\n            }\n\n            // repeat with accept header, which should be ignored for content-negotiation\n            m(s\"$testRoutePath/$path\") ~> addHeader(\"Accept\", \"application/json\") ~> Route.seal(routes(creds)) ~> check {\n              withClue(s\"with accept header, failed for: $bodyResult\") {\n                confirmEmptyResponse()\n              }\n            }\n          }\n        }\n      }\n    }\n\n    it should s\"handle all JSON values with .text extension (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(JsObject(\"a\" -> \"A\".toJson), JsArray(\"a\".toJson), JsString(\"a\"), JsTrue, JsNumber(1), JsNull)\n        .foreach { jsval =>\n          val path = s\"$systemId/proxy/export_c.text\"\n          allowedMethods.foreach { m =>\n            invocationsAllowed += 1\n            actionResult = Some(JsObject(\"body\" -> jsval))\n\n            m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n              responseAs[String] shouldBe {\n                jsval match {\n                  case _: JsObject  => jsval.prettyPrint\n                  case _: JsArray   => jsval.prettyPrint\n                  case JsString(s)  => s\n                  case JsBoolean(b) => b.toString\n                  case JsNumber(n)  => n.toString\n                  case _            => \"null\"\n                }\n              }\n            }\n          }\n        }\n    }\n\n    it should s\"handle http web action with JSON object as string response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        Seq(OK, Created).foreach { statusCode =>\n          allowedMethods.foreach { m =>\n            invocationsAllowed += 1\n            actionResult = Some(\n              JsObject(\n                \"headers\" -> JsObject(\"content-type\" -> \"application/json\".toJson),\n                webApiDirectives.statusCode -> statusCode.intValue.toJson,\n                \"body\" -> JsObject(\"field\" -> \"value\".toJson).compactPrint.toJson))\n\n            m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n              status should be(statusCode)\n              mediaType shouldBe MediaTypes.`application/json`\n              responseAs[JsObject] shouldBe JsObject(\"field\" -> \"value\".toJson)\n            }\n          }\n        }\n      }\n    }\n\n    it should s\"handle http web action with partially specified result (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        // omit status code\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(\n            JsObject(\n              \"headers\" -> JsObject(\"content-type\" -> \"application/json\".toJson),\n              \"body\" -> JsObject(\"field\" -> \"value\".toJson)))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            responseAs[JsObject] shouldBe JsObject(\"field\" -> \"value\".toJson)\n          }\n        }\n\n        // omit status code and headers\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(JsObject(\"body\" -> JsObject(\"field\" -> \"value\".toJson).compactPrint.toJson))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            responseAs[String] shouldBe actionResult.get.fields(\"body\").convertTo[String]\n            contentType shouldBe MediaTypes.`text/html`.withCharset(HttpCharsets.`UTF-8`)\n          }\n        }\n\n        // omit headers only\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(\n            JsObject(\n              webApiDirectives.statusCode -> Created.intValue.toJson,\n              \"body\" -> JsObject(\"field\" -> \"value\".toJson).compactPrint.toJson))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(Created)\n            responseAs[String] shouldBe actionResult.get.fields(\"body\").convertTo[String]\n            contentType shouldBe MediaTypes.`text/html`.withCharset(HttpCharsets.`UTF-8`)\n          }\n        }\n\n        // omit body and headers\n        Seq(OK, Created, NoContent).foreach { statusCode =>\n          allowedMethods.foreach { m =>\n            invocationsAllowed += 1\n            actionResult = Some(JsObject(webApiDirectives.statusCode -> statusCode.intValue.toJson))\n\n            m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n              status should be(statusCode)\n              headers.size shouldBe 1\n              headers.exists(_.is(ActivationIdHeader)) should be(true)\n              response.entity shouldBe HttpEntity.Empty\n            }\n          }\n        }\n\n        // omit body but include headers\n        Seq(OK, Created, NoContent).foreach { statusCode =>\n          allowedMethods.foreach { m =>\n            invocationsAllowed += 1\n            actionResult = Some(\n              JsObject(\n                \"headers\" -> JsObject(\"Set-Cookie\" -> \"a=b\".toJson, \"content-type\" -> \"application/json\".toJson),\n                webApiDirectives.statusCode -> statusCode.intValue.toJson))\n\n            m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n              status should be(statusCode)\n              headers should contain(RawHeader(\"Set-Cookie\", \"a=b\"))\n              headers.exists(_.is(ActivationIdHeader)) should be(true)\n              response.entity shouldBe HttpEntity.Empty\n            }\n          }\n        }\n      }\n    }\n\n    it should s\"handle http web action with no body when status code is set (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        // omit body and headers, but add accept header on the request\n        Seq(OK, Created, NoContent).foreach { statusCode =>\n          allowedMethods.foreach { m =>\n            invocationsAllowed += 1\n            actionResult = Some(JsObject(webApiDirectives.statusCode -> statusCode.intValue.toJson))\n\n            m(s\"$testRoutePath/$path\") ~> addHeader(\"Accept\", \"application/json\") ~> Route.seal(routes(creds)) ~> check {\n              status should be(statusCode)\n              headers.size shouldBe 1\n              headers.exists(_.is(ActivationIdHeader)) should be(true)\n              response.entity shouldBe HttpEntity.Empty\n            }\n          }\n        }\n\n        // omit body but include headers, and add accept header on the request\n        Seq(OK, Created, NoContent).foreach { statusCode =>\n          allowedMethods.foreach { m =>\n            invocationsAllowed += 1\n            actionResult = Some(\n              JsObject(\n                \"headers\" -> JsObject(\"Set-Cookie\" -> \"a=b\".toJson, \"content-type\" -> \"application/json\".toJson),\n                webApiDirectives.statusCode -> statusCode.intValue.toJson))\n\n            m(s\"$testRoutePath/$path\") ~> addHeader(\"Accept\", \"application/json\") ~> Route.seal(routes(creds)) ~> check {\n              status should be(statusCode)\n              headers should contain(RawHeader(\"Set-Cookie\", \"a=b\"))\n              headers.exists(_.is(ActivationIdHeader)) should be(true)\n              response.entity shouldBe HttpEntity.Empty\n            }\n          }\n        }\n      }\n    }\n\n    it should s\"handle http web action with JSON object response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(\n        (JsObject(\"content-type\" -> \"application/json\".toJson), OK),\n        (JsObject.empty, OK),\n        (JsObject(\"content-type\" -> \"text/html\".toJson), BadRequest)).foreach {\n        case (headers, expectedCode) =>\n          Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n            allowedMethods.foreach { m =>\n              invocationsAllowed += 1\n              actionResult = Some(\n                JsObject(\n                  \"headers\" -> headers,\n                  webApiDirectives.statusCode -> OK.intValue.toJson,\n                  \"body\" -> JsObject(\"field\" -> \"value\".toJson)))\n\n              m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n                status should be(expectedCode)\n\n                if (expectedCode == OK) {\n                  header(\"content-type\").map(_.toString shouldBe \"content-type: application/json\")\n                  responseAs[JsObject] shouldBe JsObject(\"field\" -> \"value\".toJson)\n                } else {\n                  confirmErrorWithTid(responseAs[JsObject], Some(Messages.httpContentTypeError))\n                }\n              }\n            }\n          }\n      }\n    }\n    it should s\"handle http web action with base64 encoded known '+json' response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(\n            JsObject(\n              \"headers\" -> JsObject(\"content-type\" -> \"application/json-patch+json\".toJson),\n              webApiDirectives.statusCode -> OK.intValue.toJson,\n              \"body\" -> Base64.getEncoder.encodeToString {\n                JsObject(\"field\" -> \"value\".toJson).compactPrint.getBytes\n              }.toJson))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            mediaType.value shouldBe \"application/json-patch+json\"\n            responseAs[String].parseJson shouldBe JsObject(\"field\" -> \"value\".toJson)\n          }\n        }\n      }\n    }\n\n    it should s\"handle http web action for known '+json' response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(\n        (JsObject(\"content-type\" -> \"application/json-patch+json\".toJson), OK),\n        (JsObject(\"content-type\" -> \"text/html\".toJson), BadRequest)).foreach {\n        case (headers, expectedCode) =>\n          Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n            allowedMethods.foreach { m =>\n              invocationsAllowed += 1\n              actionResult = Some(\n                JsObject(\n                  \"headers\" -> headers,\n                  webApiDirectives.statusCode -> OK.intValue.toJson,\n                  \"body\" -> JsObject(\"field\" -> \"value\".toJson)))\n\n              m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n                status should be(expectedCode)\n\n                if (expectedCode == OK) {\n                  mediaType.value shouldBe \"application/json-patch+json\"\n                  responseAs[String].parseJson shouldBe JsObject(\"field\" -> \"value\".toJson)\n                } else {\n                  confirmErrorWithTid(responseAs[JsObject], Some(Messages.httpContentTypeError))\n                }\n              }\n            }\n          }\n      }\n    }\n\n    it should s\"handle http web action for unknown '+json' response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(\n        (JsObject(\"content-type\" -> \"application/hal+json\".toJson), OK),\n        (JsObject(\"content-type\" -> \"text/html\".toJson), BadRequest)).foreach {\n        case (headers, expectedCode) =>\n          Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n            allowedMethods.foreach { m =>\n              invocationsAllowed += 1\n              actionResult = Some(\n                JsObject(\n                  \"headers\" -> headers,\n                  webApiDirectives.statusCode -> OK.intValue.toJson,\n                  \"body\" -> JsObject(\"field\" -> \"value\".toJson)))\n\n              m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n                status should be(expectedCode)\n\n                if (expectedCode == OK) {\n                  mediaType.value shouldBe \"application/hal+json\"\n                  responseAs[String].parseJson shouldBe JsObject(\"field\" -> \"value\".toJson)\n                } else {\n                  confirmErrorWithTid(responseAs[JsObject], Some(Messages.httpContentTypeError))\n                }\n              }\n            }\n          }\n      }\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(JsObject(webApiDirectives.statusCode -> OK.intValue.toJson, \"body\" -> JsNumber(3)))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            header(\"content-type\").map(_.toString shouldBe \"content-type: application/json\")\n            responseAs[String].toInt shouldBe 3\n          }\n        }\n      }\n    }\n\n    it should s\"handle http web action with base64 encoded binary response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n      val expectedEntity = HttpEntity(ContentType(MediaTypes.`image/png`), Base64.getDecoder().decode(pngSample))\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(\n            JsObject(\n              \"headers\" -> JsObject(`Content-Type`.lowercaseName -> MediaTypes.`image/png`.toString.toJson),\n              \"body\" -> pngSample.toJson))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            response.entity shouldBe expectedEntity\n          }\n        }\n      }\n    }\n\n    it should s\"handle http web action with html/text response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult =\n            Some(JsObject(webApiDirectives.statusCode -> OK.intValue.toJson, \"body\" -> \"hello world\".toJson))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            responseAs[String] shouldBe \"hello world\"\n          }\n        }\n      }\n    }\n\n    it should s\"allow web action with incorrect application/json header and text response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(\n            JsObject(\n              \"headers\" -> JsObject(\"content-type\" -> \"application/json\".toJson),\n              webApiDirectives.statusCode -> OK.intValue.toJson,\n              \"body\" -> \"hello world\".toJson))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            mediaType shouldBe MediaTypes.`application/json`\n            headers.size shouldBe 1\n            headers.exists(_.is(ActivationIdHeader)) should be(true)\n            responseAs[String] shouldBe \"hello world\"\n          }\n        }\n      }\n    }\n\n    it should s\"reject http web action with invalid content-type header (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(\n            JsObject(\n              \"headers\" -> JsObject(\"content-type\" -> \"xyzbar\".toJson),\n              webApiDirectives.statusCode -> OK.intValue.toJson,\n              \"body\" -> \"hello world\".toJson))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(BadRequest)\n            confirmErrorWithTid(responseAs[JsObject], Some(Messages.httpUnknownContentType))\n          }\n        }\n      }\n    }\n\n    it should s\"handle an activation that results in application error (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(\n            JsObject(\n              \"application_error\" -> JsObject(\n                webApiDirectives.statusCode -> OK.intValue.toJson,\n                \"body\" -> \"no hello for you\".toJson)))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            responseAs[String] shouldBe \"no hello for you\"\n          }\n        }\n      }\n    }\n\n    it should s\"handle an activation that results in application error that does not match .json extension (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.json\").foreach { path =>\n        allowedMethods.foreach { m =>\n          invocationsAllowed += 1\n          actionResult = Some(JsObject(\"application_error\" -> \"bad response type\".toJson))\n\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(BadRequest)\n            confirmErrorWithTid(responseAs[JsObject], Some(Messages.invalidMedia(MediaTypes.`application/json`)))\n          }\n        }\n      }\n    }\n\n    it should s\"handle an activation that results in developer or system error (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.json\", s\"$systemId/proxy/export_c.text\")\n        .foreach { path =>\n          Seq(\"developer_error\", \"whisk_error\").foreach { e =>\n            allowedMethods.foreach { m =>\n              invocationsAllowed += 1\n              actionResult = Some(JsObject(e -> \"bad response type\".toJson))\n\n              m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n                status should be(BadRequest)\n                if (e == \"application_error\") {\n                  confirmErrorWithTid(responseAs[JsObject], Some(Messages.invalidMedia(MediaTypes.`application/json`)))\n                } else {\n                  confirmErrorWithTid(responseAs[JsObject], Some(Messages.errorProcessingRequest))\n                }\n              }\n            }\n          }\n        }\n    }\n\n    it should s\"support formdata (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.json\").foreach { path =>\n        val form = FormData(Map(\"field1\" -> \"value1\", \"field2\" -> \"value2\"))\n        invocationsAllowed += 1\n\n        Post(s\"$testRoutePath/$path\", form.toEntity) ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          responseAs[JsObject].fields(\"content\").asJsObject.fields(\"field1\") shouldBe JsString(\"value1\")\n          responseAs[JsObject].fields(\"content\").asJsObject.fields(\"field2\") shouldBe JsString(\"value2\")\n        }\n      }\n    }\n\n    it should s\"reject requests when entity size exceeds allowed system limit (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.json\").foreach { path =>\n        val largeEntity = \"a\" * (systemPayloadLimit.toBytes.toInt + 1)\n\n        val content = s\"\"\"{\"a\":\"$largeEntity\"}\"\"\"\n        Post(s\"$testRoutePath/$path\", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {\n          status should be(ContentTooLarge)\n          val expectedErrorMsg = Messages.entityTooBig(\n            // must contains namespace's payload limit size in error message\n            SizeError(fieldDescriptionForSizeError, (largeEntity.length + 8).B, namespacePayloadLimit.toBytes.B))\n          confirmErrorWithTid(responseAs[JsObject], Some(expectedErrorMsg))\n        }\n\n        val form = FormData(Map(\"a\" -> largeEntity))\n        Post(s\"$testRoutePath/$path\", form) ~> Route.seal(routes(creds)) ~> check {\n          status should be(ContentTooLarge)\n          val expectedErrorMsg = Messages.entityTooBig(\n            // must contains namespace's payload limit size in error message\n            SizeError(fieldDescriptionForSizeError, (largeEntity.length + 2).B, namespacePayloadLimit.toBytes.B))\n          confirmErrorWithTid(responseAs[JsObject], Some(expectedErrorMsg))\n        }\n      }\n    }\n\n    it should s\"allow requests when entity size does not exceed allowed namespace limit (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.json\").foreach { path =>\n        val largeEntity = \"a\" * (namespacePayloadLimit.toBytes.toInt - 10)\n\n        val content = s\"\"\"{\"a\":\"$largeEntity\"}\"\"\"\n        Post(s\"$testRoutePath/$path\", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n        }\n\n        val form = FormData(Map(\"a\" -> largeEntity))\n        Post(s\"$testRoutePath/$path\", form) ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n        }\n      }\n    }\n\n    it should s\"reject requests when entity size exceeds allowed namespace limit (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.json\").foreach { path =>\n        val largeEntity = \"a\" * (namespacePayloadLimit.toBytes.toInt + 1)\n\n        val content = s\"\"\"{\"a\":\"$largeEntity\"}\"\"\"\n        Post(s\"$testRoutePath/$path\", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {\n          status should be(ContentTooLarge)\n          val expectedErrorMsg = Messages.entityTooBig(\n            SizeError(fieldDescriptionForSizeError, (largeEntity.length + 8).B, namespacePayloadLimit.toBytes.B))\n          confirmErrorWithTid(responseAs[JsObject], Some(expectedErrorMsg))\n        }\n\n        val form = FormData(Map(\"a\" -> largeEntity))\n        Post(s\"$testRoutePath/$path\", form) ~> Route.seal(routes(creds)) ~> check {\n          status should be(ContentTooLarge)\n          val expectedErrorMsg = Messages.entityTooBig(\n            SizeError(fieldDescriptionForSizeError, (largeEntity.length + 2).B, namespacePayloadLimit.toBytes.B))\n          confirmErrorWithTid(responseAs[JsObject], Some(expectedErrorMsg))\n        }\n      }\n    }\n\n    it should s\"reject unknown extensions (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(\n        s\"$systemId/proxy/export_c.xyz\",\n        s\"$systemId/proxy/export_c.xyz/\",\n        s\"$systemId/proxy/export_c.xyz/content\",\n        s\"$systemId/proxy/export_c.xyzz\",\n        s\"$systemId/proxy/export_c.xyzz/\",\n        s\"$systemId/proxy/export_c.xyzz/content\").foreach { path =>\n        allowedMethods.foreach { m =>\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n\n            if (webApiDirectives.enforceExtension) {\n              status should be(NotAcceptable)\n              confirmErrorWithTid(\n                responseAs[JsObject],\n                Some(Messages.contentTypeExtensionNotSupported(WhiskWebActionsApi.allowedExtensions)))\n            } else {\n              status should be(NotFound)\n            }\n          }\n        }\n      }\n    }\n\n    it should s\"reject request that tries to override reserved properties (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      allowedMethodsWithEntity.foreach { m =>\n        webApiDirectives.reservedProperties.foreach { p =>\n          m(s\"$testRoutePath/$systemId/proxy/export_c.json?$p=YYY\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(BadRequest)\n            responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n          }\n\n          m(s\"$testRoutePath/$systemId/proxy/export_c.json\", JsObject(p -> \"YYY\".toJson)) ~> Route.seal(routes(creds)) ~> check {\n            status should be(BadRequest)\n            responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n          }\n        }\n      }\n    }\n\n    it should s\"reject request that tries to override final parameters (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n      val contentX = JsObject(\"x\" -> \"overridden\".toJson)\n      val contentZ = JsObject(\"z\" -> \"overridden\".toJson)\n\n      allowedMethodsWithEntity.foreach { m =>\n        invocationsAllowed += 1\n\n        m(s\"$testRoutePath/$systemId/proxy/export_c.json?x=overridden\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n        }\n\n        m(s\"$testRoutePath/$systemId/proxy/export_c.json?y=overridden\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n        }\n\n        m(s\"$testRoutePath/$systemId/proxy/export_c.json\", contentX) ~> Route.seal(routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n        }\n\n        m(s\"$testRoutePath/$systemId/proxy/export_c.json?y=overridden\", contentZ) ~> Route.seal(routes(creds)) ~> check {\n          status should be(BadRequest)\n          responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed\n        }\n\n        m(s\"$testRoutePath/$systemId/proxy/export_c.json?empty=overridden\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          val response = responseAs[JsObject]\n          response shouldBe JsObject(\n            \"pkg\" -> s\"$systemId/proxy\".toJson,\n            \"action\" -> \"export_c\".toJson,\n            \"content\" -> metaPayload(\n              m.method.name.toLowerCase,\n              Map(\"empty\" -> \"overridden\").toJson.asJsObject,\n              creds,\n              pkgName = \"proxy\"))\n        }\n      }\n    }\n\n    it should s\"inline body when receiving entity that is not a JsObject (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n      val str = \"1,2,3\"\n      invocationsAllowed = 3\n\n      Post(s\"$testRoutePath/$systemId/proxy/export_c.json\", HttpEntity(ContentTypes.`text/html(UTF-8)`, str)) ~> Route\n        .seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response shouldBe JsObject(\n          \"pkg\" -> s\"$systemId/proxy\".toJson,\n          \"action\" -> \"export_c\".toJson,\n          \"content\" -> metaPayload(\n            Post.method.name.toLowerCase,\n            JsObject(webApiDirectives.body -> str.toJson),\n            creds,\n            pkgName = \"proxy\",\n            headers = List(`Content-Type`(ContentTypes.`text/html(UTF-8)`))))\n      }\n\n      Post(s\"$testRoutePath/$systemId/proxy/export_c.json?a=b&c=d\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response shouldBe JsObject(\n          \"pkg\" -> s\"$systemId/proxy\".toJson,\n          \"action\" -> \"export_c\".toJson,\n          \"content\" -> metaPayload(\n            Post.method.name.toLowerCase,\n            Map(\"a\" -> \"b\", \"c\" -> \"d\").toJson.asJsObject,\n            creds,\n            pkgName = \"proxy\"))\n      }\n\n      Post(s\"$testRoutePath/$systemId/proxy/export_c.json?a=b&c=d\", JsObject.empty) ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response shouldBe JsObject(\n          \"pkg\" -> s\"$systemId/proxy\".toJson,\n          \"action\" -> \"export_c\".toJson,\n          \"content\" -> metaPayload(\n            Post.method.name.toLowerCase,\n            Map(\"a\" -> \"b\", \"c\" -> \"d\").toJson.asJsObject,\n            creds,\n            pkgName = \"proxy\",\n            headers = List(`Content-Type`(ContentTypes.`application/json`))))\n      }\n    }\n\n    it should s\"throttle subject owning namespace for web action (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      // this should fail for exceeding quota\n      Seq(s\"$systemId/proxy/export_c.text/content/z\").foreach { path =>\n        allowedMethods.foreach { m =>\n          failThrottleForSubject = Some(systemId)\n          m(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n            status should be(TooManyRequests)\n            confirmErrorWithTid(responseAs[JsObject], Some(Messages.tooManyRequests(2, 1)))\n          }\n          failThrottleForSubject = None\n        }\n      }\n    }\n\n    it should s\"respond with custom options (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        invocationsAllowed += 1 // custom options means action is invoked\n        actionResult =\n          Some(JsObject(\"headers\" -> JsObject(\"Access-Control-Allow-Methods\" -> \"OPTIONS, GET, PATCH\".toJson)))\n\n        // the added headers should be ignored\n        Options(s\"$testRoutePath/$path\") ~> addHeader(`Access-Control-Request-Headers`(\"x-custom-header\")) ~> Route\n          .seal(routes(creds)) ~> check {\n          header(\"Access-Control-Allow-Origin\") shouldBe empty\n          header(\"Access-Control-Allow-Methods\").get.toString shouldBe \"Access-Control-Allow-Methods: OPTIONS, GET, PATCH\"\n          header(\"Access-Control-Request-Headers\") shouldBe empty\n        }\n      }\n    }\n\n    it should s\"respond with custom options even when authentication is required but missing (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val entityName = MakeName.next(\"export\")\n      val action =\n        stubAction(\n          proxyNamespace,\n          entityName,\n          customOptions = true,\n          requireAuthentication = true,\n          requireAuthenticationAsBoolean = true)\n      val path = action.fullyQualifiedName(false)\n      put(entityStore, action)\n\n      invocationsAllowed += 1 // custom options means action is invoked\n      actionResult =\n        Some(JsObject(\"headers\" -> JsObject(\"Access-Control-Allow-Methods\" -> \"OPTIONS, GET, PATCH\".toJson)))\n\n      // the added headers should be ignored\n      Options(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n        header(\"Access-Control-Allow-Origin\") shouldBe empty\n        header(\"Access-Control-Allow-Methods\").get.toString shouldBe \"Access-Control-Allow-Methods: OPTIONS, GET, PATCH\"\n        header(\"Access-Control-Request-Headers\") shouldBe empty\n      }\n    }\n\n    it should s\"support multiple values for headers (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        invocationsAllowed += 1\n        actionResult =\n          Some(JsObject(\"headers\" -> JsObject(\"Set-Cookie\" -> JsArray(JsString(\"a=b\"), JsString(\"c=d; Path = /\")))))\n\n        Options(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n          headers should contain allOf (RawHeader(\"Set-Cookie\", \"a=b\"),\n          RawHeader(\"Set-Cookie\", \"c=d; Path = /\"))\n        }\n      }\n    }\n\n    it should s\"invoke action and respond with default options headers (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      put(entityStore, stubAction(proxyNamespace, EntityName(\"export_without_custom_options\"), false))\n\n      Seq(s\"$systemId/proxy/export_without_custom_options.http\", s\"$systemId/proxy/export_without_custom_options.json\")\n        .foreach { path =>\n          Seq(`Access-Control-Request-Headers`(\"x-custom-header\"), RawHeader(\"x-custom-header\", \"value\")).foreach {\n            testHeader =>\n              allowedMethods.foreach { m =>\n                if (m != Options) invocationsAllowed += 1 // options verb does not invoke an action\n                m(s\"$testRoutePath/$path\") ~> addHeader(testHeader) ~> Route.seal(routes(creds)) ~> check {\n                  header(\"Access-Control-Allow-Origin\").get.toString shouldBe \"Access-Control-Allow-Origin: *\"\n                  header(\"Access-Control-Allow-Methods\").get.toString shouldBe \"Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH\"\n                  if (testHeader.name == `Access-Control-Request-Headers`.name) {\n                    header(\"Access-Control-Allow-Headers\").get.toString shouldBe \"Access-Control-Allow-Headers: x-custom-header\"\n                  } else {\n                    header(\"Access-Control-Allow-Headers\").get.toString shouldBe \"Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, User-Agent\"\n                  }\n                }\n              }\n          }\n        }\n    }\n\n    it should s\"invoke action with head verb (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        invocationsAllowed += 1\n        actionResult = Some(JsObject(\"headers\" -> JsObject(\"location\" -> \"http://openwhisk.org\".toJson)))\n\n        Head(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n          header(\"location\").get.toString shouldBe \"location: http://openwhisk.org\"\n        }\n      }\n    }\n\n    it should s\"handle html web action with text/xml response (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.html\").foreach { path =>\n        val html = \"\"\"<html><body>test</body></html>\"\"\"\n        val xml = \"\"\"<?xml version=\"1.0\" encoding=\"UTF-8\"?><note><from>test</from></note>\"\"\"\n        invocationsAllowed += 2\n        actionResult = Some(JsObject(\"html\" -> xml.toJson))\n\n        Seq((html, MediaTypes.`text/html`), (xml, MediaTypes.`text/html`)).foreach {\n          case (res, expectedMediaType) =>\n            actionResult = Some(JsObject(\"html\" -> res.toJson))\n\n            Get(s\"$testRoutePath/$path\") ~> addHeader(\"Accept\", expectedMediaType.value) ~> Route.seal(routes(creds)) ~> check {\n              status should be(OK)\n              contentType shouldBe ContentTypes.`text/html(UTF-8)`\n              responseAs[String] shouldBe res\n              mediaType shouldBe expectedMediaType\n            }\n        }\n      }\n    }\n\n    it should s\"not fail a raw http action when query or body parameters overlap with final action parameters (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n      invocationsAllowed = 2\n\n      val queryString = \"x=overridden&key2=value2\"\n      Post(s\"$testRoutePath/$systemId/proxy/raw_export_c.json?$queryString\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response shouldBe JsObject(\n          \"pkg\" -> s\"$systemId/proxy\".toJson,\n          \"action\" -> \"raw_export_c\".toJson,\n          \"content\" -> metaPayload(\n            Post.method.name.toLowerCase,\n            Map(webApiDirectives.body -> \"\".toJson, webApiDirectives.query -> queryString.toJson).toJson.asJsObject,\n            creds,\n            pkgName = \"proxy\"))\n      }\n\n      Post(\n        s\"$testRoutePath/$systemId/proxy/raw_export_c.json\",\n        JsObject(\"x\" -> \"overridden\".toJson, \"key2\" -> \"value2\".toJson)) ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response shouldBe JsObject(\n          \"pkg\" -> s\"$systemId/proxy\".toJson,\n          \"action\" -> \"raw_export_c\".toJson,\n          \"content\" -> metaPayload(\n            Post.method.name.toLowerCase,\n            Map(webApiDirectives.query -> \"\".toJson, webApiDirectives.body -> Base64.getEncoder.encodeToString {\n              JsObject(\"x\" -> JsString(\"overridden\"), \"key2\" -> JsString(\"value2\")).compactPrint.getBytes\n            }.toJson).toJson.asJsObject,\n            creds,\n            pkgName = \"proxy\",\n            headers = List(`Content-Type`(ContentTypes.`application/json`))))\n      }\n    }\n\n    it should s\"invoke raw action ensuring body and query arguments are set properly (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val queryString = \"key1=value1&key2=value2\"\n      Seq(\n        \"1,2,3\",\n        JsObject(\"a\" -> \"A\".toJson, \"b\" -> \"B\".toJson).prettyPrint,\n        JsObject(\"a\" -> \"A\".toJson, \"b\" -> \"B\".toJson).compactPrint).foreach { str =>\n        Post(\n          s\"$testRoutePath/$systemId/proxy/raw_export_c.json?$queryString\",\n          HttpEntity(ContentTypes.`application/json`, str)) ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          invocationsAllowed += 1\n          val response = responseAs[JsObject]\n          response shouldBe JsObject(\n            \"pkg\" -> s\"$systemId/proxy\".toJson,\n            \"action\" -> \"raw_export_c\".toJson,\n            \"content\" -> metaPayload(\n              Post.method.name.toLowerCase,\n              Map(webApiDirectives.body -> Base64.getEncoder.encodeToString {\n                str.getBytes\n              }.toJson, webApiDirectives.query -> queryString.toJson).toJson.asJsObject,\n              creds,\n              pkgName = \"proxy\",\n              headers = List(`Content-Type`(ContentTypes.`application/json`))))\n        }\n      }\n    }\n\n    it should s\"invoke raw action ensuring body and query arguments are empty strings when not specified in request (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Post(s\"$testRoutePath/$systemId/proxy/raw_export_c.json\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        invocationsAllowed += 1\n        val response = responseAs[JsObject]\n        response shouldBe JsObject(\n          \"pkg\" -> s\"$systemId/proxy\".toJson,\n          \"action\" -> \"raw_export_c\".toJson,\n          \"content\" -> metaPayload(\n            Post.method.name.toLowerCase,\n            Map(webApiDirectives.body -> \"\".toJson, webApiDirectives.query -> \"\".toJson).toJson.asJsObject,\n            creds,\n            pkgName = \"proxy\"))\n      }\n    }\n\n    it should s\"reject invocation of web action with invalid accept header (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        actionResult = Some(JsObject(\"body\" -> \"Plain text\".toJson))\n        invocationsAllowed += 1\n\n        Get(s\"$testRoutePath/$path\") ~> addHeader(\"Accept\", \"application/json\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(NotAcceptable)\n          response shouldBe HttpResponse(\n            NotAcceptable,\n            entity = \"Resource representation is only available with these types:\\ntext/html; charset=UTF-8\")\n        }\n      }\n    }\n\n    it should s\"reject invocation of web action which has no entitlement (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.http\").foreach { path =>\n        actionResult = Some(JsObject(\"body\" -> \"Plain text\".toJson))\n        failCheckEntitlement = true\n\n        Get(s\"$testRoutePath/$path\") ~> Route.seal(routes(creds)) ~> check {\n          status should be(Forbidden)\n        }\n      }\n    }\n\n    it should s\"not invoke an action more than once when determining entity type (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(s\"$systemId/proxy/export_c.html\").foreach { path =>\n        val html = \"\"\"<html><body>test</body></html>\"\"\"\n        val xml = \"\"\"<?xml version=\"1.0\" encoding=\"UTF-8\"?><note><from>test</from></note>\"\"\"\n        invocationsAllowed += 1\n        actionResult = Some(JsObject(\"html\" -> xml.toJson))\n\n        Get(s\"$testRoutePath/$path\") ~> addHeader(\"Accept\", MediaTypes.`text/xml`.value) ~> Route.seal(routes(creds)) ~> check {\n          status should be(NotAcceptable)\n        }\n      }\n\n      withClue(s\"allowed invoke count did not match actual\") {\n        invocationsAllowed shouldBe invocationCount\n      }\n    }\n\n    it should s\"invoke web action ensuring JSON value body arguments are received as is (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      Seq(\"this is a string\".toJson, JsArray(1.toJson, \"str str\".toJson, false.toJson), true.toJson, 99.toJson)\n        .foreach { str =>\n          invocationsAllowed += 1\n          Post(\n            s\"$testRoutePath/$systemId/proxy/export_c.json\",\n            HttpEntity(ContentTypes.`application/json`, str.compactPrint)) ~> Route.seal(routes(creds)) ~> check {\n            status should be(OK)\n            val response = responseAs[JsObject]\n            response shouldBe JsObject(\n              \"pkg\" -> s\"$systemId/proxy\".toJson,\n              \"action\" -> \"export_c\".toJson,\n              \"content\" -> metaPayload(\n                Post.method.name.toLowerCase,\n                Map(webApiDirectives.body -> str).toJson.asJsObject,\n                creds,\n                pkgName = \"proxy\",\n                headers = List(`Content-Type`(ContentTypes.`application/json`))))\n          }\n        }\n    }\n\n    it should s\"invoke web action ensuring binary body is base64 encoded (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n      val entity = HttpEntity(ContentType(MediaTypes.`image/png`), Base64.getDecoder().decode(pngSample))\n\n      invocationsAllowed += 1\n      Post(s\"$testRoutePath/$systemId/proxy/export_c.json\", entity) ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n        val response = responseAs[JsObject]\n        response shouldBe JsObject(\n          \"pkg\" -> s\"$systemId/proxy\".toJson,\n          \"action\" -> \"export_c\".toJson,\n          \"content\" -> metaPayload(\n            Post.method.name.toLowerCase,\n            Map(webApiDirectives.body -> pngSample.toJson).toJson.asJsObject,\n            creds,\n            pkgName = \"proxy\",\n            headers = List(RawHeader(`Content-Type`.lowercaseName, MediaTypes.`image/png`.toString))))\n      }\n    }\n\n    it should s\"allowed string based status code (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n      invocationsAllowed += 3\n\n      actionResult = Some(JsObject(webApiDirectives.statusCode -> JsString(\"200\")))\n      Head(s\"$testRoutePath/$systemId/proxy/export_c.http\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(OK)\n      }\n\n      actionResult = Some(JsObject(webApiDirectives.statusCode -> JsString(\"444\")))\n      Head(s\"$testRoutePath/$systemId/proxy/export_c.http\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(StatusCodes.custom(444, \"\"))\n      }\n\n      actionResult = Some(JsObject(webApiDirectives.statusCode -> JsString(\"xyz\")))\n      Head(s\"$testRoutePath/$systemId/proxy/export_c.http\") ~> Route.seal(routes(creds)) ~> check {\n        status should be(BadRequest)\n      }\n    }\n\n    it should s\"support json (including +json subtypes) (auth? ${creds.isDefined})\" in {\n      implicit val tid = transid()\n\n      val path = s\"$systemId/proxy/export_c.json\"\n      val entity = JsObject(\"field1\" -> \"value1\".toJson)\n\n      Seq(\n        ContentType(MediaType.applicationWithFixedCharset(\"cloudevents+json\", HttpCharsets.`UTF-8`)),\n        ContentTypes.`application/json`).foreach { ct =>\n        invocationsAllowed += 1\n        Post(s\"$testRoutePath/$path\", HttpEntity(ct, entity.compactPrint)) ~> Route.seal(routes(creds)) ~> check {\n          status should be(OK)\n          responseAs[JsObject].fields(\"content\").asJsObject.fields(\"field1\") shouldBe entity.fields(\"field1\")\n        }\n      }\n    }\n  }\n\n  class TestingEntitlementProvider(config: WhiskConfig, loadBalancer: LoadBalancer)\n      extends EntitlementProvider(config, loadBalancer, ControllerInstanceId(\"0\")) {\n\n    // The check method checks both throttle and entitlement.\n    protected[core] override def check(user: Identity, right: Privilege, resource: Resource)(\n      implicit transid: TransactionId): Future[Unit] = {\n      val subject = user.subject\n\n      // first, check entitlement\n      if (failCheckEntitlement) {\n        Future.failed(RejectRequest(Forbidden))\n      } else {\n        // then, check throttle\n        logging.debug(this, s\"test throttle is checking user '$subject' has not exceeded activation quota\")\n        failThrottleForSubject match {\n          case Some(subject) if subject == user.subject =>\n            Future.failed(RejectRequest(TooManyRequests, Messages.tooManyRequests(2, 1)))\n          case _ => Future.successful({})\n        }\n      }\n    }\n\n    protected[core] override def grant(user: Identity, right: Privilege, resource: Resource)(\n      implicit transid: TransactionId) = ???\n\n    /** Revokes subject right to resource by removing them from the entitlement matrix. */\n    protected[core] override def revoke(user: Identity, right: Privilege, resource: Resource)(\n      implicit transid: TransactionId) = ???\n\n    /** Checks if subject has explicit grant for a resource. */\n    protected override def entitled(user: Identity, right: Privilege, resource: Resource)(\n      implicit transid: TransactionId) = ???\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/WhiskAuthHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test\n\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entitlement.Privilege\n\nobject WhiskAuthHelpers {\n  def newAuth(s: Subject = Subject(), k: BasicAuthenticationAuthKey = BasicAuthenticationAuthKey()) = {\n    WhiskAuth(s, Set(WhiskNamespace(Namespace(EntityName(s.asString), k.uuid), k)))\n  }\n\n  def newIdentity(s: Subject = Subject(), k: BasicAuthenticationAuthKey = BasicAuthenticationAuthKey()) = {\n    Identity(s, Namespace(EntityName(s.asString), k.uuid), k, rights = Privilege.ALL)\n  }\n\n  def newIdentityGenricAuth(s: Subject = Subject(), uuid: UUID = UUID(), k: GenericAuthKey) = {\n    Identity(s, Namespace(EntityName(s.asString), uuid), k, rights = Privilege.ALL)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/controller/test/migration/SequenceActionApiMigrationTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.controller.test.migration\n\nimport scala.Vector\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.TestHelpers\nimport common.WskTestHelpers\n\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\n\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport org.apache.openwhisk.core.controller.WhiskActionsApi\nimport org.apache.openwhisk.core.controller.test.ControllerTestCommon\nimport org.apache.openwhisk.core.controller.test.WhiskAuthHelpers\nimport org.apache.openwhisk.core.entity._\n\n/**\n * Tests migration of a new implementation of sequences: old style sequences can be updated and retrieved - standalone tests\n */\n@RunWith(classOf[JUnitRunner])\nclass SequenceActionApiMigrationTests\n    extends ControllerTestCommon\n    with WhiskActionsApi\n    with TestHelpers\n    with WskTestHelpers {\n\n  behavior of \"Sequence Action API Migration\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val namespace = EntityPath(creds.subject.asString)\n  val collectionPath = s\"/${EntityPath.DEFAULT}/${collection.path}\"\n  def aname() = MakeName.next(\"seq_migration_tests\")\n\n  private def seqParameters(seq: Vector[String]) = Parameters(\"_actions\", seq.toJson)\n\n  it should \"list old-style sequence action with explicit namespace\" in {\n    implicit val tid = transid()\n    val components = Vector(\"/_/a\", \"/_/x/b\", \"/n/a\", \"/n/x/c\").map(stringToFullyQualifiedName(_))\n    val actions = (1 to 2).map { i =>\n      WhiskAction(namespace, aname(), sequence(components))\n    }.toList\n    actions foreach { put(entityStore, _) }\n    waitOnView(entityStore, WhiskAction, namespace, 2)\n    Get(s\"/$namespace/${collection.path}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[List[JsObject]]\n      actions.length should be(response.length)\n      response should contain theSameElementsAs actions.map(_.summaryAsJson)\n    }\n  }\n\n  it should \"get old-style sequence action by name in default namespace\" in {\n    implicit val tid = transid()\n    val components = Vector(\"/_/a\", \"/_/x/b\", \"/n/a\", \"/n/x/c\").map(stringToFullyQualifiedName(_))\n    val action = WhiskAction(namespace, aname(), sequence(components))\n    put(entityStore, action)\n    Get(s\"$collectionPath/${action.name}\") ~> Route.seal(routes(creds)) ~> check {\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response should be(action)\n    }\n  }\n\n  // this test is a repeat from ActionsApiTest BUT with old style sequence\n  it should \"preserve new parameters when changing old-style sequence action to non sequence\" in {\n    implicit val tid = transid()\n    val components = Vector(\"/_/a\", \"/_/x/b\", \"/n/a\", \"/n/x/c\")\n    val seqComponents = components.map(stringToFullyQualifiedName(_))\n    val action = WhiskAction(namespace, aname(), sequence(seqComponents), seqParameters(components))\n    val content = WhiskActionPut(Some(jsDefault(\"\")), parameters = Some(Parameters(\"a\", \"A\")))\n    put(entityStore, action, false)\n\n    // create an action sequence\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response.exec.kind should be(NODEJS)\n      response.parameters should be(Parameters(\"a\", \"A\"))\n    }\n  }\n\n  // this test is a repeat from ActionsApiTest BUT with old style sequence\n  it should \"reset parameters when changing old-style sequence action to non sequence\" in {\n    implicit val tid = transid()\n    val components = Vector(\"/_/a\", \"/_/x/b\", \"/n/a\", \"/n/x/c\")\n    val seqComponents = components.map(stringToFullyQualifiedName(_))\n    val action = WhiskAction(namespace, aname(), sequence(seqComponents), seqParameters(components))\n    val content = WhiskActionPut(Some(jsDefault(\"\")))\n    put(entityStore, action, false)\n\n    // create an action sequence\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[WhiskAction]\n      response.exec.kind should be(NODEJS)\n      response.parameters shouldBe Parameters()\n    }\n  }\n\n  it should \"update old-style sequence action with new annotations\" in {\n    implicit val tid = transid()\n    val components = Vector(\"/_/a\", \"/_/x/b\", \"/n/a\", \"/n/x/c\")\n    val seqComponents = components.map(stringToFullyQualifiedName(_))\n    val action = WhiskAction(namespace, aname(), sequence(seqComponents))\n    val content = \"\"\"{\"annotations\":[{\"key\":\"old\",\"value\":\"new\"}]}\"\"\".parseJson.asJsObject\n    put(entityStore, action, false)\n\n    // create an action sequence\n    Put(s\"$collectionPath/${action.name}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(action.docid)\n      status should be(OK)\n      val response = responseAs[String]\n      // contains the action\n      components map { c =>\n        response should include(c)\n      }\n      // contains the annotations\n      response should include(\"old\")\n      response should include(\"new\")\n    }\n  }\n\n  it should \"update an old-style sequence with new sequence\" in {\n    implicit val tid = transid()\n    // old sequence\n    val seqName = EntityName(s\"${aname()}_new\")\n    val oldComponents = Vector(\"/_/a\", \"/_/x/b\", \"/n/a\", \"/n/x/c\").map(stringToFullyQualifiedName(_))\n    val oldSequence = WhiskAction(namespace, seqName, sequence(oldComponents))\n    put(entityStore, oldSequence)\n\n    // new sequence\n    val limit = 5 // count of bogus actions in sequence\n    val bogus = s\"${aname()}_bogus\"\n    val bogusActionName = s\"/_/${bogus}\" // test that default namespace gets properly replaced\n    // put the action in the entity store so it exists\n    val bogusAction = WhiskAction(namespace, EntityName(bogus), jsDefault(\"??\"), Parameters(\"x\", \"y\"))\n    put(entityStore, bogusAction)\n    val seqComponents = for (i <- 1 to limit) yield stringToFullyQualifiedName(bogusActionName)\n    val seqAction = WhiskAction(namespace, seqName, sequence(seqComponents.toVector))\n    val content = WhiskActionPut(Some(seqAction.exec), Some(Parameters()))\n\n    // update an action sequence\n    Put(s\"$collectionPath/${seqName}?overwrite=true\", content) ~> Route.seal(routes(creds)) ~> check {\n      deleteAction(seqAction.docid)\n      status should be(OK)\n\n      val response = responseAs[WhiskAction]\n      response.exec.kind should be(Exec.SEQUENCE)\n      response.limits should be(seqAction.limits)\n      response.publish should be(seqAction.publish)\n      response.version should be(seqAction.version.upPatch)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/ArtifactActivationStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.apache.openwhisk.core.controller.test.WhiskAuthHelpers\nimport org.apache.openwhisk.core.database.test.behavior.ActivationStoreBehaviorBase\nimport org.scalatest.flatspec.AnyFlatSpec\n\ntrait ArtifactActivationStoreBehaviorBase extends AnyFlatSpec with ActivationStoreBehaviorBase {\n  override def storeType = \"Artifact\"\n\n  override val context = UserContext(WhiskAuthHelpers.newIdentity())\n\n  override lazy val activationStore = {\n    ArtifactActivationStoreProvider.instance(actorSystem, logging)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/ArtifactActivationStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.time.Instant\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.test.behavior.ActivationStoreBehavior\nimport org.apache.openwhisk.core.entity.{EntityPath, WhiskActivation}\nimport org.apache.openwhisk.utils.retry\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass ArtifactActivationStoreTests\n    extends AnyFlatSpec\n    with ArtifactActivationStoreBehaviorBase\n    with ActivationStoreBehavior {\n  override def checkQueryActivations(namespace: String,\n                                     name: Option[String] = None,\n                                     skip: Int = 0,\n                                     limit: Int = 1000,\n                                     includeDocs: Boolean = false,\n                                     since: Option[Instant] = None,\n                                     upto: Option[Instant] = None,\n                                     context: UserContext,\n                                     expected: IndexedSeq[WhiskActivation])(implicit transid: TransactionId): Unit = {\n    // This is for compatible with CouchDB as it use option `StaleParameter.UpdateAfter`\n    retry(super.checkQueryActivations(namespace, name, skip, limit, includeDocs, since, upto, context, expected), 100)\n  }\n\n  override def checkCountActivations(namespace: String,\n                                     name: Option[EntityPath] = None,\n                                     skip: Int = 0,\n                                     since: Option[Instant] = None,\n                                     upto: Option[Instant] = None,\n                                     context: UserContext,\n                                     expected: Long)(implicit transid: TransactionId): Unit = {\n    retry(super.checkCountActivations(namespace, name, skip, since, upto, context, expected), 100)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/ArtifactWithFileStorageActivationStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.io.File\nimport java.nio.file.Paths\nimport java.time.Instant\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model.HttpRequest\nimport org.apache.pekko.testkit.TestKit\nimport common.StreamLogging\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.{Logging, TransactionId, WhiskInstants}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.{Await, Future}\nimport scala.concurrent.duration._\nimport scala.io.Source\n\n@RunWith(classOf[JUnitRunner])\nclass ArtifactWithFileStorageActivationStoreTests()\n    extends TestKit(ActorSystem(\"ArtifactWithFileStorageActivationStoreTests\"))\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with ScalaFutures\n    with StreamLogging\n    with WhiskInstants {\n\n  implicit val transid: TransactionId = TransactionId.testing\n  implicit val notifier: Option[CacheChangeNotification] = None\n\n  private val uuid = UUID()\n  private val subject = Subject()\n  private val user =\n    Identity(subject, Namespace(EntityName(subject.asString), uuid), BasicAuthenticationAuthKey(uuid, Secret()))\n  private val context = UserContext(user, HttpRequest())\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  private def await[T](awaitable: Future[T], timeout: FiniteDuration = 10.seconds) = Await.result(awaitable, timeout)\n\n  def responsePermutations = {\n    val message = JsObject(\"result key\" -> JsString(\"result value\"))\n    Seq(\n      ActivationResponse.success(None),\n      ActivationResponse.success(Some(message)),\n      ActivationResponse.applicationError(message),\n      ActivationResponse.whiskError(message))\n  }\n\n  def logPermutations = {\n    Seq(\n      ActivationLogs(),\n      ActivationLogs(Vector(\"2018-03-05T02:10:38.196689520Z stdout: single log line\")),\n      ActivationLogs(\n        Vector(\n          \"2018-03-05T02:10:38.196689522Z stdout: first log line of multiple lines\",\n          \"2018-03-05T02:10:38.196754258Z stdout: second log line of multiple lines\")))\n  }\n\n  def expectedFileContent(activation: WhiskActivation,\n                          includeResult: Boolean,\n                          additionalFieldsForLogs: Seq[JsField] = Seq(),\n                          additionalFieldsForActivation: Seq[JsField] = Seq()) = {\n    val expectedLogs = activation.logs.logs.map { log =>\n      {\n        JsObject(\n          Seq(\n            \"type\" -> \"user_log\".toJson,\n            \"message\" -> log.toJson,\n            \"activationId\" -> activation.activationId.toJson,\n            \"namespace\" -> activation.namespace.asString.toJson,\n            \"namespaceId\" -> user.namespace.uuid.toJson)\n            ++ additionalFieldsForLogs: _*)\n      }\n    }\n    val expectedResult = if (includeResult) {\n      JsString(activation.response.result.getOrElse(JsNull).compactPrint)\n    } else {\n      JsString(s\"Activation record '${activation.activationId}' for entity '${activation.name}'\")\n    }\n    val expectedActivation = JsObject(\n      Seq(\n        \"type\" -> \"activation_record\".toJson,\n        \"duration\" -> activation.duration.toJson,\n        \"name\" -> activation.name.toJson,\n        \"subject\" -> activation.subject.toJson,\n        \"waitTime\" -> activation.annotations.get(\"waitTime\").toJson.toJson,\n        \"activationId\" -> activation.activationId.toJson,\n        \"namespaceId\" -> user.namespace.uuid.toJson,\n        \"publish\" -> activation.publish.toJson,\n        \"version\" -> activation.version.toJson,\n        \"response\" -> activation.response.withoutResult.toExtendedJson,\n        \"end\" -> activation.end.toEpochMilli.toJson,\n        \"message\" -> expectedResult,\n        \"kind\" -> activation.annotations.get(\"kind\").toJson.toJson,\n        \"start\" -> activation.start.toEpochMilli.toJson,\n        \"limits\" -> activation.annotations.get(\"limits\").toJson.toJson,\n        \"initTime\" -> activation.annotations.get(\"initTime\").toJson,\n        \"namespace\" -> activation.namespace.toJson)\n        ++ additionalFieldsForActivation: _*)\n\n    expectedLogs ++ Seq(expectedActivation)\n  }\n\n  it should \"store activations in artifact store and to file without result\" in {\n    val config = ArtifactWithFileStorageActivationStoreConfig(\"userlogs\", \"logs\", \"namespaceId\", false)\n    val activationStore = new ArtifactWithFileStorageActivationStore(system, logging, config)\n    val logDir = new File(new File(\".\").getCanonicalPath, config.logPath)\n\n    try {\n      logDir.mkdir\n\n      val activations = responsePermutations.map { response =>\n        logPermutations.map { logs =>\n          val activation = WhiskActivation(\n            namespace = EntityPath(subject.asString),\n            name = EntityName(\"name\"),\n            subject = subject,\n            activationId = ActivationId.generate(),\n            start = Instant.now.inMills,\n            end = Instant.now.inMills,\n            response = response,\n            logs = logs,\n            duration = Some(101L),\n            annotations = Parameters(\"kind\", \"nodejs:20\") ++ Parameters(\n              \"limits\",\n              ActionLimits(TimeLimit(60.second), MemoryLimit(256.MB), LogLimit(10.MB)).toJson) ++\n              Parameters(\"waitTime\", 16.toJson) ++\n              Parameters(\"initTime\", 44.toJson))\n          val docInfo = await(activationStore.store(activation, context))\n          val fullyQualifiedActivationId = ActivationId(docInfo.id.asString)\n\n          await(activationStore.get(fullyQualifiedActivationId, context)) shouldBe activation\n          await(activationStore.delete(fullyQualifiedActivationId, context))\n          activation\n        }\n      }.flatten\n\n      Source\n        .fromFile(activationStore.getLogFile.toFile.getAbsoluteFile)\n        .getLines\n        .toList\n        .map(_.parseJson)\n        .toJson\n        .convertTo[JsArray] shouldBe activations\n        .map(a => expectedFileContent(a, false))\n        .flatten\n        .toJson\n        .convertTo[JsArray]\n    } finally {\n      activationStore.getLogFile.toFile.getAbsoluteFile.delete\n      logDir.delete\n    }\n  }\n\n  it should \"store activations in artifact store and to file with result\" in {\n    val config = ArtifactWithFileStorageActivationStoreConfig(\"userlogs\", \"logs\", \"namespaceId\", true)\n    val activationStore = new ArtifactWithFileStorageActivationStore(system, logging, config)\n    val logDir = new File(new File(\".\").getCanonicalPath, config.logPath)\n\n    try {\n      logDir.mkdir\n\n      val activations = responsePermutations.map { response =>\n        logPermutations.map { logs =>\n          val activation = WhiskActivation(\n            namespace = EntityPath(subject.asString),\n            name = EntityName(\"name\"),\n            subject = subject,\n            activationId = ActivationId.generate(),\n            start = Instant.now.inMills,\n            end = Instant.now.inMills,\n            response = response,\n            logs = logs,\n            duration = Some(101L),\n            annotations = Parameters(\"kind\", \"nodejs:20\") ++ Parameters(\n              \"limits\",\n              ActionLimits(TimeLimit(60.second), MemoryLimit(256.MB), LogLimit(10.MB)).toJson) ++\n              Parameters(\"waitTime\", 16.toJson) ++\n              Parameters(\"initTime\", 44.toJson))\n          val docInfo = await(activationStore.store(activation, context))\n          val fullyQualifiedActivationId = ActivationId(docInfo.id.asString)\n\n          await(activationStore.get(fullyQualifiedActivationId, context)) shouldBe activation\n          await(activationStore.delete(fullyQualifiedActivationId, context))\n          activation\n        }\n      }.flatten\n\n      Source\n        .fromFile(activationStore.getLogFile.toFile.getAbsoluteFile)\n        .getLines\n        .toList\n        .map(_.parseJson)\n        .toJson\n        .convertTo[JsArray] shouldBe activations\n        .map(a => expectedFileContent(a, true))\n        .flatten\n        .toJson\n        .convertTo[JsArray]\n    } finally {\n      activationStore.getLogFile.toFile.getAbsoluteFile.delete\n      logDir.delete\n    }\n  }\n\n  for (includeResult <- Seq(false, true)) {\n\n    it should \"test activationToFileExtended: store activations in artifact store and in file \" +\n      (if (includeResult) \"with\" else \"without\") + \" result\" in {\n\n      // used in ArtifactWithFileStorageActivationStoreExtended and for test data\n      val additionalFieldsForLogs = Map(\"field1\" -> JsString(\"value1\"))\n      val additionalFieldsForActivation = Map(\"field2\" -> JsString(\"value2\"))\n\n      // START - example of a simple ArtifactActivationStore implementation that uses activationToFileExtended\n\n      case class ArtifactWithFileStorageActivationStoreConfigExtendedTest(logFilePrefix: String,\n                                                                          logPath: String,\n                                                                          userIdField: String,\n                                                                          writeResultToFile: Boolean)\n\n      class ArtifactWithFileStorageActivationStoreExtendedTest(\n        actorSystem: ActorSystem,\n        logging: Logging,\n        config: ArtifactWithFileStorageActivationStoreConfigExtendedTest =\n          loadConfigOrThrow[ArtifactWithFileStorageActivationStoreConfigExtendedTest](\n            ConfigKeys.activationStoreWithFileStorage))\n          extends ArtifactActivationStore(actorSystem, logging) {\n\n        private val activationFileStorage =\n          new ActivationFileStorage(\n            config.logFilePrefix,\n            Paths.get(config.logPath),\n            config.writeResultToFile,\n            actorSystem,\n            logging)\n\n        def getLogFile = activationFileStorage.getLogFile\n\n        def shallResultBeIncluded: Boolean = includeResult\n        // other simple example for the flag: (includeResult && !activation.response.isSuccess)\n\n        override def store(activation: WhiskActivation, context: UserContext)(\n          implicit\n          transid: TransactionId,\n          notifier: Option[CacheChangeNotification]): Future[DocInfo] = {\n\n          val additionalFields = Map(config.userIdField -> context.user.namespace.uuid.toJson)\n\n          activationFileStorage.activationToFileExtended(\n            activation,\n            context,\n            additionalFields ++ additionalFieldsForLogs ++ Map(\"namespace\" -> JsString(subject.asString)),\n            additionalFields ++ additionalFieldsForActivation,\n            shallResultBeIncluded)\n\n          super.store(activation, context)\n        }\n\n      }\n\n      // END - example of a simple ArtifactActivationStore implementation that uses activationToFileExtended\n\n      // writeResultToFile is defined with the inverted value of includeResult and should be overridden by the test\n      val config =\n        ArtifactWithFileStorageActivationStoreConfigExtendedTest(\"userlogs\", \"logs\", \"namespaceId\", !includeResult)\n\n      val activationStore =\n        new ArtifactWithFileStorageActivationStoreExtendedTest(system, logging, config)\n      val logDir = new File(new File(\".\").getCanonicalPath, config.logPath)\n\n      try {\n        logDir.mkdir\n\n        val activations = responsePermutations.flatMap { response =>\n          logPermutations.map { logs =>\n            val activation = WhiskActivation(\n              namespace = EntityPath(subject.asString),\n              name = EntityName(\"name\"),\n              subject = subject,\n              activationId = ActivationId.generate(),\n              start = Instant.now.inMills,\n              end = Instant.now.inMills,\n              response = response,\n              logs = logs,\n              duration = Some(101L),\n              annotations = Parameters(\"kind\", \"nodejs:20\") ++ Parameters(\n                \"limits\",\n                ActionLimits(TimeLimit(60.second), MemoryLimit(256.MB), LogLimit(10.MB)).toJson) ++\n                Parameters(\"waitTime\", 16.toJson) ++\n                Parameters(\"initTime\", 44.toJson))\n            val docInfo = await(activationStore.store(activation, context))\n            val fullyQualifiedActivationId = ActivationId(docInfo.id.asString)\n\n            await(activationStore.get(fullyQualifiedActivationId, context)) shouldBe activation\n            await(activationStore.delete(fullyQualifiedActivationId, context))\n            activation\n          }\n        }\n\n        Source\n          .fromFile(activationStore.getLogFile.toFile.getAbsoluteFile)\n          .getLines\n          .toList\n          .map(_.parseJson)\n          .toJson\n          .convertTo[JsArray] shouldBe activations\n          .flatMap(a =>\n            expectedFileContent(a, includeResult, additionalFieldsForLogs.toSeq, additionalFieldsForActivation.toSeq))\n          .toJson\n          .convertTo[JsArray]\n      } finally {\n        activationStore.getLogFile.toFile.getAbsoluteFile.delete\n        logDir.delete\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/CouchDBArtifactStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreBehavior\n\n@RunWith(classOf[JUnitRunner])\nclass CouchDBArtifactStoreTests extends AnyFlatSpec with CouchDBStoreBehaviorBase with ArtifactStoreBehavior {}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/CouchDBAttachmentStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.database.memory.MemoryAttachmentStoreProvider\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreAttachmentBehaviors\n\nimport scala.reflect.ClassTag\n\n@RunWith(classOf[JUnitRunner])\nclass CouchDBAttachmentStoreTests\n    extends AnyFlatSpec\n    with CouchDBStoreBehaviorBase\n    with ArtifactStoreAttachmentBehaviors {\n  override protected def getAttachmentStore[D <: DocumentSerializer: ClassTag]() =\n    Some(MemoryAttachmentStoreProvider.makeStore[D]())\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/CouchDBStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreBehaviorBase\nimport org.apache.openwhisk.core.entity.{\n  DocumentReader,\n  WhiskActivation,\n  WhiskAuth,\n  WhiskDocumentReader,\n  WhiskEntity,\n  WhiskEntityJsonFormat\n}\n\nimport scala.reflect.{classTag, ClassTag}\n\ntrait CouchDBStoreBehaviorBase extends AnyFlatSpec with ArtifactStoreBehaviorBase {\n  override def storeType = \"CouchDB\"\n\n  override val authStore = {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    CouchDbStoreProvider.makeArtifactStore[WhiskAuth](useBatching = false, getAttachmentStore[WhiskAuth]())\n  }\n\n  override val entityStore =\n    CouchDbStoreProvider.makeArtifactStore[WhiskEntity](useBatching = false, getAttachmentStore[WhiskEntity]())(\n      classTag[WhiskEntity],\n      WhiskEntityJsonFormat,\n      WhiskDocumentReader,\n      actorSystem,\n      logging)\n\n  override val activationStore = {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    CouchDbStoreProvider.makeArtifactStore[WhiskActivation](useBatching = true, getAttachmentStore[WhiskActivation]())\n  }\n\n  override protected def getAttachmentStore(store: ArtifactStore[_]) =\n    store.asInstanceOf[CouchDbRestStore[_]].attachmentStore\n\n  protected def getAttachmentStore[D <: DocumentSerializer: ClassTag](): Option[AttachmentStore] = None\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/LimitsCommandTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.cli.CommandMessages\nimport org.apache.openwhisk.core.database.LimitsCommand.LimitEntity\nimport org.apache.openwhisk.core.entity.{DocInfo, EntityName, UserLimits}\n\nimport scala.collection.mutable.ListBuffer\nimport scala.concurrent.duration.Duration\nimport scala.util.Try\n\n@RunWith(classOf[JUnitRunner])\nclass LimitsCommandTests extends AnyFlatSpec with WhiskAdminCliTestBase {\n  private val limitsToDelete = ListBuffer[String]()\n\n  protected val limitsStore = LimitsCommand.createDataStore()\n\n  behavior of \"limits\"\n\n  it should \"set limits for non existing namespace\" in {\n    implicit val tid = transid()\n    val ns = newNamespace()\n    resultOk(\n      \"limits\",\n      \"set\",\n      \"--invocationsPerMinute\",\n      \"3\",\n      \"--firesPerMinute\",\n      \"7\",\n      \"--concurrentInvocations\",\n      \"11\",\n      \"--allowedKinds\",\n      \"nodejs:20\",\n      \"blackbox\",\n      \"--storeActivations\",\n      \"false\",\n      ns) shouldBe CommandMessages.limitsSuccessfullySet(ns)\n\n    val limits = limitsStore.get[LimitEntity](DocInfo(LimitsCommand.limitIdOf(EntityName(ns)))).futureValue\n    limits.limits shouldBe UserLimits(\n      invocationsPerMinute = Some(3),\n      firesPerMinute = Some(7),\n      concurrentInvocations = Some(11),\n      allowedKinds = Some(Set(\"nodejs:20\", \"blackbox\")),\n      storeActivations = Some(false))\n\n    resultOk(\"limits\", \"set\", \"--invocationsPerMinute\", \"13\", ns) shouldBe CommandMessages.limitsSuccessfullyUpdated(ns)\n\n    val limits2 = limitsStore.get[LimitEntity](DocInfo(LimitsCommand.limitIdOf(EntityName(ns)))).futureValue\n    limits2.limits shouldBe UserLimits(Some(13), None, None)\n  }\n\n  it should \"set and get limits\" in {\n    val ns = newNamespace()\n    resultOk(\"limits\", \"set\", \"--invocationsPerMinute\", \"13\", ns)\n    resultOk(\"limits\", \"get\", ns) shouldBe \"invocationsPerMinute = 13\"\n  }\n\n  it should \"respond with default system limits apply for non existing namespace\" in {\n    resultOk(\"limits\", \"get\", \"non-existing-ns\") shouldBe CommandMessages.defaultLimits\n  }\n\n  it should \"delete an existing limit\" in {\n    val ns = newNamespace()\n    resultOk(\"limits\", \"set\", \"--invocationsPerMinute\", \"13\", ns)\n    resultOk(\"limits\", \"get\", ns) shouldBe \"invocationsPerMinute = 13\"\n\n    //Delete\n    resultOk(\"limits\", \"delete\", ns) shouldBe CommandMessages.limitsDeleted\n\n    //Read after delete should result in default message\n    resultOk(\"limits\", \"get\", ns) shouldBe CommandMessages.defaultLimits\n\n    //Delete of deleted namespace should result in error\n    resultNotOk(\"limits\", \"delete\", ns) shouldBe CommandMessages.limitsNotFound(ns)\n  }\n\n  it should \"update existing allowedKind limit\" in {\n    val ns = newNamespace()\n    resultOk(\"limits\", \"set\", \"--allowedKinds\", \"nodejs:20\", ns)\n    resultOk(\"limits\", \"get\", ns) shouldBe \"allowedKinds = nodejs:20\"\n    resultOk(\"limits\", \"set\", \"--allowedKinds\", \"nodejs:20\", \"blackbox\", \"python\", ns)\n    resultOk(\"limits\", \"get\", ns) shouldBe \"allowedKinds = nodejs:20, blackbox, python\"\n\n    //Delete\n    resultOk(\"limits\", \"delete\", ns) shouldBe CommandMessages.limitsDeleted\n\n    //Read after delete should result in default message\n    resultOk(\"limits\", \"get\", ns) shouldBe CommandMessages.defaultLimits\n\n    //Delete of deleted namespace should result in error\n    resultNotOk(\"limits\", \"delete\", ns) shouldBe CommandMessages.limitsNotFound(ns)\n  }\n\n  override def cleanup()(implicit timeout: Duration): Unit = {\n    implicit val tid = TransactionId.testing\n    limitsToDelete.map { u =>\n      Try {\n        val limit = limitsStore.get[LimitEntity](DocInfo(LimitsCommand.limitIdOf(EntityName(u)))).futureValue\n        delete(limitsStore, limit.docinfo)\n      }\n    }\n    limitsToDelete.clear()\n    super.cleanup()\n  }\n\n  private def newNamespace(): String = {\n    val ns = randomString()\n    limitsToDelete += ns\n    ns\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/UserCommandTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.cli.{CommandMessages, Conf, WhiskAdmin}\nimport org.apache.openwhisk.core.entity._\n\nimport scala.collection.mutable.ListBuffer\nimport scala.concurrent.duration.Duration\nimport scala.util.Try\nimport org.apache.openwhisk.core.database.UserCommand.ExtendedAuth\n\n@RunWith(classOf[JUnitRunner])\nclass UserCommandTests extends AnyFlatSpec with WhiskAdminCliTestBase {\n  private val usersToDelete = ListBuffer[String]()\n\n  behavior of \"create user\"\n\n  it should \"fail for subject less than length 5\" in {\n    the[Exception] thrownBy {\n      new Conf(Seq(\"user\", \"create\", \"foo\"))\n    } should have message CommandMessages.shortName\n  }\n\n  it should \"fail for short key\" in {\n    the[Exception] thrownBy {\n      new Conf(Seq(\"user\", \"create\", \"--auth\", \"uid:shortKey\", \"foobar\"))\n    } should have message CommandMessages.shortKey\n  }\n\n  it should \"fail for invalid uuid\" in {\n    val key = \"x\" * 64\n    the[Exception] thrownBy {\n      new Conf(Seq(\"user\", \"create\", \"--auth\", s\"uid:$key\", \"foobar\"))\n    } should have message CommandMessages.invalidUUID\n  }\n\n  it should \"create a user\" in {\n    val subject = newSubject()\n    val key = BasicAuthenticationAuthKey()\n    val conf = new Conf(Seq(\"user\", \"create\", \"--auth\", key.compact, subject))\n    val admin = WhiskAdmin(conf)\n    admin.executeCommand().futureValue.right.get shouldBe key.compact\n  }\n\n  it should \"create a user with default key\" in {\n    val subject = newSubject()\n    val generatedKey = resultOk(\"user\", \"create\", subject)\n    resultOk(\"user\", \"get\", subject) shouldBe generatedKey\n  }\n\n  it should \"force update an existing user\" in {\n    val subject = newSubject()\n    val oldKey = resultOk(\"user\", \"create\", \"--force\", subject)\n    resultOk(\"user\", \"get\", subject) shouldBe oldKey\n\n    // Force update with provided auth uuid:key\n    val key = BasicAuthenticationAuthKey()\n    val newKey = resultOk(\"user\", \"create\", \"--auth\", key.compact, \"--force\", subject)\n    resultOk(\"user\", \"get\", subject) shouldBe newKey\n    newKey shouldBe key.compact\n\n    // Force update without auth, uuid:key is randomly generated\n    val generatedKey = resultOk(\"user\", \"create\", \"--force\", subject)\n    generatedKey should not be newKey\n    generatedKey should not be oldKey\n  }\n\n  it should \"create a user or update an existing user with revoke flag\" in {\n    val subject = newSubject()\n    val oldKey = resultOk(\"user\", \"create\", \"--revoke\", subject)\n    resultOk(\"user\", \"get\", subject) shouldBe oldKey\n    val newKey = resultOk(\"user\", \"create\", \"--revoke\", subject)\n    resultOk(\"user\", \"get\", subject) shouldBe newKey\n    val oldAuthKey = BasicAuthenticationAuthKey(oldKey)\n    val newAuthKey = BasicAuthenticationAuthKey(newKey)\n    newAuthKey.uuid shouldBe oldAuthKey.uuid\n    newAuthKey.key should not be oldAuthKey.key\n  }\n\n  it should \"add namespace to existing user\" in {\n    val subject = newSubject()\n    val key = BasicAuthenticationAuthKey()\n\n    //Create user\n    WhiskAdmin(new Conf(Seq(\"user\", \"create\", \"--auth\", key.compact, subject))).executeCommand().futureValue\n\n    //Add new namespace\n    val key2 = BasicAuthenticationAuthKey()\n    resultOk(\"user\", \"create\", \"--auth\", key2.compact, \"--namespace\", \"foo\", subject) shouldBe key2.compact\n\n    //Adding same namespace should fail\n    resultNotOk(\"user\", \"create\", \"--auth\", key2.compact, \"--namespace\", \"foo\", subject) shouldBe CommandMessages.namespaceExists\n\n    //Adding same namespace with force flag should update the namespace with specified uuid:key\n    val newKey = resultOk(\"user\", \"create\", \"--force\", \"--auth\", key2.compact, \"--namespace\", \"foo\", subject)\n    newKey shouldBe key2.compact\n\n    //Adding same namespace with force flag without auth should regenerate random uuid:key\n    val generatedKey = resultOk(\"user\", \"create\", \"--force\", \"--namespace\", \"foo\", subject)\n    generatedKey should not be key2.compact\n    generatedKey should not be key.compact\n\n    //It should be possible to lookup by new namespace\n    implicit val tid = transid()\n    val i = Identity.get(authStore, EntityName(\"foo\")).futureValue\n    i.subject.asString shouldBe subject\n    resultOk(\"user\", \"get\", \"--namespace\", \"foo\", subject) shouldBe generatedKey\n  }\n\n  it should \"not add namespace to a blocked user\" in {\n    val subject = newSubject()\n    val ns = randomString()\n    val blockedAuth =\n      new ExtendedAuth(Subject(subject), Set(newNS(EntityName(ns), BasicAuthenticationAuthKey())), Some(true))\n    val authStore2 = UserCommand.createDataStore()\n\n    implicit val tid = transid()\n    authStore2.put(blockedAuth).futureValue\n\n    resultNotOk(\"user\", \"create\", \"--namespace\", \"foo\", subject) shouldBe CommandMessages.subjectBlocked\n\n    authStore2.shutdown()\n  }\n\n  behavior of \"delete user\"\n\n  it should \"fail deleting non existing user\" in {\n    resultNotOk(\"user\", \"delete\", \"non-existing-user\") shouldBe CommandMessages.subjectMissing\n  }\n\n  it should \"delete existing user\" in {\n    val subject = newSubject()\n    val key = BasicAuthenticationAuthKey()\n\n    //Create user\n    WhiskAdmin(new Conf(Seq(\"user\", \"create\", \"--auth\", key.compact, subject))).executeCommand().futureValue\n\n    resultOk(\"user\", \"delete\", subject) shouldBe CommandMessages.subjectDeleted\n  }\n\n  it should \"remove namespace from existing user\" in {\n    implicit val tid = transid()\n    val subject = newSubject()\n\n    val ns1 = newNS()\n    val ns2 = newNS()\n\n    val auth = WhiskAuth(Subject(subject), Set(ns1, ns2))\n\n    put(authStore, auth)\n\n    resultOk(\"user\", \"delete\", \"--namespace\", ns1.namespace.name.asString, subject) shouldBe CommandMessages.namespaceDeleted\n\n    val authFromDB = authStore.get[WhiskAuth](DocInfo(DocId(subject))).futureValue\n    authFromDB.namespaces shouldBe Set(ns2)\n  }\n\n  it should \"not remove missing namespace\" in {\n    implicit val tid = transid()\n    val subject = newSubject()\n    val auth = WhiskAuth(Subject(subject), Set(newNS(), newNS()))\n\n    put(authStore, auth)\n    resultNotOk(\"user\", \"delete\", \"--namespace\", \"non-existing-ns\", subject) shouldBe\n      CommandMessages.namespaceMissing(\"non-existing-ns\", subject)\n  }\n\n  behavior of \"get key\"\n\n  it should \"not get key for missing subject\" in {\n    resultNotOk(\"user\", \"get\", \"non-existing-user\") shouldBe CommandMessages.subjectMissing\n  }\n\n  it should \"get key for existing user\" in {\n    implicit val tid = transid()\n    val subject = newSubject()\n\n    val ns1 = newNS()\n    val ns2 = newNS()\n    val ns3 = newNS(EntityName(subject), BasicAuthenticationAuthKey())\n\n    val auth = WhiskAuth(Subject(subject), Set(ns1, ns2, ns3))\n    put(authStore, auth)\n\n    resultOk(\"user\", \"get\", \"--namespace\", ns1.namespace.name.asString, subject) shouldBe ns1.authkey.compact\n\n    val all = resultOk(\"user\", \"get\", \"--all\", subject)\n\n    all should include(ns1.authkey.compact)\n    all should include(ns2.authkey.compact)\n    all should include(ns3.authkey.compact)\n\n    //Is --namespace is not there look by subject\n    resultOk(\"user\", \"get\", subject) shouldBe ns3.authkey.compact\n\n    //Look for namespace which does not exist\n    resultNotOk(\"user\", \"get\", \"--namespace\", \"non-existing-ns\", subject) shouldBe\n      CommandMessages.namespaceMissing(\"non-existing-ns\", subject)\n  }\n\n  behavior of \"whois\"\n\n  it should \"not get subject for missing subject\" in {\n    resultNotOk(\"user\", \"whois\", BasicAuthenticationAuthKey().compact) shouldBe CommandMessages.subjectMissing\n  }\n\n  it should \"get key for existing user\" in {\n    implicit val tid = transid()\n    val subject = newSubject()\n\n    val ns1 = newNS()\n    val ns3 = newNS(EntityName(subject), BasicAuthenticationAuthKey())\n\n    val auth = WhiskAuth(Subject(subject), Set(ns1, ns3))\n    put(authStore, auth)\n\n    val result = resultOk(\"user\", \"whois\", ns1.authkey.compact)\n\n    result should include(subject)\n    result should include(ns1.namespace.name.asString)\n  }\n\n  behavior of \"list\"\n\n  it should \"list keys associated with given namespace\" in {\n    implicit val tid = transid()\n    def newWhiskAuth(ns: String*) =\n      WhiskAuth(Subject(newSubject()), ns.map(n => newNS(EntityName(n), BasicAuthenticationAuthKey())).toSet)\n\n    def key(a: WhiskAuth, ns: String) = a.namespaces.find(_.namespace.name.asString == ns).map(_.authkey).get\n\n    val ns1 = randomString()\n    val ns2 = randomString()\n    val ns3 = randomString()\n\n    val a1 = newWhiskAuth(ns1)\n    val a2 = newWhiskAuth(ns1, ns2)\n    val a3 = newWhiskAuth(ns1, ns2, ns3)\n\n    Seq(a1, a2, a3).foreach(put(authStore, _))\n\n    Seq(a1, a2, a3).foreach(a => waitOnView(authStore, key(a, ns1), 1))\n\n    //Check negative case\n    resultNotOk(\"user\", \"list\", \"non-existing-ns\") shouldBe CommandMessages.namespaceMissing(\"non-existing-ns\")\n\n    //Check all results\n    val r1 = resultOk(\"user\", \"list\", ns1)\n    r1.split(\"\\n\").length shouldBe 3\n    r1 should include(a1.subject.asString)\n    r1 should include(key(a2, ns1).compact)\n\n    //Check limit\n    val r2 = resultOk(\"user\", \"list\", \"-p\", \"2\", ns1)\n    r2.split(\"\\n\").length shouldBe 2\n\n    //Check key only\n    val r3 = resultOk(\"user\", \"list\", \"-k\", ns1)\n    r3 should include(key(a2, ns1).compact)\n    r3 should not include a2.subject.asString\n  }\n\n  behavior of \"block\"\n\n  it should \"block subjects\" in {\n    implicit val tid = transid()\n    val a1 = WhiskAuth(Subject(newSubject()), Set(newNS()))\n    val a2 = WhiskAuth(Subject(newSubject()), Set(newNS()))\n    val a3 = WhiskAuth(Subject(newSubject()), Set(newNS()))\n\n    Seq(a1, a2, a3).foreach(put(authStore, _))\n\n    val r1 = resultOk(\"user\", \"block\", a1.subject.asString, a2.subject.asString)\n\n    val authStore2 = UserCommand.createDataStore()\n\n    authStore2.get[ExtendedAuth](a1.docinfo).futureValue.isBlocked shouldBe true\n    authStore2.get[ExtendedAuth](a2.docinfo).futureValue.isBlocked shouldBe true\n    authStore2.get[ExtendedAuth](a3.docinfo).futureValue.isBlocked shouldBe false\n\n    val r2 = resultOk(\"user\", \"unblock\", a2.subject.asString, a3.subject.asString)\n\n    authStore2.get[ExtendedAuth](a1.docinfo).futureValue.isBlocked shouldBe true\n    authStore2.get[ExtendedAuth](a2.docinfo).futureValue.isBlocked shouldBe false\n    authStore2.get[ExtendedAuth](a3.docinfo).futureValue.isBlocked shouldBe false\n\n    authStore2.shutdown()\n  }\n\n  override def cleanup()(implicit timeout: Duration): Unit = {\n    implicit val tid = TransactionId.testing\n    usersToDelete.map { u =>\n      Try {\n        val auth = authStore.get[WhiskAuth](DocInfo(u)).futureValue\n        delete(authStore, auth.docinfo)\n      }\n    }\n    usersToDelete.clear()\n    super.cleanup()\n  }\n\n  private def newNS(): WhiskNamespace = newNS(EntityName(randomString()), BasicAuthenticationAuthKey())\n\n  private def newNS(name: EntityName, authKey: BasicAuthenticationAuthKey) =\n    WhiskNamespace(Namespace(name, authKey.uuid), authKey)\n\n  private def newSubject(): String = {\n    val subject = randomString()\n    usersToDelete += subject\n    subject\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/WhiskAdminCliTestBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport common.{StreamLogging, WskActorSystem}\nimport org.rogach.scallop.throwError\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.core.cli.{Conf, WhiskAdmin}\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entity.WhiskAuthStore\n\nimport scala.util.Random\n\ntrait WhiskAdminCliTestBase\n    extends AnyFlatSpec\n    with WskActorSystem\n    with DbUtils\n    with StreamLogging\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with ScalaFutures\n    with Matchers {\n\n  //Bring in sync the timeout used by ScalaFutures and DBUtils\n  implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = dbOpTimeout)\n  protected val authStore = WhiskAuthStore.datastore()\n\n  //Ensure scalaop does not exit upon validation failure\n  throwError.value = true\n\n  override def afterEach(): Unit = {\n    cleanup()\n  }\n\n  override def afterAll(): Unit = {\n    println(\"Shutting down store connections\")\n    authStore.shutdown()\n    super.afterAll()\n  }\n\n  protected def randomString(len: Int = 5): String = Random.alphanumeric.take(len).mkString\n\n  protected def resultOk(args: String*): String =\n    WhiskAdmin(new Conf(args.toSeq))\n      .executeCommand()\n      .futureValue\n      .right\n      .get\n\n  protected def resultNotOk(args: String*): String =\n    WhiskAdmin(new Conf(args.toSeq))\n      .executeCommand()\n      .futureValue\n      .left\n      .get\n      .message\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/azblob/AzureBlob.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.azblob\n\nimport org.apache.pekko.actor.ActorSystem\nimport com.typesafe.config.ConfigFactory\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.database.{AttachmentStore, DocumentSerializer}\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.reflect.ClassTag\n\ntrait AzureBlob extends AnyFlatSpec {\n  def azureCdnConfig: String = \"\"\n\n  def makeAzureStore[D <: DocumentSerializer: ClassTag]()(implicit actorSystem: ActorSystem,\n                                                          logging: Logging): AttachmentStore = {\n    val config = ConfigFactory.parseString(s\"\"\"\n        |whisk {\n        |  azure-blob {\n        |    endpoint = \"$endpoint\"\n        |    account-name = \"$accountName\"\n        |    container-name = \"$containerName\"\n        |    account-key = \"$accountKey\"\n        |    prefix = $prefix\n        |    $azureCdnConfig\n        |  }\n        |}\"\"\".stripMargin).withFallback(ConfigFactory.load()).resolve()\n    AzureBlobAttachmentStoreProvider.makeStore[D](config)\n  }\n\n  override protected def withFixture(test: NoArgTest) = {\n    assume(\n      accountKey != null,\n      \"'AZ_ACCOUNT_KEY' env not configured. Configure following \" +\n        \"env variables for test to run. 'AZ_ENDPOINT', 'AZ_ACCOUNT_NAME', 'AZ_CONTAINER_NAME'\")\n    super.withFixture(test)\n  }\n\n  val endpoint = System.getenv(\"AZ_ENDPOINT\")\n  val accountName = System.getenv(\"AZ_ACCOUNT_NAME\")\n  val containerName = sys.env.getOrElse(\"AZ_CONTAINER_NAME\", \"test-ow-travis\")\n  val accountKey = System.getenv(\"AZ_ACCOUNT_KEY\")\n\n  def prefix: String\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/azblob/AzureBlobAttachmentStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.azblob\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.database.{AttachmentStore, DocumentSerializer}\nimport org.apache.openwhisk.core.database.memory.{MemoryArtifactStoreBehaviorBase, MemoryArtifactStoreProvider}\nimport org.apache.openwhisk.core.database.test.AttachmentStoreBehaviors\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreAttachmentBehaviors\nimport org.apache.openwhisk.core.entity.WhiskEntity\nimport org.scalatest.flatspec.AnyFlatSpec\n\nimport scala.reflect.ClassTag\nimport scala.util.Random\n\ntrait AzureBlobAttachmentStoreBehaviorBase\n    extends AnyFlatSpec\n    with MemoryArtifactStoreBehaviorBase\n    with ArtifactStoreAttachmentBehaviors\n    with AttachmentStoreBehaviors {\n  override lazy val store = makeAzureStore[WhiskEntity]\n\n  override val prefix = s\"attachmentTCK_${Random.alphanumeric.take(4).mkString}\"\n\n  override protected def beforeAll(): Unit = {\n    MemoryArtifactStoreProvider.purgeAll()\n    super.beforeAll()\n  }\n\n  override def getAttachmentStore[D <: DocumentSerializer: ClassTag](): AttachmentStore =\n    makeAzureStore[D]()\n\n  def makeAzureStore[D <: DocumentSerializer: ClassTag]()(implicit actorSystem: ActorSystem,\n                                                          logging: Logging): AttachmentStore\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/azblob/AzureBlobAttachmentStoreCDNTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.azblob\n\nimport org.apache.openwhisk.core.entity.WhiskEntity\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass AzureBlobAttachmentStoreCDNTests extends AzureBlobAttachmentStoreBehaviorBase with AzureBlob {\n  override lazy val store = makeAzureStore[WhiskEntity]\n\n  override def storeType: String = \"AzureBlob_AzureCDN\"\n  override def azureCdnConfig: String =\n    \"\"\"\n      |azure-cdn-config {\n      |  domain-name = ${AZ_CDN_DOMAIN}\n      |}\n    \"\"\".stripMargin\n\n  override protected def withFixture(test: NoArgTest) = {\n    assume(\n      System.getenv(\"AZ_CDN_DOMAIN\") != null,\n      \"Configure following env variables for test \" +\n        \"to run 'AZ_CDN_DOMAIN' \")\n    super.withFixture(test)\n  }\n\n  //With AzureCDN deletes are not immediate and instead the objects may live in CDN cache until TTL\n  override protected val lazyDeletes = true\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/azblob/AzureBlobAttachmentStoreITTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.azblob\n\nimport org.apache.openwhisk.core.entity.WhiskEntity\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass AzureBlobAttachmentStoreITTests extends AzureBlobAttachmentStoreBehaviorBase with AzureBlob {\n  override lazy val store = makeAzureStore[WhiskEntity]\n\n  override def storeType: String = \"Azure\"\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/azblob/AzureBlobConfigTest.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.azblob\n\nimport com.azure.storage.common.policy.RetryPolicyType\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport pureconfig._\nimport pureconfig.generic.auto._\n\n@RunWith(classOf[JUnitRunner])\nclass AzureBlobConfigTest extends AnyFlatSpec with Matchers {\n\n  behavior of \"AzureBlobConfig\"\n  it should \"use valid defaults for retry option\" in {\n    val config: AzBlobRetryConfig = loadConfigOrThrow[AzBlobRetryConfig](ConfigKeys.azBlob + \".retry-config\")\n    //make sure retry type is set to FIXED\n    config.retryPolicyType shouldBe RetryPolicyType.FIXED\n    //make sure optional secondaryHost is not set\n    config.secondaryHost shouldBe None\n\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CollectionResourceUsageTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport org.apache.openwhisk.core.database.cosmosdb.CollectionResourceUsage.{indexHeader, quotaHeader, usageHeader}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.core.entity.size._\n\n@RunWith(classOf[JUnitRunner])\nclass CollectionResourceUsageTests extends AnyFlatSpec with Matchers {\n  behavior of \"CollectionInfo\"\n\n  it should \"populate resource usage info\" in {\n    val headers = Map(\n      usageHeader ->\n        \"storedProcedures=0;triggers=0;functions=0;documentsCount=5058;documentsSize=780;collectionSize=800\",\n      quotaHeader -> \"storedProcedures=100;triggers=25;functions=25;documentsCount=-1;documentsSize=335544320;collectionSize=1000\",\n      indexHeader -> \"42\")\n\n    val usage = CollectionResourceUsage(headers).get\n    usage shouldBe CollectionResourceUsage(\n      documentsSize = Some(780.KB),\n      collectionSize = Some(800.KB),\n      documentsCount = Some(5058),\n      indexingProgress = Some(42),\n      documentsSizeQuota = Some(1000.KB))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBArtifactStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport java.util.concurrent.CountDownLatch\n\nimport org.apache.pekko.stream.scaladsl.Source\nimport com.typesafe.config.ConfigFactory\nimport io.netty.util.ResourceLeakDetector\nimport io.netty.util.ResourceLeakDetector.Level\nimport kamon.metric.Counter.LongAdder\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.DocumentSerializer\nimport org.apache.openwhisk.core.database.memory.MemoryAttachmentStoreProvider\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreBehavior\nimport org.apache.openwhisk.core.entity.WhiskQueries.TOP\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.{\n  DocumentReader,\n  Parameters,\n  WhiskActivation,\n  WhiskDocumentReader,\n  WhiskEntity,\n  WhiskEntityJsonFormat,\n  WhiskPackage\n}\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.JsString\n\nimport scala.concurrent.duration._\nimport scala.reflect.ClassTag\n\n@RunWith(classOf[JUnitRunner])\nclass CosmosDBArtifactStoreTests extends AnyFlatSpec with CosmosDBStoreBehaviorBase with ArtifactStoreBehavior {\n  override protected def maxAttachmentSizeWithoutAttachmentStore = 1.MB\n\n  private var initialLevel: Level = _\n\n  override protected def beforeAll(): Unit = {\n    RecordingLeakDetectorFactory.register()\n    initialLevel = ResourceLeakDetector.getLevel\n    ResourceLeakDetector.setLevel(Level.PARANOID)\n    super.beforeAll()\n  }\n\n  override protected def getAttachmentStore[D <: DocumentSerializer: ClassTag]() =\n    Some(MemoryAttachmentStoreProvider.makeStore[D]())\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    ResourceLeakDetector.setLevel(initialLevel)\n\n    //Try triggering GC which may trigger leak detection logic\n    System.gc()\n\n    withClue(\"Recorded leak count should be zero\") {\n      RecordingLeakDetectorFactory.counter.cur shouldBe 0\n    }\n  }\n\n  behavior of \"CosmosDB Setup\"\n\n  it should \"be configured with default throughput\" in {\n    //Trigger loading of the db\n    val stores = Seq(entityStore, authStore, activationStore)\n    stores.foreach { s =>\n      val doc = s.asInstanceOf[CosmosDBArtifactStore[_]].documentCollection()\n      val offer = client\n        .queryOffers(s\"SELECT * from c where c.offerResourceId = '${doc.getResourceId}'\", null)\n        .blockingOnlyResult()\n        .get\n      withClue(s\"Collection ${doc.getId} : \") {\n        offer.getThroughput shouldBe storeConfig.throughput\n      }\n    }\n  }\n\n  it should \"have clusterId set\" in {\n    implicit val tid: TransactionId = TransactionId.testing\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    implicit val format = WhiskEntityJsonFormat\n    val conf = ConfigFactory.parseString(s\"\"\"\n      | whisk.cosmosdb {\n      |  collections {\n      |     WhiskEntity = {\n      |        cluster-id = \"foo\"\n      |     }\n      |  }\n      | }\n         \"\"\".stripMargin).withFallback(ConfigFactory.load())\n\n    val cosmosDBConfig = CosmosDBConfig(conf, \"WhiskEntity\")\n    cosmosDBConfig.clusterId shouldBe Some(\"foo\")\n\n    val testConfig = cosmosDBConfig.copy(db = config.db)\n    val store = CosmosDBArtifactStoreProvider.makeArtifactStore[WhiskEntity](testConfig, None)\n\n    val pkg = WhiskPackage(newNS(), aname())\n    val info = put(store, pkg)\n\n    val js = store.getRaw(info.id).futureValue\n    js.get.fields(CosmosDBConstants.clusterId) shouldBe JsString(\"foo\")\n  }\n\n  it should \"fetch collection usage info\" in {\n    val uopt = activationStore.getResourceUsage().futureValue\n    uopt shouldBe defined\n    val u = uopt.get\n    println(u.asString)\n    u.documentsCount shouldBe defined\n    u.documentsSize shouldBe defined\n  }\n\n  behavior of \"CosmosDB query debug\"\n\n  it should \"log query metrics in debug flow\" in {\n    val debugTid = TransactionId(\"42\", extraLogging = true)\n    val tid = TransactionId(\"42\")\n    val ns = newNS()\n    val activations = (1000 until 1100 by 10).map(newActivation(ns.asString, \"testact\", _))\n    activations foreach (put(activationStore, _)(tid))\n\n    val entityPath = s\"${ns.asString}/testact\"\n    stream.reset()\n    query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 1050),\n      List(entityPath, TOP, TOP))(tid)\n\n    stream.toString should not include (\"[QueryMetricsEnabled]\")\n    stream.reset()\n\n    query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 1050),\n      List(entityPath, TOP, TOP))(debugTid)\n    stream.toString should include(\"[QueryMetricsEnabled]\")\n\n  }\n\n  behavior of \"CosmosDB retry metrics\"\n\n  it should \"capture success retries\" in {\n    implicit val tid: TransactionId = TransactionId.testing\n    val bigPkg = WhiskPackage(newNS(), aname(), parameters = Parameters(\"foo\", \"x\" * 1024 * 1024))\n    val latch = new CountDownLatch(1)\n    val f = Source(1 to 500)\n      .mapAsync(100) { i =>\n        latch.countDown()\n        if (i % 5 == 0) println(i)\n        require(retryCount == 0)\n        entityStore.put(bigPkg)\n      }\n      .runForeach { doc =>\n        docsToDelete += ((entityStore, doc))\n      }\n\n    //Wait for one save operation before checking for stats\n    latch.await()\n    retry(() => f, 500.millis)\n    retryCount should be > 0\n  }\n\n  private def retryCount: Int = {\n    //If KamonTags are disabled then Kamon uses CounterMetricImpl which does not provide\n    //any way of determining the current count. So in those cases the retry collector\n    //would increment a counter\n    if (TransactionId.metricsKamonTags) {\n      RetryMetricsCollector.getCounter(CosmosDBAction.Create) match {\n        case Some(x: LongAdder) => x.snapshot(false).intValue\n        case _                  => 0\n      }\n    } else {\n      RetryMetricsCollector.retryCounter.cur.toInt\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBAttachmentStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.database.DocumentSerializer\nimport org.apache.openwhisk.core.database.memory.MemoryAttachmentStoreProvider\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreAttachmentBehaviors\n\nimport scala.reflect.ClassTag\n\n@RunWith(classOf[JUnitRunner])\nclass CosmosDBAttachmentStoreTests\n    extends AnyFlatSpec\n    with CosmosDBStoreBehaviorBase\n    with ArtifactStoreAttachmentBehaviors {\n  override protected def getAttachmentStore[D <: DocumentSerializer: ClassTag]() =\n    Some(MemoryAttachmentStoreProvider.makeStore[D]())\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBConfigTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\nimport com.typesafe.config.ConfigFactory\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport com.microsoft.azure.cosmosdb.{ConnectionMode, ConnectionPolicy => JConnectionPolicy}\n\nimport scala.collection.JavaConverters._\n\n@RunWith(classOf[JUnitRunner])\nclass CosmosDBConfigTests extends AnyFlatSpec with Matchers {\n  val globalConfig = ConfigFactory.defaultApplication()\n  behavior of \"CosmosDB Config\"\n\n  it should \"match SDK defaults\" in {\n    val config = ConfigFactory.parseString(s\"\"\"\n      | whisk.cosmosdb {\n      |  endpoint = \"http://localhost\"\n      |  key = foo\n      |  db  = openwhisk\n      | }\n         \"\"\".stripMargin).withFallback(globalConfig)\n    val cosmos = CosmosDBConfig(config, \"WhiskAuth\")\n\n    //Cosmos SDK does not have equals defined so match them explicitly\n    val policy = cosmos.connectionPolicy.asJava\n    val defaultPolicy = JConnectionPolicy.GetDefault()\n    policy.getConnectionMode shouldBe defaultPolicy.getConnectionMode\n    policy.getEnableEndpointDiscovery shouldBe defaultPolicy.getEnableEndpointDiscovery\n    policy.getIdleConnectionTimeoutInMillis shouldBe defaultPolicy.getIdleConnectionTimeoutInMillis\n    policy.getMaxPoolSize shouldBe defaultPolicy.getMaxPoolSize\n    policy.getPreferredLocations shouldBe defaultPolicy.getPreferredLocations\n    policy.getRequestTimeoutInMillis shouldBe defaultPolicy.getRequestTimeoutInMillis\n    policy.isUsingMultipleWriteLocations shouldBe defaultPolicy.isUsingMultipleWriteLocations\n\n    val retryOpts = policy.getRetryOptions\n    val defaultOpts = defaultPolicy.getRetryOptions\n\n    retryOpts.getMaxRetryAttemptsOnThrottledRequests shouldBe defaultOpts.getMaxRetryAttemptsOnThrottledRequests\n    retryOpts.getMaxRetryWaitTimeInSeconds shouldBe defaultOpts.getMaxRetryWaitTimeInSeconds\n  }\n\n  it should \"work with generic config\" in {\n    val config = ConfigFactory.parseString(s\"\"\"\n      | whisk.cosmosdb {\n      |  endpoint = \"http://localhost\"\n      |  key = foo\n      |  db  = openwhisk\n      | }\n         \"\"\".stripMargin).withFallback(globalConfig)\n    val cosmos = CosmosDBConfig(config, \"WhiskAuth\")\n    cosmos.endpoint shouldBe \"http://localhost\"\n    cosmos.key shouldBe \"foo\"\n    cosmos.db shouldBe \"openwhisk\"\n  }\n\n  it should \"work with extended config\" in {\n    val config = ConfigFactory.parseString(s\"\"\"\n      | whisk.cosmosdb {\n      |  endpoint = \"http://localhost\"\n      |  key = foo\n      |  db  = openwhisk\n      |  connection-policy {\n      |     max-pool-size = 42\n      |  }\n      | }\n         \"\"\".stripMargin).withFallback(globalConfig)\n    val cosmos = CosmosDBConfig(config, \"WhiskAuth\")\n    cosmos.endpoint shouldBe \"http://localhost\"\n    cosmos.key shouldBe \"foo\"\n    cosmos.db shouldBe \"openwhisk\"\n\n    cosmos.connectionPolicy.maxPoolSize shouldBe 42\n    val policy = cosmos.connectionPolicy.asJava\n    val defaultPolicy = JConnectionPolicy.GetDefault()\n    policy.getConnectionMode shouldBe defaultPolicy.getConnectionMode\n    policy.getRetryOptions.getMaxRetryAttemptsOnThrottledRequests shouldBe defaultPolicy.getRetryOptions.getMaxRetryAttemptsOnThrottledRequests\n    policy.getRetryOptions.getMaxRetryWaitTimeInSeconds shouldBe defaultPolicy.getRetryOptions.getMaxRetryWaitTimeInSeconds\n  }\n\n  it should \"work with specific extended config\" in {\n    val config = ConfigFactory.parseString(s\"\"\"\n      | whisk.cosmosdb {\n      |  endpoint = \"http://localhost\"\n      |  key = foo\n      |  db  = openwhisk\n      |  connection-policy {\n      |     max-pool-size = 42\n      |     retry-options {\n      |        max-retry-wait-time = 2 m\n      |     }\n      |  }\n      |  collections {\n      |     WhiskAuth = {\n      |        connection-policy {\n      |           using-multiple-write-locations = true\n      |           preferred-locations = [a, b]\n      |           connection-mode = Direct\n      |        }\n      |     }\n      |  }\n      | }\n         \"\"\".stripMargin).withFallback(globalConfig)\n    val cosmos = CosmosDBConfig(config, \"WhiskAuth\")\n    cosmos.endpoint shouldBe \"http://localhost\"\n    cosmos.key shouldBe \"foo\"\n    cosmos.db shouldBe \"openwhisk\"\n\n    val policy = cosmos.connectionPolicy.asJava\n    policy.isUsingMultipleWriteLocations shouldBe true\n    policy.getMaxPoolSize shouldBe 42\n    policy.getConnectionMode shouldBe ConnectionMode.Direct\n    policy.getPreferredLocations.asScala.toSeq should contain only (\"a\", \"b\")\n    policy.getRetryOptions.getMaxRetryWaitTimeInSeconds shouldBe 120\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBLeakTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport io.netty.util.ResourceLeakDetector\nimport io.netty.util.ResourceLeakDetector.Level\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.{\n  BasicAuthenticationAuthKey,\n  Identity,\n  Namespace,\n  Secret,\n  Subject,\n  UUID,\n  WhiskAuth,\n  WhiskNamespace\n}\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration.DurationInt\n\n/**\n * Performs query flow which causes leak multiple times. Post fix this test should always pass\n * By default this test is disabled\n */\n@RunWith(classOf[JUnitRunner])\nclass CosmosDBLeakTests extends AnyFlatSpec with CosmosDBStoreBehaviorBase {\n\n  behavior of s\"CosmosDB leak\"\n\n  private var initialLevel: Level = _\n\n  override protected def beforeAll(): Unit = {\n    RecordingLeakDetectorFactory.register()\n    initialLevel = ResourceLeakDetector.getLevel\n    ResourceLeakDetector.setLevel(Level.PARANOID)\n    super.beforeAll()\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    ResourceLeakDetector.setLevel(initialLevel)\n\n    withClue(\"Recorded leak count should be zero\") {\n      RecordingLeakDetectorFactory.counter.cur shouldBe 0\n    }\n  }\n\n  it should \"not happen in performing subject query\" ignore {\n    implicit val tid: TransactionId = transid()\n    val uuid = UUID()\n    val ak = BasicAuthenticationAuthKey(uuid, Secret())\n    val ns = Namespace(aname(), uuid)\n    val subs =\n      Array(WhiskAuth(Subject(), Set(WhiskNamespace(ns, ak))))\n    subs foreach (put(authStore, _))\n\n    implicit val patienceConfig: PatienceConfig = PatienceConfig(timeout = 30.minutes)\n\n    Source(1 to 500)\n      .filter(_ => RecordingLeakDetectorFactory.counter.cur == 0)\n      .mapAsync(5) { i =>\n        if (i % 5 == 0) println(i)\n        queryName(ns)\n      }\n      .runWith(Sink.ignore)\n      .futureValue\n\n    System.gc()\n\n    withClue(\"Recorded leak count should be zero\") {\n      RecordingLeakDetectorFactory.counter.cur shouldBe 0\n    }\n  }\n\n  def queryName(ns: Namespace)(implicit tid: TransactionId) = {\n    Identity.list(authStore, List(ns.name.asString), limit = 1)\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBSoftDeleteTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\nimport org.apache.openwhisk.core.database.DocumentSerializer\nimport org.apache.openwhisk.core.database.memory.MemoryAttachmentStoreProvider\nimport org.apache.openwhisk.core.database.test.behavior.{ArtifactStoreCRUDBehaviors, ArtifactStoreQueryBehaviors}\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.reflect.ClassTag\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass CosmosDBSoftDeleteTests\n    extends AnyFlatSpec\n    with CosmosDBStoreBehaviorBase\n    with ArtifactStoreCRUDBehaviors\n    with ArtifactStoreQueryBehaviors {\n  override def storeType = \"CosmosDB_SoftDelete\"\n\n  override protected def getAttachmentStore[D <: DocumentSerializer: ClassTag]() =\n    Some(MemoryAttachmentStoreProvider.makeStore[D]())\n\n  override def adaptCosmosDBConfig(config: CosmosDBConfig): CosmosDBConfig =\n    config.copy(softDeleteTTL = Some(15.minutes))\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreBehaviorBase\nimport org.apache.openwhisk.core.database.{ArtifactStore, AttachmentStore, DocumentSerializer}\nimport org.apache.openwhisk.core.entity.{\n  DocumentReader,\n  WhiskActivation,\n  WhiskAuth,\n  WhiskDocumentReader,\n  WhiskEntity,\n  WhiskEntityJsonFormat\n}\n\nimport scala.reflect.{classTag, ClassTag}\nimport scala.util.Try\n\ntrait CosmosDBStoreBehaviorBase extends AnyFlatSpec with ArtifactStoreBehaviorBase with CosmosDBTestSupport {\n  override def storeType = \"CosmosDB\"\n\n  override lazy val storeAvailableCheck: Try[Any] = storeConfigTry\n\n  protected lazy val config: CosmosDBConfig = adaptCosmosDBConfig(storeConfig.copy(db = createTestDB().getId))\n\n  override lazy val authStore = {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    CosmosDBArtifactStoreProvider.makeArtifactStore[WhiskAuth](config, getAttachmentStore[WhiskAuth]())\n  }\n\n  override lazy val entityStore =\n    CosmosDBArtifactStoreProvider.makeArtifactStore[WhiskEntity](config, getAttachmentStore[WhiskEntity]())(\n      classTag[WhiskEntity],\n      WhiskEntityJsonFormat,\n      WhiskDocumentReader,\n      actorSystem,\n      logging)\n\n  override lazy val activationStore = {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    CosmosDBArtifactStoreProvider.makeArtifactStore[WhiskActivation](config, getAttachmentStore[WhiskActivation]())\n  }\n\n  override protected def getAttachmentStore(store: ArtifactStore[_]) =\n    store.asInstanceOf[CosmosDBArtifactStore[_]].attachmentStore\n\n  protected def getAttachmentStore[D <: DocumentSerializer: ClassTag](): Option[AttachmentStore] = None\n\n  protected def adaptCosmosDBConfig(config: CosmosDBConfig): CosmosDBConfig = config\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBSupportTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport com.microsoft.azure.cosmosdb.IndexKind.Range\nimport com.microsoft.azure.cosmosdb.DataType.String\nimport com.microsoft.azure.cosmosdb.DocumentCollection\nimport com.microsoft.azure.cosmosdb.rx.AsyncDocumentClient\nimport com.typesafe.config.ConfigFactory\nimport common.{StreamLogging, WskActorSystem}\nimport org.apache.openwhisk.core.entity.{\n  DocumentReader,\n  WhiskActivation,\n  WhiskDocumentReader,\n  WhiskEntity,\n  WhiskEntityJsonFormat\n}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.duration.DurationInt\n\n@RunWith(classOf[JUnitRunner])\nclass CosmosDBSupportTests\n    extends AnyFlatSpec\n    with CosmosDBTestSupport\n    with MockFactory\n    with Matchers\n    with StreamLogging\n    with WskActorSystem {\n\n  behavior of \"CosmosDB init\"\n\n  it should \"create and update index\" in {\n    val testDb = createTestDB()\n    val config: CosmosDBConfig = storeConfig.copy(db = testDb.getId)\n\n    val indexedPaths1 = Set(\"/foo/?\", \"/bar/?\")\n    val (_, coll) = new CosmosTest(config, client, newMapper(indexedPaths1)).initialize()\n    coll.getDefaultTimeToLive shouldBe -1\n    indexedPaths(coll) should contain theSameElementsAs indexedPaths1\n  }\n\n  it should \"set ttl\" in {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    val config = ConfigFactory.parseString(s\"\"\"\n      | whisk.cosmosdb {\n      |  collections {\n      |     WhiskActivation = {\n      |        time-to-live = 60 s\n      |     }\n      |  }\n      | }\n         \"\"\".stripMargin).withFallback(ConfigFactory.load())\n\n    val cosmosDBConfig = CosmosDBConfig(config, \"WhiskActivation\")\n    cosmosDBConfig.timeToLive shouldBe Some(60.seconds)\n\n    val testDb = createTestDB()\n    val testConfig = cosmosDBConfig.copy(db = testDb.getId)\n    val coll = CosmosDBArtifactStoreProvider.makeArtifactStore[WhiskActivation](testConfig, None).collection\n    coll.getDefaultTimeToLive shouldBe 60.seconds.toSeconds\n  }\n\n  it should \"not set ttl for WhiskEntity\" in {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    implicit val format = WhiskEntityJsonFormat\n    val config = ConfigFactory.parseString(s\"\"\"\n      | whisk.cosmosdb {\n      |  collections {\n      |     WhiskEntity = {\n      |        time-to-live = 60 s\n      |     }\n      |  }\n      | }\n         \"\"\".stripMargin).withFallback(ConfigFactory.load())\n\n    val cosmosDBConfig = CosmosDBConfig(config, \"WhiskEntity\")\n    cosmosDBConfig.timeToLive shouldBe Some(60.seconds)\n\n    val testConfig = cosmosDBConfig.copy(db = \"foo\")\n\n    an[IllegalArgumentException] shouldBe thrownBy {\n      CosmosDBArtifactStoreProvider.makeArtifactStore[WhiskEntity](testConfig, None).collection\n    }\n  }\n\n  private def newMapper(paths: Set[String]) = {\n    val mapper = stub[CosmosDBViewMapper]\n    (mapper.indexingPolicy _).when().returns(newTestIndexingPolicy(paths))\n    mapper\n  }\n\n  private def indexedPaths(coll: DocumentCollection) =\n    coll.getIndexingPolicy.getIncludedPaths.asScala.map(_.getPath).toList\n\n  protected def newTestIndexingPolicy(paths: Set[String]): IndexingPolicy =\n    IndexingPolicy(includedPaths = paths.map(p => IncludedPath(p, Index(Range, String, -1))))\n\n  private class CosmosTest(override val config: CosmosDBConfig,\n                           override val client: AsyncDocumentClient,\n                           mapper: CosmosDBViewMapper)\n      extends CosmosDBSupport {\n    override protected def collName = \"test\"\n    override protected def viewMapper = mapper\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBTestSupport.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport com.microsoft.azure.cosmosdb.{Database, SqlParameter, SqlParameterCollection, SqlQuerySpec}\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreTestUtil.storeAvailable\n\nimport scala.collection.mutable.ListBuffer\nimport scala.util.{Random, Try}\n\ntrait CosmosDBTestSupport extends AnyFlatSpecLike with BeforeAndAfterAll with RxObservableImplicits {\n  private val dbsToDelete = ListBuffer[Database]()\n\n  lazy val storeConfigTry = Try { loadConfigOrThrow[CosmosDBConfig](ConfigKeys.cosmosdb) }\n  lazy val client = storeConfig.createClient()\n  val useExistingDB = java.lang.Boolean.getBoolean(\"whisk.cosmosdb.useExistingDB\")\n\n  def storeConfig = storeConfigTry.get\n\n  override protected def withFixture(test: NoArgTest) = {\n    assume(storeAvailable(storeConfigTry), \"CosmosDB not configured or available\")\n    super.withFixture(test)\n  }\n\n  protected def generateDBName() = {\n    s\"travis-${getClass.getSimpleName}-${Random.alphanumeric.take(5).mkString}\"\n  }\n\n  protected def createTestDB() = {\n    if (useExistingDB) {\n      val db = getOrCreateDatabase()\n      println(s\"Using existing database ${db.getId}\")\n      db\n    } else {\n      val databaseDefinition = new Database\n      databaseDefinition.setId(generateDBName())\n      val db = client.createDatabase(databaseDefinition, null).blockingResult()\n      dbsToDelete += db\n      println(s\"Created database ${db.getId}\")\n      db\n    }\n  }\n\n  private def getOrCreateDatabase(): Database = {\n    client\n      .queryDatabases(querySpec(storeConfig.db), null)\n      .blockingOnlyResult()\n      .getOrElse {\n        client.createDatabase(newDatabase, null).blockingResult()\n      }\n  }\n\n  protected def querySpec(id: String) =\n    new SqlQuerySpec(\"SELECT * FROM root r WHERE r.id=@id\", new SqlParameterCollection(new SqlParameter(\"@id\", id)))\n\n  private def newDatabase = {\n    val databaseDefinition = new Database\n    databaseDefinition.setId(storeConfig.db)\n    databaseDefinition\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    if (!useExistingDB) {\n      dbsToDelete.foreach(db => client.deleteDatabase(db.getSelfLink, null).blockingResult())\n    }\n    client.close()\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/CosmosDBUtilTest.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.OptionValues\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json._\nimport org.apache.openwhisk.utils.JsHelpers\n\n@RunWith(classOf[JUnitRunner])\nclass CosmosDBUtilTest extends AnyFlatSpec with Matchers with OptionValues {\n\n  behavior of \"prepare field\"\n\n  it should \"always have id\" in {\n    val result = fieldsAsJson()\n    val expected = \"\"\"{\"id\" : \"r['id']\"}\"\"\".parseJson\n    result shouldBe expected\n    result.fields(\"id\") shouldBe JsString(\"r['id']\")\n  }\n\n  it should \"build a json like string\" in {\n    val result = fieldsAsJson(\"a\")\n    val expected = \"\"\"{ \"a\" : \"r['a']\", \"id\" : \"r['id']\"}\"\"\".parseJson\n    result shouldBe expected\n    result.fields(\"a\") shouldBe JsString(\"r['a']\")\n  }\n\n  it should \"support nested fields\" in {\n    val result = fieldsAsJson(\"a\", \"b.c\")\n    val expected = \"\"\"{\n                     |  \"a\": \"r['a']\",\n                     |  \"b\": {\n                     |    \"c\": \"r['b']['c']\"\n                     |  },\n                     |  \"id\": \"r['id']\"\n                     |}\"\"\".stripMargin.parseJson\n    result shouldBe expected\n    JsHelpers.getFieldPath(result, \"b\", \"c\").value shouldBe JsString(\"r['b']['c']\")\n  }\n\n  private def fieldsAsJson(fields: String*) = toJson(CosmosDBUtil.prepareFieldClause(fields.toSet))\n\n  private def toJson(s: String): JsObject = {\n    //Strip of last `As VIEW`\n    s.replace(\" AS view\", \"\")\n      .replaceAll(raw\"(r[\\w'\\[\\]]+)\", \"\\\"$1\\\"\")\n      .parseJson\n      .asJsObject\n  }\n\n  behavior of \"escaping\"\n\n  it should \"escape /\" in {\n    CosmosDBUtil.escapeId(\"foo/bar\") shouldBe \"foo|bar\"\n  }\n\n  it should \"throw exception when input contains replacement char |\" in {\n    an[IllegalArgumentException] should be thrownBy CosmosDBUtil.escapeId(\"foo/bar|baz\")\n  }\n\n  it should \"support unescaping\" in {\n    val s = \"foo/bar\"\n    CosmosDBUtil.unescapeId(CosmosDBUtil.escapeId(s)) shouldBe s\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/IndexingPolicyTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport com.microsoft.azure.cosmosdb.DataType.String\nimport com.microsoft.azure.cosmosdb.IndexKind.Range\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\n@RunWith(classOf[JUnitRunner])\nclass IndexingPolicyTests extends AnyFlatSpec with Matchers {\n  behavior of \"IndexingPolicy\"\n\n  it should \"match same instance\" in {\n    val policy =\n      IndexingPolicy(includedPaths = Set(IncludedPath(\"foo\", Index(Range, String, -1))))\n    IndexingPolicy.isSame(policy, policy) shouldBe true\n  }\n\n  it should \"not match when same path are different\" in {\n    val policy =\n      IndexingPolicy(\n        includedPaths =\n          Set(IncludedPath(\"foo\", Index(Range, String, -1)), IncludedPath(\"bar\", Index(Range, String, -1))))\n\n    val policy2 =\n      IndexingPolicy(\n        includedPaths = Set(\n          IncludedPath(\"foo2\", Index(Range, String, -1)),\n          IncludedPath(\"bar\", Set(Index(Range, String, -1), Index(Range, String, -1)))))\n\n    IndexingPolicy.isSame(policy, policy2) shouldBe false\n  }\n\n  it should \"convert and match java IndexingPolicy\" in {\n    val policy =\n      IndexingPolicy(\n        includedPaths = Set(\n          IncludedPath(\"foo\", Index(Range, String, -1)),\n          IncludedPath(\"bar\", Set(Index(Range, String, -1), Index(Range, String, -1)))))\n\n    val jpolicy = policy.asJava()\n    val policy2 = IndexingPolicy(jpolicy)\n\n    policy shouldBe policy2\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/RecordingLeakDetector.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb;\n\nimport io.netty.util.ResourceLeakDetector;\nimport org.apache.openwhisk.common.Counter;\n\npublic class RecordingLeakDetector<T> extends ResourceLeakDetector<T> {\n    private final Counter counter;\n    public RecordingLeakDetector(Counter counter, Class<?> resourceType, int samplingInterval) {\n        super(resourceType, samplingInterval);\n        this.counter = counter;\n    }\n\n    @Override\n    protected void reportTracedLeak(String resourceType, String records) {\n        super.reportTracedLeak(resourceType, records);\n        counter.next();\n    }\n\n    @Override\n    protected void reportUntracedLeak(String resourceType) {\n        super.reportUntracedLeak(resourceType);\n        counter.next();\n    }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/RecordingLeakDetectorFactory.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb;\n\nimport io.netty.util.ResourceLeakDetector;\nimport io.netty.util.ResourceLeakDetectorFactory;\nimport org.apache.openwhisk.common.Counter;\n\npublic class RecordingLeakDetectorFactory extends ResourceLeakDetectorFactory {\n    static final Counter counter = new Counter();\n    @Override\n    @SuppressWarnings(\"deprecation\")\n    public <T> ResourceLeakDetector<T> newResourceLeakDetector(Class<T> resource, int samplingInterval, long maxActive) {\n        return new RecordingLeakDetector<T>(counter, resource, samplingInterval);\n    }\n\n    public static void register() {\n        ResourceLeakDetectorFactory.setResourceLeakDetectorFactory(new RecordingLeakDetectorFactory());\n    }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/ReferenceCountedTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\n@RunWith(classOf[JUnitRunner])\nclass ReferenceCountedTests extends AnyFlatSpec with Matchers {\n\n  class CloseProbe extends AutoCloseable {\n    var closed: Boolean = _\n    var closedCount: Int = _\n    override def close(): Unit = {\n      closed = true\n      closedCount += 1\n    }\n  }\n\n  behavior of \"ReferenceCounted\"\n\n  it should \"close only once\" in {\n    val probe = new CloseProbe\n    val refCounted = ReferenceCounted(probe)\n\n    val ref1 = refCounted.reference()\n\n    ref1.get should be theSameInstanceAs probe\n    ref1.close()\n    ref1.close()\n\n    probe.closed shouldBe true\n    probe.closedCount shouldBe 1\n    refCounted.isClosed shouldBe true\n  }\n\n  it should \"not close with one reference active\" in {\n    val probe = new CloseProbe\n    val refCounted = ReferenceCounted(probe)\n\n    val ref1 = refCounted.reference()\n    val ref2 = refCounted.reference()\n\n    ref1.close()\n    ref1.close()\n\n    probe.closed shouldBe false\n  }\n\n  it should \"be closed when all reference close\" in {\n    val probe = new CloseProbe\n    val refCounted = ReferenceCounted(probe)\n\n    val ref1 = refCounted.reference()\n    val ref2 = refCounted.reference()\n\n    ref1.close()\n    ref2.close()\n\n    probe.closed shouldBe true\n    probe.closedCount shouldBe 1\n  }\n\n  it should \"throw exception if closed\" in {\n    val probe = new CloseProbe\n    val refCounted = ReferenceCounted(probe)\n\n    val ref1 = refCounted.reference()\n    ref1.close()\n\n    intercept[IllegalArgumentException] {\n      refCounted.reference()\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/cache/CacheInvalidatorTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb.cache\nimport java.net.UnknownHostException\nimport org.apache.pekko.Done\nimport org.apache.pekko.actor.CoordinatedShutdown\nimport org.apache.pekko.kafka.testkit.scaladsl.ScalatestKafkaSpec\nimport com.typesafe.config.ConfigFactory\nimport common.FreePortFinder\nimport io.github.embeddedkafka.{EmbeddedKafka, EmbeddedKafkaConfig}\nimport org.apache.kafka.common.KafkaException\nimport org.apache.kafka.common.serialization.StringDeserializer\nimport org.apache.openwhisk.common.{PekkoLogging, TransactionId}\nimport org.apache.openwhisk.core.database.{CacheInvalidationMessage, RemoteCacheInvalidation}\nimport org.apache.openwhisk.core.database.cosmosdb.{CosmosDBArtifactStoreProvider, CosmosDBTestSupport}\nimport org.apache.openwhisk.core.entity.{\n  DocumentReader,\n  EntityName,\n  EntityPath,\n  WhiskDocumentReader,\n  WhiskEntity,\n  WhiskEntityJsonFormat,\n  WhiskPackage\n}\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.TryValues\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.concurrent.duration._\nimport scala.util.Random\n\n@RunWith(classOf[JUnitRunner])\nclass CacheInvalidatorTests\n    extends ScalatestKafkaSpec(0)\n    with EmbeddedKafka\n    with CosmosDBTestSupport\n    with Matchers\n    with ScalaFutures\n    with TryValues {\n\n  private implicit val logging = new PekkoLogging(system.log)\n  implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = 300.seconds)\n\n  implicit val embeddedKafkaConfig: EmbeddedKafkaConfig =\n    EmbeddedKafkaConfig(kafkaPort = FreePortFinder.freePort(), zooKeeperPort = FreePortFinder.freePort())\n\n  override def bootstrapServers = s\"localhost:${embeddedKafkaConfig.kafkaPort}\"\n\n  override def setUp(): Unit = {\n    EmbeddedKafka.start()(embeddedKafkaConfig)\n    super.setUp()\n  }\n\n  override def cleanUp(): Unit = {\n    super.cleanUp()\n    EmbeddedKafka.stop()\n  }\n\n  behavior of \"CosmosDB CacheInvalidation\"\n\n  private val server = s\"localhost:${embeddedKafkaConfig.kafkaPort}\"\n  private var dbName: String = _\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    CoordinatedShutdown(system).run(CoordinatedShutdown.ClusterDowningReason)\n    shutdown()\n  }\n\n  it should \"send event upon entity change\" in {\n    implicit val tid = TransactionId.testing\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    implicit val format = WhiskEntityJsonFormat\n    dbName = createTestDB().getId\n    val dbConfig = storeConfig.copy(db = dbName)\n    val store = CosmosDBArtifactStoreProvider.makeArtifactStore[WhiskEntity](dbConfig, None)\n    val pkg = WhiskPackage(EntityPath(\"cacheInvalidationTest\"), EntityName(randomString()))\n\n    //Start cache invalidator after the db for whisks is created\n    val cacheInvalidator = startCacheInvalidator()\n    val (start, finish) = cacheInvalidator.start()\n    start.futureValue shouldBe Done\n    log.info(\"Cache Invalidator service started\")\n\n    //Store stuff in db\n    val info = store.put(pkg).futureValue\n    log.info(s\"Added document ${info.id}\")\n\n    //This should result in change feed trigger and event to kafka topic\n    val topic = RemoteCacheInvalidation.cacheInvalidationTopic\n    val msgs =\n      consumeNumberMessagesFromTopics(Set(topic), 1, timeout = 60.seconds)(\n        embeddedKafkaConfig,\n        new StringDeserializer())(topic)\n\n    CacheInvalidationMessage.parse(msgs.head).get.key.mainId shouldBe pkg.docid.asString\n\n    store.del(info).futureValue\n    cacheInvalidator.stop(None)\n    finish.futureValue shouldBe Done\n  }\n\n  it should \"exit if there is a missing kafka broker config\" in {\n    implicit val tid = TransactionId.testing\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    implicit val format = WhiskEntityJsonFormat\n    dbName = createTestDB().getId\n    val dbConfig = storeConfig.copy(db = dbName)\n    val store = CosmosDBArtifactStoreProvider.makeArtifactStore[WhiskEntity](dbConfig, None)\n    val pkg = WhiskPackage(EntityPath(\"cacheInvalidationTest\"), EntityName(randomString()))\n\n    //Start cache invalidator after the db for whisks is created\n    val cacheInvalidator = startCacheInvalidatorWithoutKafka()\n    val (start, finish) = cacheInvalidator.start()\n    start.futureValue shouldBe Done\n    log.info(\"Cache Invalidator service started\")\n    //when kafka config is missing, we expect KafkaException from producer immediately (although stopping feed processor takes some time)\n    finish.failed.futureValue shouldBe an[KafkaException]\n  }\n  it should \"exit if kafka is not consuming\" in {\n    implicit val tid = TransactionId.testing\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    implicit val format = WhiskEntityJsonFormat\n    dbName = createTestDB().getId\n    val dbConfig = storeConfig.copy(db = dbName)\n    val store = CosmosDBArtifactStoreProvider.makeArtifactStore[WhiskEntity](dbConfig, None)\n    val pkg = WhiskPackage(EntityPath(\"cacheInvalidationTest\"), EntityName(randomString()))\n\n    //Start cache invalidator with a bogus kafka broker after the db for whisks is created\n    val cacheInvalidator = startCacheInvalidatorWithInvalidKafka()\n    val (start, finish) = cacheInvalidator.start()\n    start.futureValue shouldBe Done\n    log.info(\"Cache Invalidator service started\")\n\n    //Store stuff in db\n    val info = store.put(pkg).futureValue\n    log.info(s\"Added document ${info.id}\")\n    //when we cannot connect to kafka, we expect KafkaException from producer after timeout\n    finish.failed.futureValue shouldBe an[KafkaException]\n  }\n\n  it should \"exit if there is a bad db config\" in {\n    //Start cache invalidator after the db for whisks is created\n    val cacheInvalidator = startCacheInvalidatorWithoutCosmos()\n    val (start, finish) = cacheInvalidator.start()\n    //when db config is broken, we expect reactor.core.Exceptions$ReactiveException (a non-public RuntimeException)\n    start.failed.futureValue.getCause shouldBe an[UnknownHostException]\n  }\n\n  private def randomString() = Random.alphanumeric.take(5).mkString\n\n  private def startCacheInvalidator(): CacheInvalidator = {\n    val tsconfig = ConfigFactory.parseString(s\"\"\"\n      |pekko.kafka.producer {\n      |  kafka-clients {\n      |    bootstrap.servers = \"$server\"\n      |  }\n      |}\n      |whisk {\n      |  cache-invalidator {\n      |    cosmosdb {\n      |      db = \"$dbName\"\n      |      start-from-beginning  = true\n      |    }\n      |  }\n      |}\n      \"\"\".stripMargin).withFallback(ConfigFactory.load())\n    new CacheInvalidator(tsconfig)\n  }\n  private def startCacheInvalidatorWithoutKafka(): CacheInvalidator = {\n    val tsconfig = ConfigFactory.parseString(s\"\"\"\n      |pekko.kafka.producer {\n      |  kafka-clients {\n      |    #this config is missing\n      |  }\n      |}\n      |whisk {\n      |  cache-invalidator {\n      |    cosmosdb {\n      |      db = \"$dbName\"\n      |      start-from-beginning  = true\n      |    }\n      |  }\n      |}\n      \"\"\".stripMargin).withFallback(ConfigFactory.load())\n    new CacheInvalidator(tsconfig)\n  }\n  private def startCacheInvalidatorWithInvalidKafka(): CacheInvalidator = {\n    val tsconfig = ConfigFactory.parseString(s\"\"\"\n      |pekko.kafka.producer {\n      |  kafka-clients {\n      |    bootstrap.servers = \"localhost:9092\"\n      |  }\n      |}\n      |whisk {\n      |  cache-invalidator {\n      |    cosmosdb {\n      |      db = \"$dbName\"\n      |      start-from-beginning  = true\n      |    }\n      |  }\n      |}\n      \"\"\".stripMargin).withFallback(ConfigFactory.load())\n    new CacheInvalidator(tsconfig)\n  }\n  private def startCacheInvalidatorWithoutCosmos(): CacheInvalidator = {\n    val tsconfig = ConfigFactory.parseString(s\"\"\"\n      |pekko.kafka.producer {\n      |  kafka-clients {\n      |    bootstrap.servers = \"$server\"\n      |  }\n      |}\n      |whisk {\n      |  cache-invalidator {\n      |    cosmosdb {\n      |      db = \"$dbName\"\n      |      endpoint = \"https://BADENDPOINT-nobody-home.documents.azure.com:443/\"\n      |      start-from-beginning  = true\n      |    }\n      |  }\n      |}\n      \"\"\".stripMargin).withFallback(ConfigFactory.load())\n    new CacheInvalidator(tsconfig)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/cosmosdb/cache/WhiskChangeEventObserverTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.cosmosdb.cache\nimport com.azure.data.cosmos.CosmosItemProperties\nimport common.StreamLogging\nimport org.apache.openwhisk.core.database.CacheInvalidationMessage\nimport org.apache.openwhisk.core.entity.CacheKey\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.DefaultJsonProtocol\n\nimport scala.collection.immutable.Seq\n\n@RunWith(classOf[JUnitRunner])\nclass WhiskChangeEventObserverTests extends AnyFlatSpec with Matchers with StreamLogging {\n  import WhiskChangeEventObserver.instanceId\n\n  behavior of \"CosmosDB extract LSN from Session token\"\n\n  it should \"parse old session token\" in {\n    WhiskChangeEventObserver.getSessionLsn(\"0:12345\") shouldBe 12345\n  }\n\n  it should \"parse new session token\" in {\n    WhiskChangeEventObserver.getSessionLsn(\"0:-1#12345\") shouldBe 12345\n  }\n\n  it should \"parse new session token with multiple regional lsn\" in {\n    WhiskChangeEventObserver.getSessionLsn(\"0:-1#12345#Region1=1#Region2=2\") shouldBe 12345\n  }\n\n  behavior of \"CosmosDB feed events\"\n\n  it should \"generate cache events\" in {\n    val config = InvalidatorConfig(8080, None)\n    val docs = Seq(createDoc(\"foo\"), createDoc(\"bar\"))\n    val processedDocs = WhiskChangeEventObserver.processDocs(docs, config)\n\n    processedDocs.map(CacheInvalidationMessage.parse(_).get) shouldBe Seq(\n      CacheInvalidationMessage(CacheKey(\"foo\"), instanceId),\n      CacheInvalidationMessage(CacheKey(\"bar\"), instanceId))\n  }\n\n  it should \"filter clusterId\" in {\n    val config = InvalidatorConfig(8080, Some(\"cid1\"))\n    val docs = Seq(createDoc(\"foo\", Some(\"cid2\")), createDoc(\"bar\", Some(\"cid1\")), createDoc(\"baz\"))\n    val processedDocs = WhiskChangeEventObserver.processDocs(docs, config)\n\n    //Should not include bar as the clusterId matches\n    processedDocs.map(CacheInvalidationMessage.parse(_).get) shouldBe Seq(\n      CacheInvalidationMessage(CacheKey(\"foo\"), instanceId),\n      CacheInvalidationMessage(CacheKey(\"baz\"), instanceId))\n  }\n\n  private def createDoc(id: String, clusterId: Option[String] = None): CosmosItemProperties = {\n    val cdoc = CosmosDBDoc(id, clusterId)\n    val json = CosmosDBDoc.seredes.write(cdoc).compactPrint\n    new CosmosItemProperties(json)\n  }\n\n  case class CosmosDBDoc(id: String, _clusterId: Option[String], _lsn: Int = 42)\n\n  object CosmosDBDoc extends DefaultJsonProtocol {\n    implicit val seredes = jsonFormat3(CosmosDBDoc.apply)\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/elasticsearch/ElasticSearchActivationStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.elasticsearch\n\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.apache.openwhisk.core.controller.test.WhiskAuthHelpers\nimport org.apache.openwhisk.core.database.UserContext\nimport org.apache.openwhisk.core.database.test.behavior.ActivationStoreBehaviorBase\nimport org.apache.openwhisk.core.entity.{ActivationResponse, Parameters, WhiskActivation}\nimport org.testcontainers.elasticsearch.ElasticsearchContainer\nimport pureconfig.loadConfigOrThrow\nimport spray.json.{JsObject, JsString}\n\ntrait ElasticSearchActivationStoreBehaviorBase extends AnyFlatSpec with ActivationStoreBehaviorBase {\n  val imageName = loadConfigOrThrow[String](\"whisk.elasticsearch.docker-image\")\n  val container = new ElasticsearchContainer(imageName)\n  container.start()\n\n  override def afterAll = {\n    container.close()\n    super.afterAll()\n  }\n\n  override def storeType = \"ElasticSearch\"\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  override val context = UserContext(creds)\n\n  override lazy val activationStore = {\n    val storeConfig =\n      ElasticSearchActivationStoreConfig(\"http\", container.getHttpHostAddress, \"unittest-%s\", \"fake\", \"fake\")\n    new ElasticSearchActivationStore(None, storeConfig, true)\n  }\n\n  // add result and annotations\n  override def newActivation(ns: String, actionName: String, start: Long): WhiskActivation = {\n    super\n      .newActivation(ns, actionName, start)\n      .copy(\n        response = ActivationResponse.success(Some(JsObject(\"name\" -> JsString(\"whisker\")))),\n        annotations = Parameters(\"database\", \"elasticsearch\") ++ Parameters(\"type\", \"test\"))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/elasticsearch/ElasticSearchActivationStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.elasticsearch\n\nimport java.time.Instant\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.UserContext\nimport org.apache.openwhisk.core.database.test.behavior.ActivationStoreBehavior\nimport org.apache.openwhisk.core.entity.{EntityPath, WhiskActivation}\nimport org.apache.openwhisk.utils.retry\nimport scala.concurrent.duration.DurationInt\n\n@RunWith(classOf[JUnitRunner])\nclass ElasticSearchActivationStoreTests\n    extends AnyFlatSpec\n    with ElasticSearchActivationStoreBehaviorBase\n    with ActivationStoreBehavior {\n\n  override def checkGetActivation(activation: WhiskActivation)(implicit transid: TransactionId): Unit = {\n    retry(super.checkGetActivation(activation), 10, Some(500.milliseconds))\n  }\n\n  override def checkDeleteActivation(activation: WhiskActivation)(implicit transid: TransactionId): Unit = {\n    retry(super.checkDeleteActivation(activation), 10, Some(500.milliseconds))\n  }\n\n  override def checkQueryActivations(namespace: String,\n                                     name: Option[String] = None,\n                                     skip: Int = 0,\n                                     limit: Int = 1000,\n                                     includeDocs: Boolean = false,\n                                     since: Option[Instant] = None,\n                                     upto: Option[Instant] = None,\n                                     context: UserContext,\n                                     expected: IndexedSeq[WhiskActivation])(implicit transid: TransactionId): Unit = {\n    retry(super.checkQueryActivations(namespace, name, skip, limit, includeDocs, since, upto, context, expected), 10)\n  }\n\n  override def checkCountActivations(namespace: String,\n                                     name: Option[EntityPath] = None,\n                                     skip: Int = 0,\n                                     since: Option[Instant] = None,\n                                     upto: Option[Instant] = None,\n                                     context: UserContext,\n                                     expected: Long)(implicit transid: TransactionId): Unit = {\n    retry(super.checkCountActivations(namespace, name, skip, since, upto, context, expected), 10)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/memory/MemoryArtifactStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.memory\n\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.apache.openwhisk.core.database.{ArtifactStore, AttachmentStore, DocumentSerializer}\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreBehaviorBase\nimport org.apache.openwhisk.core.entity.{\n  DocumentReader,\n  WhiskActivation,\n  WhiskAuth,\n  WhiskDocumentReader,\n  WhiskEntity,\n  WhiskEntityJsonFormat\n}\n\nimport scala.reflect.{classTag, ClassTag}\n\ntrait MemoryArtifactStoreBehaviorBase extends AnyFlatSpec with ArtifactStoreBehaviorBase {\n  override def storeType = \"Memory\"\n\n  override lazy val authStore = {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    MemoryArtifactStoreProvider.makeArtifactStore[WhiskAuth](getAttachmentStore[WhiskAuth]())\n  }\n\n  override protected def beforeAll(): Unit = {\n    MemoryArtifactStoreProvider.purgeAll()\n    super.beforeAll()\n  }\n\n  override lazy val entityStore =\n    MemoryArtifactStoreProvider.makeArtifactStore[WhiskEntity](getAttachmentStore[WhiskEntity]())(\n      classTag[WhiskEntity],\n      WhiskEntityJsonFormat,\n      WhiskDocumentReader,\n      actorSystem,\n      logging)\n\n  override lazy val activationStore = {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    MemoryArtifactStoreProvider.makeArtifactStore[WhiskActivation](getAttachmentStore[WhiskActivation]())\n  }\n\n  override protected def getAttachmentStore(store: ArtifactStore[_]) =\n    Some(store.asInstanceOf[MemoryArtifactStore[_]].attachmentStore)\n\n  protected def getAttachmentStore[D <: DocumentSerializer: ClassTag](): AttachmentStore =\n    MemoryAttachmentStoreProvider.makeStore()\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/memory/MemoryArtifactStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.memory\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreBehavior\n\n@RunWith(classOf[JUnitRunner])\nclass MemoryArtifactStoreTests extends AnyFlatSpec with MemoryArtifactStoreBehaviorBase with ArtifactStoreBehavior\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/memory/MemoryAttachmentStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.memory\n\nimport common.WskActorSystem\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.database.AttachmentStore\nimport org.apache.openwhisk.core.database.test.AttachmentStoreBehaviors\nimport org.apache.openwhisk.core.entity.WhiskEntity\n\n@RunWith(classOf[JUnitRunner])\nclass MemoryAttachmentStoreTests extends AnyFlatSpec with AttachmentStoreBehaviors with WskActorSystem {\n\n  override val store: AttachmentStore = MemoryAttachmentStoreProvider.makeStore[WhiskEntity]()\n\n  override def storeType: String = \"Memory\"\n\n  override protected def beforeAll(): Unit = {\n    MemoryArtifactStoreProvider.purgeAll()\n    super.beforeAll()\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    val count = store.asInstanceOf[MemoryAttachmentStore].attachmentCount\n    require(count == 0, s\"AttachmentStore not empty after all runs - $count\")\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/mongodb/MongoDBArtifactStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreBehavior\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass MongoDBArtifactStoreTests extends AnyFlatSpec with MongoDBStoreBehaviorBase with ArtifactStoreBehavior {}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/mongodb/MongoDBAsyncStreamGraphTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport java.io.{ByteArrayInputStream, ByteArrayOutputStream, IOException, InputStream}\n\nimport org.apache.pekko.stream.scaladsl.{Keep, Sink, StreamConverters}\nimport org.apache.pekko.stream.testkit.TestSubscriber\nimport org.apache.pekko.util.ByteString\nimport common.WskActorSystem\nimport org.apache.commons.io.IOUtils\nimport org.junit.runner.RunWith\nimport org.mockito.ArgumentMatchers._\nimport org.mockito.Mockito._\nimport org.mongodb.scala.gridfs.helpers.AsyncStreamHelper\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatestplus.mockito.MockitoSugar\n\nimport scala.util.Random\n\n@RunWith(classOf[JUnitRunner])\nclass MongoDBAsyncStreamGraphTests\n    extends AnyFlatSpec\n    with Matchers\n    with ScalaFutures\n    with WskActorSystem\n    with MockitoSugar\n    with IntegrationPatience {\n\n  behavior of \"MongoDBAsyncStreamSource\"\n\n  it should \"read all bytes\" in {\n    val bytes = randomBytes(4000)\n    val asyncStream = AsyncStreamHelper.toAsyncInputStream(bytes)\n\n    val readStream = MongoDBAsyncStreamSource(asyncStream, 42).runWith(StreamConverters.asInputStream())\n    val readBytes = IOUtils.toByteArray(readStream)\n\n    bytes shouldBe readBytes\n  }\n\n  it should \"close the stream when done\" in {\n    val bytes = randomBytes(4000)\n    val inputStream = new ByteArrayInputStream(bytes)\n    val spiedStream = spy(inputStream)\n    val asyncStream = AsyncStreamHelper.toAsyncInputStream(spiedStream)\n\n    val readStream = MongoDBAsyncStreamSource(asyncStream, 42).runWith(StreamConverters.asInputStream())\n    val readBytes = IOUtils.toByteArray(readStream)\n\n    bytes shouldBe readBytes\n    verify(spiedStream).close()\n  }\n\n  it should \"onError with failure and return a failed IOResult when reading from failed stream\" in {\n    val inputStream = mock[InputStream]\n\n    val exception = new IOException(\"Boom\")\n    doThrow(exception).when(inputStream).read(any())\n    val asyncStream = AsyncStreamHelper.toAsyncInputStream(inputStream)\n\n    val (_, p) = MongoDBAsyncStreamSource(asyncStream).toMat(Sink.asPublisher(false))(Keep.both).run()\n    val c = TestSubscriber.manualProbe[ByteString]()\n    p.subscribe(c)\n\n    val sub = c.expectSubscription()\n    sub.request(1)\n\n    val error = c.expectError()\n    error.getCause should be theSameInstanceAs exception\n\n  }\n\n  behavior of \"MongoDBAsyncStreamSink\"\n\n  it should \"write all bytes\" in {\n    val bytes = randomBytes(4000)\n    val source = StreamConverters.fromInputStream(() => new ByteArrayInputStream(bytes), 42)\n\n    val os = new ByteArrayOutputStream()\n    val asyncStream = AsyncStreamHelper.toAsyncOutputStream(os)\n\n    val sink = MongoDBAsyncStreamSink(asyncStream)\n    val ioResult = source.toMat(sink)(Keep.right).run()\n\n    ioResult.futureValue.count shouldBe bytes.length\n\n    val writtenBytes = os.toByteArray\n    writtenBytes shouldBe bytes\n  }\n\n  it should \"close the stream when done\" in {\n    val bytes = randomBytes(4000)\n    val source = StreamConverters.fromInputStream(() => new ByteArrayInputStream(bytes), 42)\n\n    val outputStream = new CloseRecordingStream()\n    val asyncStream = AsyncStreamHelper.toAsyncOutputStream(outputStream)\n\n    val sink = MongoDBAsyncStreamSink(asyncStream)\n    val ioResult = source.toMat(sink)(Keep.right).run()\n\n    ioResult.futureValue.count shouldBe 4000\n    outputStream.toByteArray shouldBe bytes\n    outputStream.closed shouldBe true\n  }\n\n  private def randomBytes(size: Int): Array[Byte] = {\n    val arr = new Array[Byte](size)\n    Random.nextBytes(arr)\n    arr\n  }\n\n  private class CloseRecordingStream extends ByteArrayOutputStream {\n    var closed: Boolean = _\n    override def close() = { super.close(); closed = true }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/mongodb/MongoDBAttachmentStoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport org.apache.openwhisk.core.database.DocumentSerializer\nimport org.apache.openwhisk.core.database.memory.MemoryAttachmentStoreProvider\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreAttachmentBehaviors\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.reflect.ClassTag\n\n@RunWith(classOf[JUnitRunner])\nclass MongoDBAttachmentStoreTests\n    extends AnyFlatSpec\n    with MongoDBStoreBehaviorBase\n    with ArtifactStoreAttachmentBehaviors {\n  override protected def getAttachmentStore[D <: DocumentSerializer: ClassTag]() =\n    Some(MemoryAttachmentStoreProvider.makeStore[D]())\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/mongodb/MongoDBStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreBehaviorBase\nimport org.apache.openwhisk.core.database.{ArtifactStore, AttachmentStore, DocumentSerializer}\nimport org.apache.openwhisk.core.entity._\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.testcontainers.containers.MongoDBContainer\nimport pureconfig.loadConfigOrThrow\nimport pureconfig.generic.auto._\n\nimport scala.reflect.{classTag, ClassTag}\n\ntrait MongoDBStoreBehaviorBase extends AnyFlatSpec with ArtifactStoreBehaviorBase {\n  val imageName = loadConfigOrThrow[String](\"whisk.mongodb.docker-image\")\n  val container = new MongoDBContainer(imageName)\n  container.start()\n\n  override def afterAll = {\n    container.close()\n    super.afterAll()\n  }\n\n  override def storeType = \"MongoDB\"\n\n  val storeConfig = MongoDBConfig(container.getReplicaSetUrl, \"unittest\")\n\n  override lazy val authStore = {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    MongoDBArtifactStoreProvider.makeArtifactStore[WhiskAuth](storeConfig, getAttachmentStore[WhiskAuth]())\n  }\n\n  override lazy val entityStore =\n    MongoDBArtifactStoreProvider.makeArtifactStore[WhiskEntity](storeConfig, getAttachmentStore[WhiskEntity]())(\n      classTag[WhiskEntity],\n      WhiskEntityJsonFormat,\n      WhiskDocumentReader,\n      actorSystem,\n      logging)\n\n  override lazy val activationStore = {\n    implicit val docReader: DocumentReader = WhiskDocumentReader\n    MongoDBArtifactStoreProvider\n      .makeArtifactStore[WhiskActivation](storeConfig, getAttachmentStore[WhiskActivation]())\n  }\n\n  override protected def getAttachmentStore(store: ArtifactStore[_]) =\n    store.asInstanceOf[MongoDBArtifactStore[_]].attachmentStore\n\n  protected def getAttachmentStore[D <: DocumentSerializer: ClassTag](): Option[AttachmentStore] = None\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/mongodb/MongoDBViewMapperTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.mongodb\n\nimport org.apache.openwhisk.core.database.{UnsupportedQueryKeys, UnsupportedView}\nimport org.apache.openwhisk.core.entity.WhiskQueries.TOP\nimport org.bson.conversions.Bson\nimport org.junit.runner.RunWith\nimport org.mongodb.scala.MongoClient\nimport org.mongodb.scala.bson.BsonDocument\nimport org.mongodb.scala.bson.collection.immutable.Document\nimport org.mongodb.scala.model.Filters.{equal => meq, _}\nimport org.mongodb.scala.model.Sorts\nimport org.scalatest.OptionValues\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass MongoDBViewMapperTests extends AnyFlatSpec with Matchers with OptionValues {\n  implicit class RichBson(val b: Bson) {\n    def toDoc: BsonDocument = b.toBsonDocument(classOf[Document], MongoClient.DEFAULT_CODEC_REGISTRY)\n  }\n\n  behavior of \"ActivationViewMapper filter\"\n\n  it should \"match all activations in namespace\" in {\n    ActivationViewMapper.filter(\"whisks.v2.1.0\", \"activations\", List(\"ns1\"), List(\"ns1\", TOP)).toDoc shouldBe\n      meq(\"namespace\", \"ns1\").toDoc\n    ActivationViewMapper.filter(\"whisks-filters.v2.1.0\", \"activations\", List(\"ns1\"), List(\"ns1\", TOP)).toDoc shouldBe\n      meq(\"_computed.nspath\", \"ns1\").toDoc\n  }\n\n  it should \"match all activations in namespace since zero\" in {\n    ActivationViewMapper.filter(\"whisks.v2.1.0\", \"activations\", List(\"ns1\", 0), List(\"ns1\", TOP, TOP)).toDoc shouldBe\n      and(meq(\"namespace\", \"ns1\"), gte(\"start\", 0)).toDoc\n\n    ActivationViewMapper\n      .filter(\"whisks-filters.v2.1.0\", \"activations\", List(\"ns1\", 0), List(\"ns1\", TOP, TOP))\n      .toDoc shouldBe\n      and(meq(\"_computed.nspath\", \"ns1\"), gte(\"start\", 0)).toDoc\n  }\n\n  it should \"match all activations in namespace since some value\" in {\n    ActivationViewMapper.filter(\"whisks.v2.1.0\", \"activations\", List(\"ns1\", 42), List(\"ns1\", TOP, TOP)).toDoc shouldBe\n      and(meq(\"namespace\", \"ns1\"), gte(\"start\", 42)).toDoc\n\n    ActivationViewMapper\n      .filter(\"whisks-filters.v2.1.0\", \"activations\", List(\"ns1\", 42), List(\"ns1\", TOP, TOP))\n      .toDoc shouldBe\n      and(meq(\"_computed.nspath\", \"ns1\"), gte(\"start\", 42)).toDoc\n  }\n\n  it should \"match all activations in namespace between 2 instants\" in {\n    ActivationViewMapper.filter(\"whisks.v2.1.0\", \"activations\", List(\"ns1\", 42), List(\"ns1\", 314, TOP)).toDoc shouldBe\n      and(meq(\"namespace\", \"ns1\"), gte(\"start\", 42), lte(\"start\", 314)).toDoc\n\n    ActivationViewMapper\n      .filter(\"whisks-filters.v2.1.0\", \"activations\", List(\"ns1\", 42), List(\"ns1\", 314, TOP))\n      .toDoc shouldBe\n      and(meq(\"_computed.nspath\", \"ns1\"), gte(\"start\", 42), lte(\"start\", 314)).toDoc\n  }\n\n  it should \"throw UnsupportedQueryKeys for unknown keys\" in {\n    intercept[UnsupportedQueryKeys] {\n      ActivationViewMapper.filter(\"whisks.v2.1.0\", \"activations\", List(\"ns1\"), List(\"ns1\", \"foo\"))\n    }\n  }\n\n  it should \"throw UnsupportedView exception for unknown views\" in {\n    intercept[UnsupportedView] {\n      ActivationViewMapper.filter(\"whisks.v2.1.0\", \"activation-foo\", List(\"ns1\"), List(\"ns1\", TOP))\n    }\n  }\n\n  behavior of \"ActivationViewMapper sort\"\n\n  it should \"sort descending\" in {\n    ActivationViewMapper.sort(\"whisks-filters.v2.1.0\", \"activations\", descending = true).value.toDoc shouldBe\n      Sorts.descending(\"_computed.nspath\", \"start\").toDoc\n    ActivationViewMapper.sort(\"whisks.v2.1.0\", \"activations\", descending = true).value.toDoc shouldBe\n      Sorts.descending(\"namespace\", \"start\").toDoc\n  }\n\n  it should \"sort ascending\" in {\n    ActivationViewMapper.sort(\"whisks-filters.v2.1.0\", \"activations\", descending = false).value.toDoc shouldBe\n      Sorts.ascending(\"_computed.nspath\", \"start\").toDoc\n    ActivationViewMapper.sort(\"whisks.v2.1.0\", \"activations\", descending = false).value.toDoc shouldBe\n      Sorts.ascending(\"namespace\", \"start\").toDoc\n  }\n\n  it should \"throw UnsupportedView\" in {\n    intercept[UnsupportedView] {\n      ActivationViewMapper.sort(\"whisks.v2.1.0\", \"activation-foo\", descending = true)\n    }\n  }\n\n  behavior of \"WhisksViewMapper filter\"\n\n  val whiskTypes = Seq(\n    (\"actions\", \"action\"),\n    (\"packages\", \"package\"),\n    (\"packages-public\", \"package\"),\n    (\"rules\", \"rule\"),\n    (\"triggers\", \"trigger\"))\n\n  it should \"match entities of specific type in namespace\" in {\n    whiskTypes.foreach {\n      case (view, entityType) =>\n        var filters =\n          or(\n            and(meq(\"entityType\", entityType), meq(\"namespace\", \"ns1\")),\n            and(meq(\"entityType\", entityType), meq(\"_computed.rootns\", \"ns1\")))\n        if (view == \"packages-public\")\n          filters = getPublicPackageFilter(filters)\n        WhisksViewMapper.filter(\"whisks.v2.1.0\", view, List(\"ns1\"), List(\"ns1\", TOP)).toDoc shouldBe filters.toDoc\n    }\n  }\n\n  it should \"match entities of specific type in namespace and updated since\" in {\n    whiskTypes.foreach {\n      case (view, entityType) =>\n        var filters =\n          or(\n            and(meq(\"entityType\", entityType), meq(\"namespace\", \"ns1\"), gte(\"updated\", 42)),\n            and(meq(\"entityType\", entityType), meq(\"_computed.rootns\", \"ns1\"), gte(\"updated\", 42)))\n        if (view == \"packages-public\")\n          filters = getPublicPackageFilter(filters)\n        WhisksViewMapper\n          .filter(\"whisks.v2.1.0\", view, List(\"ns1\", 42), List(\"ns1\", TOP, TOP))\n          .toDoc shouldBe filters.toDoc\n    }\n  }\n\n  it should \"match all entities of specific type in namespace and between\" in {\n    whiskTypes.foreach {\n      case (view, entityType) =>\n        var filters =\n          or(\n            and(meq(\"entityType\", entityType), meq(\"namespace\", \"ns1\"), gte(\"updated\", 42), lte(\"updated\", 314)),\n            and(meq(\"entityType\", entityType), meq(\"_computed.rootns\", \"ns1\"), gte(\"updated\", 42), lte(\"updated\", 314)))\n        if (view == \"packages-public\")\n          filters = getPublicPackageFilter(filters)\n        WhisksViewMapper\n          .filter(\"whisks.v2.1.0\", view, List(\"ns1\", 42), List(\"ns1\", 314, TOP))\n          .toDoc shouldBe filters.toDoc\n    }\n  }\n\n  it should \"match all entities in namespace\" in {\n    WhisksViewMapper.filter(\"whisks.v2.1.0\", \"all\", List(\"ns1\"), List(\"ns1\", TOP)).toDoc shouldBe\n      and(exists(\"entityType\"), meq(\"_computed.rootns\", \"ns1\")).toDoc\n  }\n\n  it should \"throw UnsupportedQueryKeys for unknown keys\" in {\n    intercept[UnsupportedQueryKeys] {\n      WhisksViewMapper.filter(\"whisks.v2.1.0\", \"actions\", List(\"ns1\"), List(\"ns1\", \"foo\"))\n    }\n    intercept[UnsupportedQueryKeys] {\n      WhisksViewMapper.filter(\"whisks.v2.1.0\", \"all\", List(\"ns1\"), List(\"ns1\", \"foo\"))\n    }\n  }\n\n  it should \"throw UnsupportedView exception for unknown views\" in {\n    intercept[UnsupportedView] {\n      WhisksViewMapper.filter(\"whisks.v2.1.0\", \"actions-foo\", List(\"ns1\"), List(\"ns1\", TOP))\n    }\n  }\n\n  behavior of \"WhisksViewMapper sort\"\n\n  it should \"sort descending\" in {\n    whiskTypes.foreach {\n      case (view, _) =>\n        WhisksViewMapper.sort(\"whisks.v2.1.0\", view, descending = true).value.toDoc shouldBe\n          Sorts.descending(\"updated\").toDoc\n    }\n  }\n\n  it should \"sort ascending\" in {\n    whiskTypes.foreach {\n      case (view, _) =>\n        WhisksViewMapper.sort(\"whisks.v2.1.0\", view, descending = false).value.toDoc shouldBe\n          Sorts.ascending(\"updated\").toDoc\n    }\n  }\n\n  it should \"throw UnsupportedView\" in {\n    intercept[UnsupportedView] {\n      WhisksViewMapper.sort(\"whisks.v2.1.0\", \"action-foo\", descending = true)\n    }\n  }\n\n  behavior of \"SubjectViewMapper filter\"\n\n  it should \"match by subject or namespace\" in {\n    SubjectViewMapper.filter(\"subjects\", \"identities\", List(\"foo\"), List(\"foo\")).toDoc shouldBe\n      and(notEqual(\"blocked\", true), or(meq(\"subject\", \"foo\"), meq(\"namespaces.name\", \"foo\"))).toDoc\n  }\n\n  it should \"match by uuid and key\" in {\n    SubjectViewMapper.filter(\"subjects\", \"identities\", List(\"u1\", \"k1\"), List(\"u1\", \"k1\")).toDoc shouldBe\n      and(\n        notEqual(\"blocked\", true),\n        or(and(meq(\"uuid\", \"u1\"), meq(\"key\", \"k1\")), and(meq(\"namespaces.uuid\", \"u1\"), meq(\"namespaces.key\", \"k1\")))).toDoc\n  }\n\n  it should \"match by blocked or invocationsPerMinute or concurrentInvocations\" in {\n    SubjectViewMapper\n      .filter(\"namespaceThrottlings\", \"blockedNamespaces\", List(\"u1\", \"k1\"), List(\"u1\", \"k1\"))\n      .toDoc shouldBe\n      or(meq(\"blocked\", true), meq(\"concurrentInvocations\", 0), meq(\"invocationsPerMinute\", 0)).toDoc\n  }\n\n  it should \"throw exception when keys are not same\" in {\n    intercept[IllegalArgumentException] {\n      SubjectViewMapper.filter(\"subjects\", \"identities\", List(\"u1\", \"k1\"), List(\"u1\", \"k2\"))\n    }\n  }\n\n  it should \"throw UnsupportedQueryKeys exception when keys are not know\" in {\n    intercept[UnsupportedQueryKeys] {\n      SubjectViewMapper.filter(\"subjects\", \"identities\", List(\"u1\", \"k1\", \"foo\"), List(\"u1\", \"k1\", \"foo\"))\n    }\n  }\n\n  it should \"throw UnsupportedView exception when view is not known\" in {\n    intercept[UnsupportedView] {\n      SubjectViewMapper.filter(\"subjects\", \"identities-foo\", List(\"u1\", \"k1\", \"foo\"), List(\"u1\", \"k1\", \"foo\"))\n    }\n  }\n\n  behavior of \"SubjectViewMapper sort\"\n\n  it should \"sort none\" in {\n    SubjectViewMapper.sort(\"subjects\", \"identities\", descending = true) shouldBe None\n    SubjectViewMapper.sort(\"namespaceThrottlings\", \"blockedNamespaces\", descending = true) shouldBe None\n  }\n\n  private def getPublicPackageFilter(filters: Bson): Bson = {\n    and(meq(\"binding\", Map.empty), meq(\"publish\", true), filters)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/s3/CloudFrontSignerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\nimport org.apache.pekko.http.scaladsl.model.Uri.Path\nimport com.typesafe.config.ConfigFactory\nimport java.time.Instant\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.s3.S3AttachmentStoreProvider.S3Config\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.OptionValues\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport pureconfig._\nimport pureconfig.generic.auto._\n\n@RunWith(classOf[JUnitRunner])\nclass CloudFrontSignerTests extends AnyFlatSpec with Matchers with OptionValues {\n\n  val qt = \"\\\"\\\"\\\"\"\n  val privateKey =\n    \"\"\"-----BEGIN RSA PRIVATE KEY-----\n      |MIIBPAIBAAJBAOY+Q7vyH1SnCUoFIpzqmZe1TNCxiE6zuiMRmjuJqiAzQWdb5hEA\n      |ZaC+f7Lcu53IvczZR0KsP4JndzG23rVg/y0CAwEAAQJBAMK+F3x4ppdrUSgSf9xJ\n      |cfAnoPlDsA8hZWcUFGgXYJYqKYw3NqoYG5fwyZ7xrwdMhpbdgD++nsBC/JMwUhEB\n      |h+ECIQDzj5Tbd7WvfaKGjozwQgHA9u3f53kxCWovpFEngU6VNwIhAPIAkAPnzuDr\n      |q3cEyAbM49ozjyc6/NOV6QK65HQj1gC7AiBrax/Ty3At/dL4VVaDgBkV6dHvtj8V\n      |CXnzmRzRt43Y8QIhAIzrvPE5RGP/eEqHUz96glhm276Zf+5qBlTbpfrnf0/PAiEA\n      |r1vFsvC8+KSHv7XGU1xfeiHHpHxEfDvJlX7/CxeWumQ=\n      |-----END RSA PRIVATE KEY-----\n      |\"\"\".stripMargin\n\n  val keyPairId = \"OPENWHISKISFUNTOUSE\"\n  val configString =\n    s\"\"\"whisk {\n       |  s3 {\n       |    bucket = \"openwhisk-test\"\n       |    prefix = \"dev\"\n       |    cloud-front-config {\n       |      domain-name = \"foo.com\"\n       |      key-pair-id = \"$keyPairId\"\n       |      private-key = $qt$privateKey$qt\n       |      timeout = 10 m\n       |    }\n       |  }\n       |}\"\"\".stripMargin\n\n  behavior of \"CloudFront config\"\n\n  it should \"generate a signed url\" in {\n    val config = ConfigFactory.parseString(configString).withFallback(ConfigFactory.load())\n    val s3Config = loadConfigOrThrow[S3Config](config, ConfigKeys.s3)\n    val signer = CloudFrontSigner(s3Config.cloudFrontConfig.get)\n    val expiration = Instant.now().plusSeconds(s3Config.cloudFrontConfig.get.timeout.toSeconds)\n    val uri = signer.getSignedURL(\"bar\")\n    val query = uri.query()\n\n    //A signed url is of format\n    //https://<domain-name>/<object key>?Expires=xxx&Signature=xxx&Key-Pair-Id=xxx\n    uri.scheme shouldBe \"https\"\n    uri.path.tail shouldBe Path(\"bar\")\n    query.get(\"Expires\") shouldBe Some(expiration.getEpochSecond.toString)\n    query.get(\"Signature\") shouldBe defined\n    query.get(\"Key-Pair-Id\").value shouldBe keyPairId\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/s3/S3AttachmentStoreAwsTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.entity.WhiskEntity\n\n@RunWith(classOf[JUnitRunner])\nclass S3AttachmentStoreAwsTests extends S3AttachmentStoreBehaviorBase with S3Aws {\n  override lazy val store = makeS3Store[WhiskEntity]\n\n  override def storeType: String = \"S3\"\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/s3/S3AttachmentStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.database.{AttachmentStore, DocumentSerializer}\nimport org.apache.openwhisk.core.database.memory.{MemoryArtifactStoreBehaviorBase, MemoryArtifactStoreProvider}\nimport org.apache.openwhisk.core.database.test.AttachmentStoreBehaviors\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreAttachmentBehaviors\nimport org.apache.openwhisk.core.entity.WhiskEntity\n\nimport scala.reflect.ClassTag\nimport scala.util.Random\n\ntrait S3AttachmentStoreBehaviorBase\n    extends AnyFlatSpec\n    with MemoryArtifactStoreBehaviorBase\n    with ArtifactStoreAttachmentBehaviors\n    with AttachmentStoreBehaviors {\n  override lazy val store = makeS3Store[WhiskEntity]\n\n  override val prefix = s\"attachmentTCK_${Random.alphanumeric.take(4).mkString}\"\n\n  override protected def beforeAll(): Unit = {\n    MemoryArtifactStoreProvider.purgeAll()\n    super.beforeAll()\n  }\n\n  override def getAttachmentStore[D <: DocumentSerializer: ClassTag](): AttachmentStore =\n    makeS3Store[D]()\n\n  def makeS3Store[D <: DocumentSerializer: ClassTag]()(implicit actorSystem: ActorSystem,\n                                                       logging: Logging): AttachmentStore\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/s3/S3AttachmentStoreCloudFrontTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\nimport org.apache.openwhisk.core.entity.WhiskEntity\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass S3AttachmentStoreCloudFrontTests extends S3AttachmentStoreBehaviorBase with S3Aws {\n  override lazy val store = makeS3Store[WhiskEntity]\n\n  override def storeType: String = \"S3_CloudFront\"\n  override def cloudFrontConfig: String =\n    \"\"\"\n      |cloud-front-config {\n      |  domain-name = ${CLOUDFRONT_DOMAIN_NAME}\n      |  key-pair-id = ${CLOUDFRONT_KEY_PAIR_ID}\n      |  private-key = ${CLOUDFRONT_PRIVATE_KEY}\n      |}\n    \"\"\".stripMargin\n\n  override protected def withFixture(test: NoArgTest) = {\n    assume(\n      System.getenv(\"CLOUDFRONT_PRIVATE_KEY\") != null,\n      \"Configure following env variables for test \" +\n        \"to run 'CLOUDFRONT_DOMAIN_NAME', 'CLOUDFRONT_KEY_PAIR_ID', 'CLOUDFRONT_PRIVATE_KEY'\")\n    super.withFixture(test)\n  }\n\n  //With CloudFront deletes are not immediate and instead the objects may live in CDN cache until TTL\n  override protected val lazyDeletes = true\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/s3/S3AttachmentStoreMinioTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.entity.WhiskEntity\n\n@RunWith(classOf[JUnitRunner])\nclass S3AttachmentStoreMinioTests extends S3AttachmentStoreBehaviorBase with S3Minio {\n  override lazy val store = makeS3Store[WhiskEntity]\n\n  override def storeType: String = \"S3Minio\"\n\n  override def garbageCollectAttachments: Boolean = false\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/s3/S3Aws.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\n\nimport org.apache.pekko.actor.ActorSystem\nimport com.typesafe.config.ConfigFactory\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.database.{AttachmentStore, DocumentSerializer}\n\nimport scala.reflect.ClassTag\n\ntrait S3Aws extends AnyFlatSpec {\n\n  def cloudFrontConfig: String = \"\"\n\n  def makeS3Store[D <: DocumentSerializer: ClassTag]()(implicit actorSystem: ActorSystem,\n                                                       logging: Logging): AttachmentStore = {\n    val config = ConfigFactory.parseString(s\"\"\"\n       |whisk {\n       |   s3 {\n       |      pekko-connectors {\n       |         aws {\n       |           credentials {\n       |             provider = static\n       |             access-key-id = \"$accessKeyId\"\n       |             secret-access-key = \"$secretAccessKey\"\n       |           }\n       |           region {\n       |             provider = static\n       |             default-region = \"$region\"\n       |           }\n       |         }\n       |      }\n       |      bucket = \"$bucket\"\n       |      $cloudFrontConfig\n       |    }\n       |}\n      \"\"\".stripMargin).withFallback(ConfigFactory.load()).resolve()\n    S3AttachmentStoreProvider.makeStore[D](config)\n  }\n\n  override protected def withFixture(test: NoArgTest) = {\n    assume(\n      secretAccessKey != null && secretAccessKey != \"\",\n      \"'AWS_SECRET_ACCESS_KEY' env not configured. Configure following \" +\n        \"env variables for test to run. 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'\")\n\n    require(accessKeyId != null, \"'AWS_ACCESS_KEY_ID' env variable not set\")\n    require(region != null, \"'AWS_REGION' env variable not set\")\n\n    super.withFixture(test)\n  }\n\n  val bucket = Option(System.getenv(\"AWS_BUCKET\")).getOrElse(\"test-ow-travis\")\n\n  val accessKeyId = System.getenv(\"AWS_ACCESS_KEY_ID\")\n  val secretAccessKey = System.getenv(\"AWS_SECRET_ACCESS_KEY\")\n  val region = System.getenv(\"AWS_REGION\")\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/s3/S3Minio.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\n\nimport java.net.ServerSocket\n\nimport actionContainers.ActionContainer\nimport org.apache.pekko.actor.ActorSystem\nimport com.amazonaws.auth.{AWSStaticCredentialsProvider, BasicAWSCredentials}\nimport com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration\nimport com.amazonaws.services.s3.AmazonS3ClientBuilder\nimport com.typesafe.config.ConfigFactory\nimport common.{SimpleExec, StreamLogging}\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.database.{AttachmentStore, DocumentSerializer}\n\nimport scala.concurrent.duration._\nimport scala.reflect.ClassTag\n\ntrait S3Minio extends AnyFlatSpec with BeforeAndAfterAll with StreamLogging {\n  def makeS3Store[D <: DocumentSerializer: ClassTag]()(implicit actorSystem: ActorSystem,\n                                                       logging: Logging): AttachmentStore = {\n    val config = ConfigFactory.parseString(s\"\"\"\n      |whisk {\n      |     s3 {\n      |      pekko-connectors {\n      |         aws {\n      |           credentials {\n      |             provider = static\n      |             access-key-id = \"$accessKey\"\n      |             secret-access-key = \"$secretAccessKey\"\n      |           }\n      |           region {\n      |             provider = static\n      |             default-region = us-west-2\n      |           }\n      |         }\n      |         endpoint-url = \"http://localhost:$port\"\n      |      }\n      |      bucket = \"$bucket\"\n      |      $prefixConfig\n      |     }\n      |}\n      \"\"\".stripMargin).withFallback(ConfigFactory.load())\n    S3AttachmentStoreProvider.makeStore[D](config)\n  }\n\n  private val accessKey = \"TESTKEY\"\n  private val secretAccessKey = \"TESTSECRET\"\n  private val port = freePort()\n  private val bucket = \"test-ow-travis\"\n\n  private def prefixConfig = {\n    if (bucketPrefix.nonEmpty) s\"prefix = $bucketPrefix\" else \"\"\n  }\n\n  protected def bucketPrefix: String = \"\"\n\n  override protected def beforeAll(): Unit = {\n    super.beforeAll()\n    dockerExec(\n      s\"run -d -e MINIO_ACCESS_KEY=$accessKey -e MINIO_SECRET_KEY=$secretAccessKey -p $port:9000 minio/minio server /data\")\n    println(s\"Started minio on $port\")\n    createTestBucket()\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    val containerId = dockerExec(\"ps -q --filter ancestor=minio/minio\")\n    containerId.split(\"\\n\").map(_.trim).foreach(id => dockerExec(s\"stop $id\"))\n    println(s\"Stopped minio container\")\n  }\n\n  def createTestBucket(): Unit = {\n    val endpoint = new EndpointConfiguration(s\"http://localhost:$port\", \"us-west-2\")\n    val client = AmazonS3ClientBuilder.standard\n      .withPathStyleAccessEnabled(true)\n      .withEndpointConfiguration(endpoint)\n      .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretAccessKey)))\n      .build\n\n    org.apache.openwhisk.utils.retry(client.createBucket(bucket), 6, Some(1.minute))\n    println(s\"Created bucket $bucket\")\n  }\n\n  private def dockerExec(cmd: String): String = {\n    implicit val tid: TransactionId = TransactionId.testing\n    val command = s\"${ActionContainer.dockerCmd} $cmd\"\n    val cmdSeq = command.split(\" \").map(_.trim).filter(_.nonEmpty)\n    val (out, err, code) = SimpleExec.syncRunCmd(cmdSeq)\n    assert(code == 0, s\"Error occurred for command '$command'. Exit code: $code, Error: $err\")\n    out\n  }\n\n  private def freePort(): Int = {\n    val socket = new ServerSocket(0)\n    try socket.getLocalPort\n    finally if (socket != null) socket.close()\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/s3/S3WithPrefixTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.s3\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.database.s3.S3AttachmentStoreProvider.S3Config\nimport org.apache.openwhisk.core.entity.WhiskEntity\n\n@RunWith(classOf[JUnitRunner])\nclass S3WithPrefixTests extends S3AttachmentStoreMinioTests {\n  override protected val bucketPrefix: String = \"master\"\n\n  behavior of \"S3Config\"\n\n  it should \"work with none prefix\" in {\n    val config = S3Config(\"foo\", None)\n    config.prefixFor[WhiskEntity] shouldBe \"whiskentity\"\n  }\n\n  it should \"work with optional prefix\" in {\n    val config = S3Config(\"foo\", Some(\"bar\"))\n    config.prefixFor[WhiskEntity] shouldBe \"bar/whiskentity\"\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/AttachmentCompatibilityTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport java.io.ByteArrayInputStream\nimport java.util.Base64\n\nimport org.apache.pekko.http.scaladsl.model.{ContentType, StatusCodes}\nimport org.apache.pekko.stream.scaladsl.{Source, StreamConverters}\nimport org.apache.pekko.util.ByteString\nimport common.{StreamLogging, WskActorSystem}\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.controller.test.WhiskAuthHelpers\nimport org.apache.openwhisk.core.database.memory.MemoryAttachmentStoreProvider\nimport org.apache.openwhisk.core.database.{CouchDbConfig, CouchDbRestClient, CouchDbStoreProvider, NoDocumentException}\nimport org.apache.openwhisk.core.entity.Attachments.Inline\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.apache.openwhisk.core.entity._\n\nimport scala.concurrent.Future\nimport scala.reflect.classTag\n\n@RunWith(classOf[JUnitRunner])\nclass AttachmentCompatibilityTests\n    extends AnyFlatSpec\n    with Matchers\n    with ScalaFutures\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with WskActorSystem\n    with ExecHelpers\n    with DbUtils\n    with DefaultJsonProtocol\n    with StreamLogging {\n\n  //Bring in sync the timeout used by ScalaFutures and DBUtils\n  implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = dbOpTimeout)\n\n  val creds = WhiskAuthHelpers.newIdentity()\n  val namespace = EntityPath(creds.subject.asString)\n  def aname() = MakeName.next(\"action_tests\")\n  val config = loadConfigOrThrow[CouchDbConfig](ConfigKeys.couchdb)\n  val entityStore = WhiskEntityStore.datastore()\n  val client =\n    new CouchDbRestClient(\n      config.protocol,\n      config.host,\n      config.port,\n      config.username,\n      config.password,\n      config.databaseFor[WhiskEntity])\n\n  override def afterEach(): Unit = {\n    cleanup()\n  }\n\n  override protected def withFixture(test: NoArgTest) = {\n    assume(isCouchStore(entityStore))\n    super.withFixture(test)\n  }\n\n  behavior of \"Attachments\"\n\n  it should \"read attachments created using old scheme\" in {\n    implicit val tid: TransactionId = transid()\n    val namespace = EntityPath(\"attachment-compat-test1\")\n    val exec = javaDefault(\"ZHViZWU=\", Some(\"hello\"))\n    val doc =\n      WhiskAction(namespace, EntityName(\"attachment_unique\"), exec)\n\n    createAction(doc)\n\n    val doc2 = WhiskAction.get(entityStore, doc.docid).futureValue\n    doc2.exec shouldBe exec\n  }\n\n  it should \"read attachments created using old scheme with AttachmentStore\" in {\n    implicit val tid: TransactionId = transid()\n    val namespace = EntityPath(\"attachment-compat-test2\")\n    val exec = javaDefault(\"ZHViZWU=\", Some(\"hello\"))\n    val doc =\n      WhiskAction(namespace, EntityName(\"attachment_unique\"), exec)\n\n    createAction(doc)\n\n    val entityStore2 = createEntityStore()\n    val doc2 = WhiskAction.get(entityStore2, doc.docid).futureValue\n    doc2.exec shouldBe exec\n  }\n\n  it should \"read existing base64 encoded code string\" in {\n    implicit val tid: TransactionId = transid()\n    val exec = \"\"\"{\n               |  \"kind\": \"nodejs:20\",\n               |  \"code\": \"SGVsbG8gT3BlbldoaXNr\"\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    val (id, action) = makeActionJson(namespace, aname(), exec)\n    val info = putDoc(id, action)\n\n    val action2 = WhiskAction.get(entityStore, info.id).futureValue\n    codeExec(action2).codeAsJson shouldBe JsString(\"SGVsbG8gT3BlbldoaXNr\")\n  }\n\n  it should \"read existing simple code string\" in {\n    implicit val tid: TransactionId = transid()\n    val exec = \"\"\"{\n                 |  \"kind\": \"nodejs:20\",\n                 |  \"code\": \"while (true)\"\n                 |}\"\"\".stripMargin.parseJson.asJsObject\n    val (id, action) = makeActionJson(namespace, aname(), exec)\n    val info = putDoc(id, action)\n\n    val action2 = WhiskAction.get(entityStore, info.id).futureValue\n    codeExec(action2).codeAsJson shouldBe JsString(\"while (true)\")\n  }\n\n  it should \"read existing simple code string for blackbox action\" in {\n    implicit val tid: TransactionId = transid()\n    val exec = \"\"\"{\n                 |  \"kind\": \"blackbox\",\n                 |  \"image\": \"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\",\n                 |  \"code\":  \"while (true)\",\n                 |  \"binary\": false\n                 |}\"\"\".stripMargin.parseJson.asJsObject\n    val (id, action) = makeActionJson(namespace, aname(), exec)\n    val info = putDoc(id, action)\n\n    val action2 = WhiskAction.get(entityStore, info.id).futureValue\n    codeExec(action2).codeAsJson shouldBe JsString(\"while (true)\")\n  }\n\n  private def codeExec(a: WhiskAction) = a.exec.asInstanceOf[CodeExec[_]]\n\n  private def makeActionJson(namespace: EntityPath, name: EntityName, exec: JsObject): (String, JsObject) = {\n    val id = namespace.addPath(name).asString\n    val base = s\"\"\"{\n                 |  \"name\": \"${name.asString}\",\n                 |  \"_id\": \"$id\",\n                 |  \"publish\": false,\n                 |  \"annotations\": [],\n                 |  \"version\": \"0.0.1\",\n                 |  \"updated\": 1533623651650,\n                 |  \"entityType\": \"action\",\n                 |  \"parameters\": [\n                 |    {\n                 |      \"key\": \"x\",\n                 |      \"value\": \"b\"\n                 |    }\n                 |  ],\n                 |  \"limits\": {\n                 |    \"timeout\": 60000,\n                 |    \"memory\": 256,\n                 |    \"logs\": 10\n                 |  },\n                 |  \"namespace\": \"${namespace.asString}\"\n                 |}\"\"\".stripMargin.parseJson.asJsObject\n    (id, JsObject(base.fields + (\"exec\" -> exec)))\n  }\n\n  private def putDoc(id: String, js: JsObject): DocInfo = {\n    val r = client.putDoc(id, js).futureValue\n    r match {\n      case Right(response) =>\n        val info = response.convertTo[DocInfo]\n        docsToDelete += ((entityStore, info))\n        info\n      case _ => fail()\n    }\n  }\n\n  private def createAction(doc: WhiskAction) = {\n    implicit val tid: TransactionId = transid()\n    doc.exec match {\n      case exec @ CodeExecAsAttachment(_, Inline(code), _, _) =>\n        val attached = exec.manifest.attached.get\n\n        val newDoc = doc.copy(exec = exec.copy(code = attached))\n        newDoc.revision(doc.rev)\n\n        val codeBytes = Base64.getDecoder().decode(code)\n        val stream = new ByteArrayInputStream(codeBytes)\n        val src = StreamConverters.fromInputStream(() => stream)\n        val info = entityStore.put(newDoc).futureValue\n        val info2 = attach(info, attached.attachmentName, attached.attachmentType, src).futureValue\n        docsToDelete += ((entityStore, info2))\n      case _ =>\n        fail(\"Exec must be code attachment\")\n    }\n  }\n\n  object MakeName {\n    @volatile var counter = 1\n    def next(prefix: String = \"test\")(): EntityName = {\n      counter = counter + 1\n      EntityName(s\"${prefix}_name$counter\")\n    }\n  }\n\n  private def attach(doc: DocInfo,\n                     name: String,\n                     contentType: ContentType,\n                     docStream: Source[ByteString, _]): Future[DocInfo] = {\n    client.putAttachment(doc.id.id, doc.rev.rev, name, contentType, docStream).map {\n      case Right(response) =>\n        val id = response.fields(\"id\").convertTo[String]\n        val rev = response.fields(\"rev\").convertTo[String]\n        DocInfo ! (id, rev)\n\n      case Left(StatusCodes.NotFound) =>\n        throw NoDocumentException(\"Not found on 'readAttachment'.\")\n\n      case Left(code) =>\n        throw new Exception(\"Unexpected http response code: \" + code)\n    }\n  }\n\n  private def createEntityStore() =\n    CouchDbStoreProvider\n      .makeArtifactStore[WhiskEntity](\n        useBatching = false,\n        Some(MemoryAttachmentStoreProvider.makeStore[WhiskEntity]()))(\n        classTag[WhiskEntity],\n        WhiskEntityJsonFormat,\n        WhiskDocumentReader,\n        actorSystem,\n        logging)\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/AttachmentStoreBehaviors.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport java.io.ByteArrayInputStream\n\nimport org.apache.pekko.http.scaladsl.model.ContentTypes\nimport org.apache.pekko.stream.scaladsl.{Sink, Source, StreamConverters}\nimport org.apache.pekko.util.{ByteString, ByteStringBuilder}\nimport common.{StreamLogging, WskActorSystem}\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.{AttachmentStore, NoDocumentException}\nimport org.apache.openwhisk.core.entity.DocId\n\nimport scala.collection.mutable.ListBuffer\nimport scala.concurrent.Await\nimport scala.concurrent.duration.DurationInt\nimport scala.util.Random\n\ntrait AttachmentStoreBehaviors\n    extends ScalaFutures\n    with DbUtils\n    with Matchers\n    with StreamLogging\n    with WskActorSystem\n    with BeforeAndAfterAll {\n  this: AnyFlatSpec =>\n\n  //Bring in sync the timeout used by ScalaFutures and DBUtils\n  implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = dbOpTimeout)\n\n  protected val prefix = s\"attachmentTCK_${Random.alphanumeric.take(4).mkString}\"\n\n  private val attachmentsToDelete = ListBuffer[String]()\n\n  def store: AttachmentStore\n\n  def storeType: String\n\n  def garbageCollectAttachments: Boolean = true\n\n  /**\n   * In some cases like when CloudFront CDN is used then deletes are not immediately reflected in reads\n   * as the objects are still present in cache. For such cases we would relax some of the test assertions\n   */\n  protected def lazyDeletes: Boolean = false\n\n  behavior of s\"$storeType AttachmentStore\"\n\n  it should \"add and read attachment\" in {\n    implicit val tid: TransactionId = transid()\n    val bytes = randomBytes(16023)\n\n    val docId = newDocId()\n    val result = store.attach(docId, \"code\", ContentTypes.`application/octet-stream`, chunkedSource(bytes)).futureValue\n\n    result.length shouldBe 16023\n\n    val byteBuilder = store.readAttachment(docId, \"code\", byteStringSink()).futureValue\n\n    byteBuilder.result() shouldBe ByteString(bytes)\n    garbageCollect(docId)\n  }\n\n  it should \"add and delete attachments\" in {\n    implicit val tid: TransactionId = transid()\n    val b1 = randomBytes(1000)\n    val b2 = randomBytes(2000)\n    val b3 = randomBytes(3000)\n\n    val docId = newDocId()\n    //create another doc with similar name to verify it is unaffected by deletes of the first docs attachments\n    val docId2 = DocId(docId.id + \"2\")\n    val r1 = store.attach(docId, \"c1\", ContentTypes.`application/octet-stream`, chunkedSource(b1)).futureValue\n    val r2 = store.attach(docId, \"c2\", ContentTypes.`application/json`, chunkedSource(b2)).futureValue\n    val r3 = store.attach(docId, \"c3\", ContentTypes.`application/json`, chunkedSource(b3)).futureValue\n    //create attachments for the other doc\n    val r21 = store.attach(docId2, \"c21\", ContentTypes.`application/octet-stream`, chunkedSource(b1)).futureValue\n    val r22 = store.attach(docId2, \"c22\", ContentTypes.`application/json`, chunkedSource(b2)).futureValue\n\n    r1.length shouldBe 1000\n    r2.length shouldBe 2000\n    r3.length shouldBe 3000\n    r21.length shouldBe 1000\n    r22.length shouldBe 2000\n\n    println(s\"creating doc ${docId}\")\n    attachmentBytes(docId, \"c1\").futureValue.result() shouldBe ByteString(b1)\n    attachmentBytes(docId, \"c2\").futureValue.result() shouldBe ByteString(b2)\n    attachmentBytes(docId, \"c3\").futureValue.result() shouldBe ByteString(b3)\n    attachmentBytes(docId2, \"c21\").futureValue.result() shouldBe ByteString(b1)\n    attachmentBytes(docId2, \"c22\").futureValue.result() shouldBe ByteString(b2)\n\n    //Delete single attachment\n    store.deleteAttachment(docId, \"c1\").futureValue shouldBe true\n\n    //Non deleted attachments related to same docId must still be accessible\n    println(s\"read missing doc ${docId}\")\n    if (!lazyDeletes) attachmentBytes(docId, \"c1\").failed.futureValue shouldBe a[NoDocumentException]\n    attachmentBytes(docId, \"c2\").futureValue.result() shouldBe ByteString(b2)\n    attachmentBytes(docId, \"c3\").futureValue.result() shouldBe ByteString(b3)\n\n    //Delete all attachments\n    store.deleteAttachments(docId).futureValue shouldBe true\n\n    if (!lazyDeletes) attachmentBytes(docId, \"c2\").failed.futureValue shouldBe a[NoDocumentException]\n    if (!lazyDeletes) attachmentBytes(docId, \"c3\").failed.futureValue shouldBe a[NoDocumentException]\n\n    //Make sure doc2 attachments are left untouched\n    if (!lazyDeletes) attachmentBytes(docId2, \"c21\").futureValue.result() shouldBe ByteString(b1)\n    if (!lazyDeletes) attachmentBytes(docId2, \"c22\").futureValue.result() shouldBe ByteString(b2)\n\n    attachmentsToDelete += docId2.asString\n  }\n\n  it should \"throw NoDocumentException on reading non existing attachment\" in {\n    implicit val tid: TransactionId = transid()\n\n    val docId = DocId(\"no-existing-id\")\n    val f = store.readAttachment(docId, \"code\", byteStringSink())\n\n    f.failed.futureValue shouldBe a[NoDocumentException]\n  }\n\n  it should \"not write an attachment when there is error in Source\" in {\n    implicit val tid: TransactionId = transid()\n\n    val docId = newDocId()\n    val error = new Error(\"boom!\")\n    val faultySource = Source(1 to 10)\n      .map { n =>\n        if (n == 7) throw error\n        n\n      }\n      .map(ByteString(_))\n    val writeResult = store.attach(docId, \"code\", ContentTypes.`application/octet-stream`, faultySource)\n    writeResult.failed.futureValue.getCause should be theSameInstanceAs error\n\n    val readResult = store.readAttachment(docId, \"code\", byteStringSink())\n    readResult.failed.futureValue shouldBe a[NoDocumentException]\n  }\n\n  override def afterAll(): Unit = {\n    if (garbageCollectAttachments) {\n      implicit val tid: TransactionId = transid()\n      val f =\n        Source(attachmentsToDelete.toList)\n          .mapAsync(2)(id => store.deleteAttachments(DocId(id)))\n          .runWith(Sink.ignore)\n      Await.result(f, 1.minute)\n    }\n    super.afterAll()\n  }\n\n  protected def garbageCollect(docId: DocId): Unit = {}\n\n  protected def newDocId(): DocId = {\n    //By default create an info with dummy revision\n    //as apart from CouchDB other stores do not support the revision property\n    //for blobs\n    counter = counter + 1\n    val docId = s\"${prefix}_$counter\"\n    attachmentsToDelete += docId\n    DocId(docId)\n  }\n\n  @volatile var counter = 0\n\n  private def attachmentBytes(id: DocId, name: String) = {\n    implicit val tid: TransactionId = transid()\n    store.readAttachment(id, name, byteStringSink())\n  }\n\n  private def chunkedSource(bytes: Array[Byte]): Source[ByteString, _] = {\n    StreamConverters.fromInputStream(() => new ByteArrayInputStream(bytes), 42)\n  }\n\n  private def byteStringSink() = {\n    Sink.fold[ByteStringBuilder, ByteString](new ByteStringBuilder)((builder, b) => builder ++= b)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/AttachmentSupportTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.util.CompactByteString\nimport common.WskActorSystem\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.{AttachmentSupport, InliningConfig}\nimport org.apache.openwhisk.core.entity.WhiskEntity\nimport org.apache.openwhisk.core.entity.size._\n\n@RunWith(classOf[JUnitRunner])\nclass AttachmentSupportTests extends AnyFlatSpec with Matchers with ScalaFutures with WskActorSystem {\n\n  behavior of \"Attachment inlining\"\n\n  it should \"not inline if maxInlineSize set to zero\" in {\n    val inliner = new AttachmentSupportTestMock(InliningConfig(maxInlineSize = 0.KB))\n    val bs = CompactByteString(\"hello world\")\n\n    val bytesOrSource = inliner.inlineOrAttach(Source.single(bs)).futureValue\n    val uri = inliner.uriOf(bytesOrSource, \"foo\")\n\n    uri shouldBe Uri(\"test:foo\")\n  }\n\n  class AttachmentSupportTestMock(val inliningConfig: InliningConfig) extends AttachmentSupport[WhiskEntity] {\n    override protected def attachmentScheme: String = \"test\"\n    override protected def executionContext = actorSystem.dispatcher\n    override protected[database] def put(d: WhiskEntity)(implicit transid: TransactionId) = ???\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/BatcherTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport java.time.Instant\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport common.{LoggedFunction, WskActorSystem}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.core.database.Batcher\nimport org.apache.openwhisk.utils.retry\n\nimport scala.collection.mutable\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, Future, Promise}\n\n@RunWith(classOf[JUnitRunner])\nclass BatcherTests extends AnyFlatSpec with Matchers with WskActorSystem {\n\n  def await[V](f: Future[V]) = Await.result(f, 10.seconds)\n\n  def between(start: Instant, end: Instant) =\n    Duration.fromNanos(java.time.Duration.between(start, end).toNanos)\n\n  val promiseDelay = 100.milliseconds\n  def resolveDelayed(p: Promise[Unit], delay: FiniteDuration = promiseDelay) =\n    org.apache.pekko.pattern.after(delay, actorSystem.scheduler) {\n      p.success(())\n      Future.successful(())\n    }\n\n  behavior of \"Batcher\"\n\n  it should \"batch based on batch size\" in {\n    val ps = Seq.fill(3)(Promise[Unit]())\n    val batchPromises = mutable.Queue(ps: _*)\n\n    val transform = (i: Int) => i + 1\n\n    val batchOperation = LoggedFunction((els: Seq[Int], retry: Int) => {\n      batchPromises.dequeue().future.map(_ => els.map(transform))\n    })\n\n    val batcher = new Batcher[Int, Int](2, 1, 1)(batchOperation)\n\n    val values = 1 to 5\n    val results = values.map(batcher.put)\n\n    // First \"batch\"\n    retry(batchOperation.calls should have size 1, (promiseDelay.toMillis * 2).toInt)\n    batchOperation.calls(0)._1 should have size 1\n\n    // Allow batch to build up\n    resolveDelayed(ps(0))\n\n    // Second batch\n    retry(batchOperation.calls should have size 2, (promiseDelay.toMillis * 2).toInt)\n    batchOperation.calls(1)._1 should have size 2\n\n    // Allow batch to build up\n    resolveDelayed(ps(1))\n\n    // Third batch\n    retry(batchOperation.calls should have size 3, (promiseDelay.toMillis * 2).toInt)\n    batchOperation.calls(2)._1 should have size 2\n    ps(2).success(())\n\n    await(Future.sequence(results)) shouldBe values.map(transform)\n  }\n\n  it should \"run batches through the operation in parallel\" in {\n    val p = Promise[Unit]()\n    val parallel = new AtomicInteger(0)\n    val concurrency = 2\n\n    val batcher = new Batcher[Int, Int](1, concurrency, 1)((els, _) => {\n      parallel.incrementAndGet()\n      p.future.map(_ => els)\n    })\n\n    val values = 1 to 3\n    val results = values.map(batcher.put)\n\n    // Before we resolve the promise, 2 batches should have entered the batch operation\n    // which is now hanging and waiting for the promise to be resolved.\n    retry(parallel.get shouldBe concurrency, 100)\n\n    p.success(())\n\n    await(Future.sequence(results)) shouldBe values\n  }\n\n  it should \"complete batched values with the thrown exception\" in {\n    val batcher = new Batcher[Int, Int](2, 1, 1)((_, _) => Future.failed(new Exception))\n\n    val r1 = batcher.put(1)\n    val r2 = batcher.put(2)\n\n    an[Exception] should be thrownBy await(r1)\n    an[Exception] should be thrownBy await(r2)\n\n    // the batcher is still intact\n    val r3 = batcher.put(3)\n    val r4 = batcher.put(4)\n\n    an[Exception] should be thrownBy await(r3)\n    an[Exception] should be thrownBy await(r4)\n  }\n\n  it should \"complete batched values with max retry limit\" in {\n    val p = Promise[Unit]()\n\n    val maxRetry = 3\n    val batchSize = 1\n    val concurrency = 1\n\n    var retryCount = new AtomicInteger(0)\n\n    def doStore(els: Seq[Int], retry: Int): Future[Seq[Int]] = {\n      val result = if (retry > 0) {\n        Future.failed(new Exception)\n      } else {\n        p.future.map(_ => els)\n      }\n\n      result.recoverWith {\n        case _ if retry > 0 =>\n          retryCount.incrementAndGet()\n          doStore(els, retry - 1)\n        case e =>\n          Future.failed(e)\n      }\n\n    }\n    val batcher = new Batcher[Int, Int](batchSize, concurrency, maxRetry)(doStore)\n\n    val values = List(1)\n    val results = values.map(batcher.put)\n\n    p.success(())\n\n    await(Future.sequence(results)) shouldBe values\n\n    retryCount.get() shouldBe maxRetry\n  }\n\n  it should \"complete batched values with the thrown exception with max retry limit\" in {\n    val p = Promise[Unit]()\n\n    val maxRetry = 3\n    val batchSize = 1\n    val concurrency = 1\n\n    val retryCount = new AtomicInteger(0)\n\n    def doStore(els: Seq[Int], retry: Int): Future[Seq[Int]] = {\n      val result = Future.failed(new Exception)\n\n      result.recoverWith {\n        case _ if retry > 0 =>\n          retryCount.incrementAndGet()\n          doStore(els, retry - 1)\n        case e =>\n          Future.failed(e)\n      }\n\n    }\n    val batcher = new Batcher[Int, Int](batchSize, concurrency, maxRetry)(doStore)\n\n    val r1 = batcher.put(1)\n\n    an[Exception] should be thrownBy await(r1)\n\n    retryCount.get() shouldBe maxRetry\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/CacheConcurrencyTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport scala.concurrent.duration.DurationInt\nimport java.util.concurrent.Executors\n\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport common.TestUtils._\nimport common._\nimport common.rest.WskRestOperations\nimport spray.json.JsString\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.utils.retry\n\nimport scala.concurrent.ExecutionContext\n\n@RunWith(classOf[JUnitRunner])\nclass CacheConcurrencyTests\n    extends AnyFlatSpec\n    with WskTestHelpers\n    with WskActorSystem\n    with BeforeAndAfterEach\n    with ConcurrencyHelpers {\n\n  val timeout = 5.minutes\n  println(s\"Running tests on # proc: ${Runtime.getRuntime.availableProcessors()}\")\n\n  implicit private val transId = TransactionId.testing\n  implicit private val wp = WskProps()\n  private val wsk = new WskRestOperations\n\n  val nExternalIters = 1\n  val nInternalIters = 5\n  val nThreads = nInternalIters * 30 // The maximum number of tasks running in parallel at any given time\n  val parallelismExecutionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(nThreads))\n\n  def run[W](phase: String)(block: String => W) =\n    concurrently((1 to nInternalIters), timeout) { i =>\n      val name = s\"testy${i}\"\n      withClue(s\"$phase: failed for $name\") { (name, block(name)) }\n    }(parallelismExecutionContext)\n\n  override def beforeEach() = {\n    run(\"pre-test sanitize\") { name =>\n      wsk.action.sanitize(name)\n    }\n  }\n\n  override def afterEach() = {\n    run(\"post-test sanitize\") { name =>\n      wsk.action.sanitize(name)\n    }\n  }\n\n  for (n <- 1 to nExternalIters)\n    \"the cache\" should s\"support concurrent CRUD without bogus residual cache entries, iter ${n}\" in {\n      val actionFile = TestUtils.getTestActionFilename(\"empty.js\")\n\n      run(\"create\") { name =>\n        wsk.action.create(name, Some(actionFile))\n      }\n\n      run(\"update\") { name =>\n        wsk.action.create(name, None, update = true)\n      }\n\n      run(\"delete+get\") { name =>\n        // run 30 operations in parallel: 15 get, 1 delete, 14 more get\n        concurrently((1 to 30), timeout) { i =>\n          if (i != 16) {\n            val rr = wsk.action.get(name, expectedExitCode = DONTCARE_EXIT)\n            withClue(s\"expecting get to either succeed or fail with not found: $rr\") {\n              // some will succeed and some should fail with not found\n              rr.exitCode should (be(SUCCESS_EXIT) or be(NOT_FOUND))\n            }\n          } else {\n            wsk.action.delete(name)\n          }\n        }(parallelismExecutionContext)\n      }\n\n      // Give some time to replicate the state between the controllers\n      retry(\n        {\n          // Check that every controller has the correct state (used round robin)\n          WhiskProperties.getControllerHosts.split(\",\").foreach { _ =>\n            run(\"get after delete\") { name =>\n              wsk.action.get(name, expectedExitCode = NotFound.intValue)\n            }\n          }\n        },\n        10,\n        Some(2.second))\n\n      run(\"recreate\") { name =>\n        wsk.action.create(name, Some(actionFile))\n      }\n\n      run(\"reupdate\") { name =>\n        wsk.action.create(name, None, parameters = Map(\"color\" -> JsString(\"red\")), update = true)\n      }\n\n      run(\"update+get\") { name =>\n        // run 30 operations in parallel: 15 get, 1 update, 14 more get\n        concurrently((1 to 30), timeout) { i =>\n          if (i != 16) {\n            val rr = wsk.action.get(name, expectedExitCode = DONTCARE_EXIT)\n            withClue(s\"expecting get to either succeed or fail with not found: $rr\") {\n              // some will succeed and some should fail with not found\n              rr.exitCode should (be(SUCCESS_EXIT) or be(NOT_FOUND))\n            }\n          } else {\n            wsk.action.create(name, None, parameters = Map(\"color\" -> JsString(\"blue\")), update = true)\n          }\n        }(parallelismExecutionContext)\n      }\n\n      // All controllers should have the correct action\n      // As they are used round robin, we ask every controller for the action.\n      // We add a retry to tollarate a short interval to bring the controllers in sync.\n      retry(\n        {\n          WhiskProperties.getControllerHosts.split(\",\").foreach { _ =>\n            run(\"get after update\") { name =>\n              wsk.action.get(name)\n            } map {\n              case (name, rr) =>\n                withClue(s\"get after update: failed check for $name\") {\n                  rr.stdout should include(\"blue\")\n                  rr.stdout should not include (\"red\")\n                }\n            }\n          }\n        },\n        10,\n        Some(2.second))\n    }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/CleanUpActivationsTest.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport java.io.File\nimport java.time.Instant\n\nimport scala.concurrent.duration.DurationInt\nimport scala.concurrent.duration.FiniteDuration\nimport scala.language.implicitConversions\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\nimport common.StreamLogging\nimport common.TestUtils\nimport common.WaitFor\nimport common.WhiskProperties\nimport common.WskActorSystem\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\n@RunWith(classOf[JUnitRunner])\nclass CleanUpActivationsTest\n    extends AnyFlatSpec\n    with Matchers\n    with ScalaFutures\n    with WskActorSystem\n    with WaitFor\n    with StreamLogging\n    with DatabaseScriptTestUtils {\n\n  val testDbPrefix = s\"cleanuptest_$dbPrefix\"\n  val cleanUpTool = WhiskProperties.getFileRelativeToWhiskHome(\"tools/db/cleanUpActivations.py\").getAbsolutePath\n  val designDocPath = WhiskProperties\n    .getFileRelativeToWhiskHome(\"ansible/files/activations_design_document_for_activations_db.json\")\n    .getAbsolutePath\n\n  implicit def toDuration(dur: FiniteDuration) = java.time.Duration.ofMillis(dur.toMillis)\n\n  /** Runs the clean up script to delete old activations */\n  def runCleanUpTool(dbUrl: DatabaseUrl, dbName: String, days: Int, docsPerRequest: Int = 200) = {\n    println(s\"Running clean up tool: ${dbUrl.safeUrl}, $dbName, $days, $docsPerRequest\")\n\n    val cmd = Seq(\n      python,\n      cleanUpTool,\n      \"--dbUrl\",\n      dbUrl.url,\n      \"--dbName\",\n      dbName,\n      \"--days\",\n      days.toString,\n      \"--docsPerRequest\",\n      docsPerRequest.toString)\n    val rr = TestUtils.runCmd(0, new File(\".\"), cmd: _*)\n  }\n\n  behavior of \"Database clean up script\"\n\n  it should \"delete old activations and keep new ones\" in {\n    // Create a database\n    val dbName = testDbPrefix + \"database_to_clean_old_and_keep_new\"\n    val client = createDatabase(dbName, Some(designDocPath))\n\n    println(s\"Creating testdocuments\")\n    val oldDocument =\n      JsObject(\"start\" -> Instant.now.minus(6.days).toEpochMilli.toJson, \"activationId\" -> \"abcde0123\".toJson)\n    val newDocument = JsObject(\"start\" -> Instant.now.toEpochMilli.toJson, \"activationId\" -> \"0123abcde\".toJson)\n    client.putDoc(\"testId1\", oldDocument).futureValue\n    client.putDoc(\"testId2\", newDocument).futureValue\n    waitForView(client, \"activations\", \"byDate\", 2)\n\n    // Trigger clean up script and verify that old document is deleted and the new one not\n    runCleanUpTool(dbUrl, dbName, 5)\n    val resultOld = client.getDoc(\"testId1\").futureValue\n    resultOld shouldBe 'left\n    resultOld.left.get shouldBe StatusCodes.NotFound\n\n    val resultNew = client.getDoc(\"testId2\").futureValue\n    resultNew shouldBe 'right\n    resultNew.right.get.fields\n      .filterNot(current => current._1 == \"_id\" || current._1 == \"_rev\")\n      .toJson shouldBe newDocument\n\n    // Remove created database\n    removeDatabase(dbName)\n  }\n\n  it should \"delete old activations in several iterations\" in {\n    // Create a database\n    val dbName = testDbPrefix + \"database_to_clean_in_iterations\"\n    val client = createDatabase(dbName, Some(designDocPath))\n    println(s\"Creating testdocuments\")\n    val ids = (1 to 5).map { current =>\n      client\n        .putDoc(\n          s\"testId_$current\",\n          JsObject(\n            \"start\" -> Instant.now.minus(2.days).toEpochMilli.toJson,\n            \"activationId\" -> s\"abcde0123$current\".toJson))\n        .futureValue\n      s\"testId_$current\"\n    }\n\n    val newDocument = JsObject(\"start\" -> Instant.now.toEpochMilli.toJson, \"activationId\" -> \"0123abcde\".toJson)\n    client.putDoc(\"testIdNew\", newDocument).futureValue\n    waitForView(client, \"activations\", \"byDate\", 6)\n\n    // Trigger clean up script and verify that old document is deleted and the new one not\n    runCleanUpTool(dbUrl, dbName, 1, 2)\n    ids.foreach { id =>\n      val resultOld = client.getDoc(id).futureValue\n      resultOld shouldBe 'left\n      resultOld.left.get shouldBe StatusCodes.NotFound\n    }\n\n    val resultNew = client.getDoc(\"testIdNew\").futureValue\n    resultNew shouldBe 'right\n    resultNew.right.get.fields\n      .filterNot(current => current._1 == \"_id\" || current._1 == \"_rev\")\n      .toJson shouldBe newDocument\n\n    // Remove created database\n    removeDatabase(dbName)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/CouchDbRestClientTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport scala.concurrent.Await\nimport scala.concurrent.Future\nimport scala.concurrent.Promise\nimport scala.concurrent.duration.DurationDouble\nimport scala.concurrent.duration.DurationInt\nimport scala.util._\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.actor.Props\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.stream.scaladsl._\nimport org.apache.pekko.util.ByteString\nimport common.StreamLogging\nimport common.WskActorSystem\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.CouchDbConfig\nimport org.apache.openwhisk.test.http.RESTProxy\n\n@RunWith(classOf[JUnitRunner])\nclass CouchDbRestClientTests\n    extends AnyFlatSpec\n    with Matchers\n    with ScalaFutures\n    with BeforeAndAfterAll\n    with WskActorSystem\n    with DbUtils\n    with StreamLogging {\n\n  override implicit val patienceConfig = PatienceConfig(timeout = 10.seconds, interval = 0.5.seconds)\n\n  private def someId(prefix: String): String = s\"${prefix}${Random.nextInt().abs}\"\n\n  val config = loadConfigOrThrow[CouchDbConfig](ConfigKeys.couchdb)\n\n  // We assume this DB does not exist.\n  val dbName = someId(\"whisk_test_db_\")\n\n  val client =\n    new ExtendedCouchDbRestClient(config.protocol, config.host, config.port, config.username, config.password, dbName)\n\n  override def beforeAll() = {\n    super.beforeAll()\n    whenReady(client.createDb()) { r =>\n      assert(r.isRight)\n    }\n  }\n\n  override def afterAll() = {\n    whenReady(client.deleteDb()) { r =>\n      assert(r.isRight)\n    }\n    super.afterAll()\n  }\n\n  def checkInstanceInfoResponse(response: Either[StatusCode, JsObject]): Unit = response match {\n    case Right(obj) =>\n      assert(obj.fields.contains(\"couchdb\"), \"response object doesn't contain 'couchdb'\")\n\n    case Left(code) =>\n      assert(false, s\"unsuccessful response (code ${code.intValue})\")\n  }\n\n  behavior of \"CouchDbRestClient\"\n\n  it should \"successfully access the DB instance info\" in {\n    assume(config.provider == \"Cloudant\" || config.provider == \"CouchDB\")\n    val f = client.instanceInfo()\n    whenReady(f) { e =>\n      checkInstanceInfoResponse(e)\n    }\n  }\n\n  it should \"successfully read and write documents containing unicode\" in {\n    val docId = someId(\"unicode_doc_\")\n    val doc = JsObject(\"winter\" -> JsString(\"❄ ☃ ❄\"))\n    val f1 = client.putDoc(docId, doc)\n\n    whenReady(f1) { e1 =>\n      assert(e1.isRight)\n\n      val f2 = client.getDoc(docId)\n      whenReady(f2) { e2 =>\n        assert(e2.isRight)\n        assert(JsObject(e2.right.get.fields.filter(_._1 == \"winter\")) === doc)\n      }\n    }\n  }\n\n  it should \"successfully write documents in bulk\" in {\n    val docs = (1 to 2).map(i => JsObject(\"_id\" -> someId(\"bulk\").toJson, \"int\" -> i.toJson))\n    client.putDocs(docs).futureValue shouldBe 'right\n\n    docs.foreach { doc =>\n      val dbDoc = retry[JsObject](\n        () =>\n          client\n            .getDoc(doc.fields(\"_id\").convertTo[String])\n            .collect({\n              case Right(doc) => doc\n            }),\n        dbOpTimeout).get\n\n      JsObject(dbDoc.fields - \"_rev\") shouldBe doc\n    }\n  }\n\n  it should \"bulk-write documents even if some of them fail\" in {\n    val ids = (1 to 2).map(_ => someId(\"failedBulk\"))\n    val docs = ids.map(id => JsObject(\"_id\" -> id.toJson))\n\n    val (firstId, firstDoc) = ids.zip(docs).head\n    client.putDoc(firstId, firstDoc).futureValue shouldBe 'right\n\n    val bulkResult = client.putDocs(docs).futureValue\n    bulkResult shouldBe 'right\n    bulkResult.right.get.compactPrint should include(\"conflict\")\n\n    // even though the bulk request contained a conflict, the other document was successfully written\n    val (secondId, secondDoc) = ids.zip(docs).last\n    val dbDoc = retry[JsObject](\n      () =>\n        client\n          .getDoc(secondId)\n          .collect({\n            case Right(doc) => doc\n          }),\n      dbOpTimeout).get\n\n    JsObject(dbDoc.fields - \"_rev\") shouldBe secondDoc\n  }\n\n  ignore /* it */ should \"successfully access the DB despite transient connection failures\" in {\n    assume(config.provider == \"Cloudant\" || config.provider == \"CouchDB\")\n\n    val dbAuthority = Uri.Authority(host = Uri.Host(config.host), port = config.port)\n\n    val proxyPort = 15975\n    val proxyActor =\n      actorSystem.actorOf(Props(new RESTProxy(\"0.0.0.0\", proxyPort)(dbAuthority, config.protocol == \"https\")))\n\n    val proxiedClient =\n      new ExtendedCouchDbRestClient(\"http\", \"localhost\", proxyPort, config.username, config.password, dbName)\n\n    // sprays the client with requests, makes sure they are all answered\n    // despite temporary connection failure.\n    val numRequests = 30\n    val timeSpan = 5.seconds\n    val delta = timeSpan / numRequests\n\n    val promises = Vector.fill(numRequests)(Promise[Either[StatusCode, JsObject]])\n\n    for (i <- 0 until numRequests) {\n      actorSystem.scheduler.scheduleOnce(delta * (i + 1)) {\n        proxiedClient.instanceInfo().andThen({ case r => promises(i).tryComplete(r) })\n      }\n    }\n\n    // Mayhem! Havoc!\n    actorSystem.scheduler.scheduleOnce(2.5.seconds, proxyActor, RESTProxy.UnbindFor(1.second))\n\n    // What a type!\n    val futures: Vector[Future[Try[Either[StatusCode, JsObject]]]] =\n      promises.map(_.future.map(e => Success(e)).recover { case t: Throwable => Failure(t) })\n\n    val results = Await.result(Future.sequence(futures), timeSpan * 2)\n\n    // We check that the first result was OK\n    // (i.e. the service worked before the disruption)\n    results.head.toOption shouldBe defined\n    checkInstanceInfoResponse(results.head.get)\n\n    // We check that the last result was OK\n    // (i.e. the service worked again after the disruption)\n    results.last.toOption shouldBe defined\n    checkInstanceInfoResponse(results.last.get)\n\n    // We check that there was at least one error\n    // (i.e. we did manage to unbind for a while)\n    results.find(_.isFailure) shouldBe defined\n  }\n\n  it should \"upload then download an attachment\" in {\n    assume(config.provider == \"Cloudant\" || config.provider == \"CouchDB\")\n\n    val docId = \"some_doc\"\n    val doc = JsObject(\"greeting\" -> JsString(\"hello\"))\n    val attachmentName = \"misc\"\n    val attachmentType = ContentTypes.`text/plain(UTF-8)`\n    val attachment = (\"\"\"\n            | This could have been poetry.\n            | But it isn't.\n        \"\"\").stripMargin\n\n    val attachmentSource = Source.single(ByteString.fromString(attachment))\n\n    val retrievalSink = Sink.fold[String, ByteString](\"\")((s, bs) => s + bs.decodeString(\"UTF-8\"))\n\n    val insertAndRetrieveResult: Future[(ContentType, String)] = for (docResponse <- client.putDoc(docId, doc);\n                                                                      Right(d) = docResponse;\n                                                                      rev1 = d.fields(\"rev\").convertTo[String];\n                                                                      attResponse <- client.putAttachment(\n                                                                        docId,\n                                                                        rev1,\n                                                                        attachmentName,\n                                                                        attachmentType,\n                                                                        attachmentSource);\n                                                                      Right(a) = attResponse;\n                                                                      rev2 = a.fields(\"rev\").convertTo[String];\n                                                                      retResponse <- client.getAttachment[String](\n                                                                        docId,\n                                                                        rev2,\n                                                                        attachmentName,\n                                                                        retrievalSink);\n                                                                      Right(pair) = retResponse) yield pair\n\n    whenReady(insertAndRetrieveResult) {\n      case (t, r) =>\n        assert(t === ContentTypes.`text/plain(UTF-8)`)\n        assert(r === attachment)\n    }\n  }\n\n  it should \"fail if group=true is used together with reduce=false\" in {\n    intercept[IllegalArgumentException] {\n      Await.result(client.executeView(\"\", \"\")(reduce = false, group = true), 15.seconds)\n    }\n  }\n\n  it should \"check group Parameter on view-execution\" in {\n    assume(config.provider == \"Cloudant\" || config.provider == \"CouchDB\")\n\n    val ids = List(\"some_doc_1\", \"some_doc_2\", \"some_doc_3\", \"some_doc_4\", \"some_doc_5\")\n    val docs = Map(\n      ids(0) -> JsObject(\"key\" -> JsString(\"a\"), \"value\" -> JsNumber(1)),\n      ids(1) -> JsObject(\"key\" -> JsString(\"a\"), \"value\" -> JsNumber(2)),\n      ids(2) -> JsObject(\"key\" -> JsString(\"b\"), \"value\" -> JsNumber(3)),\n      ids(3) -> JsObject(\"key\" -> JsString(\"b\"), \"value\" -> JsNumber(4)),\n      ids(4) -> JsObject(\"key\" -> JsString(\"c\"), \"value\" -> JsNumber(5)))\n    val designDocName = \"testDocument\"\n    val viewName = \"sumOfValues\"\n    val designDoc = JsObject(\n      \"views\" -> JsObject(viewName -> JsObject(\n        \"reduce\" -> JsString(\"_sum\"),\n        \"map\" -> JsString(\"function (doc) {\\n  if(doc.key && doc.value) {\\n    emit([doc.key], doc.value);\\n  }\\n}\"))),\n      \"language\" -> JsString(\"javascript\"))\n\n    Await.result(client.putDoc(s\"_design/$designDocName\", designDoc), 15.seconds)\n    docs.map {\n      case (id, doc) =>\n        Await.result(client.putDoc(id, doc), 15.seconds)\n    }\n\n    waitOnView(client, designDocName, viewName, docs.size)\n\n    val resultGroupedTrue =\n      Await.result(client.executeView(designDocName, viewName)(reduce = true, group = true), 15.seconds)\n    resultGroupedTrue should be('right)\n    val jsObjectTrue = resultGroupedTrue.right.get\n    var rows = jsObjectTrue.fields(\"rows\").convertTo[List[JsObject]]\n    rows.length should equal(3)\n    rows(0) shouldBe JsObject(\"key\" -> JsArray(JsString(\"a\")), \"value\" -> JsNumber(3))\n    rows(1) shouldBe JsObject(\"key\" -> JsArray(JsString(\"b\")), \"value\" -> JsNumber(7))\n    rows(2) shouldBe JsObject(\"key\" -> JsArray(JsString(\"c\")), \"value\" -> JsNumber(5))\n\n    val resultGroupedFalse =\n      Await.result(client.executeView(designDocName, viewName)(reduce = true, group = false), 15.seconds)\n    resultGroupedFalse should be('right)\n    val jsObjectFalse = resultGroupedFalse.right.get\n    rows = jsObjectFalse.fields(\"rows\").convertTo[List[JsObject]]\n    rows.length should equal(1)\n    rows(0).fields(\"value\") should equal(JsNumber(15))\n\n    val resultGroupedWithout = Await.result(client.executeView(designDocName, viewName)(reduce = true), 15.seconds)\n    resultGroupedWithout should be('right)\n    val jsObjectWithout = resultGroupedWithout.right.get\n    rows = jsObjectWithout.fields(\"rows\").convertTo[List[JsObject]]\n    rows.length should equal(1)\n    rows(0).fields(\"value\") should equal(JsNumber(15))\n\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/DatabaseScriptTestUtils.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport scala.concurrent.duration.DurationInt\nimport scala.io.Source\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.IntegrationPatience\nimport org.scalatest.concurrent.ScalaFutures\nimport org.apache.pekko.actor.ActorSystem\nimport common.WaitFor\nimport common.WhiskProperties\nimport pureconfig._\nimport pureconfig.generic.auto._\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.database.CouchDbRestClient\nimport org.apache.openwhisk.core.database.CouchDbConfig\n\ntrait DatabaseScriptTestUtils extends ScalaFutures with Matchers with WaitFor with IntegrationPatience {\n\n  case class DatabaseUrl(dbProtocol: String, dbUsername: String, dbPassword: String, dbHost: String, dbPort: String) {\n    def url = s\"$dbProtocol://$dbUsername:$dbPassword@$dbHost:$dbPort\"\n\n    def safeUrl = s\"$dbProtocol://$dbHost:$dbPort\"\n  }\n\n  val python = WhiskProperties.python\n  val config = loadConfigOrThrow[CouchDbConfig](ConfigKeys.couchdb)\n  val dbProtocol = config.protocol\n  val dbHost = config.host\n  val dbPort = config.port\n  val dbUsername = config.username\n  val dbPassword = config.password\n  val dbPrefix = WhiskProperties.getProperty(WhiskConfig.dbPrefix)\n  val dbUrl = DatabaseUrl(dbProtocol, dbUsername, dbPassword, dbHost, dbPort.toString)\n\n  def retry[T](task: => T) = org.apache.openwhisk.utils.retry(task, 10, Some(500.milliseconds))\n\n  /** Creates a new database with the given name */\n  def createDatabase(name: String, designDocPath: Option[String])(implicit as: ActorSystem, logging: Logging) = {\n    // Implicitly remove database for sanitization purposes\n    removeDatabase(name, ignoreFailure = true)\n\n    println(s\"Creating database: $name\")\n    val db = new ExtendedCouchDbRestClient(dbProtocol, dbHost, dbPort, dbUsername, dbPassword, name)\n    retry(db.createDb().futureValue shouldBe 'right)\n\n    retry {\n      val list = db.dbs().futureValue.right.get\n      list should contain(name)\n    }\n\n    designDocPath.map { path =>\n      val designDoc = Source.fromFile(path).mkString.parseJson.asJsObject\n      db.putDoc(designDoc.fields(\"_id\").convertTo[String], designDoc).futureValue\n    }\n\n    db\n  }\n\n  /** Wait for database to appear */\n  def waitForDatabase(dbName: String)(implicit as: ActorSystem, logging: Logging) = {\n    val client = new ExtendedCouchDbRestClient(dbProtocol, dbHost, dbPort, dbUsername, dbPassword, dbName)\n    waitfor(() => {\n      client.getAllDocs(includeDocs = Some(true)).futureValue.isRight\n    })\n    client\n  }\n\n  /** Removes the database with the given name */\n  def removeDatabase(name: String, ignoreFailure: Boolean = false)(implicit as: ActorSystem, logging: Logging) = {\n    println(s\"Removing database: $name\")\n    val db = new ExtendedCouchDbRestClient(dbProtocol, dbHost, dbPort, dbUsername, dbPassword, name)\n    retry {\n      val delete = db.deleteDb().futureValue\n      if (!ignoreFailure) delete shouldBe 'right\n    }\n    db\n  }\n\n  /** Wait for a document to appear */\n  def waitForDocument(client: ExtendedCouchDbRestClient, id: String) =\n    waitfor(() => client.getDoc(id).futureValue.isRight)\n\n  /** Get all docs within one database */\n  def getAllDocs(dbName: String)(implicit as: ActorSystem, logging: Logging) = {\n    val client = new ExtendedCouchDbRestClient(dbProtocol, dbHost, dbPort, dbUsername, dbPassword, dbName)\n    val documents = client.getAllDocs(includeDocs = Some(true)).futureValue\n    documents shouldBe 'right\n    documents.right.get\n  }\n\n  /** wait until all documents are processed by the view */\n  def waitForView(db: CouchDbRestClient, designDoc: String, viewName: String, numDocuments: Int) = {\n    waitfor(() => {\n      val view = db.executeView(designDoc, viewName)().futureValue\n      view shouldBe 'right\n      view.right.get.fields(\"rows\").convertTo[List[JsObject]].length == numDocuments\n    }, totalWait = 2.minutes)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/DbUtils.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport java.util.Base64\nimport java.util.concurrent.TimeoutException\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.apache.pekko.http.scaladsl.model.ContentType\nimport org.apache.pekko.stream.scaladsl.Source\nimport org.apache.pekko.util.ByteString\nimport org.scalatest.Assertions\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.database.memory.MemoryArtifactStore\nimport org.apache.openwhisk.core.entity.Attachments.Attached\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types.{AuthStore, EntityStore}\n\nimport scala.collection.mutable.ListBuffer\nimport scala.concurrent.{Await, ExecutionContext, Future}\nimport scala.concurrent.duration.{Duration, DurationInt}\nimport scala.language.postfixOps\nimport scala.util.{Failure, Random, Success, Try}\n\n/**\n * WARNING: the put/get/del operations in this trait operate directly on the datastore,\n * and in the presence of a cache, there will be inconsistencies if one mixes these\n * operations with those that flow through the cache. To mitigate this, use unique asset\n * names in tests, and defer all cleanup to the end of a test suite.\n */\ntrait DbUtils extends Assertions {\n  implicit val dbOpTimeout = 15 seconds\n  val instance = ControllerInstanceId(\"0\")\n  val docsToDelete = ListBuffer[(ArtifactStore[_], DocInfo)]()\n  case class RetryOp() extends Throwable\n\n  val cnt = new AtomicInteger(0)\n  def transid() = TransactionId(cnt.incrementAndGet().toString)\n\n  // Call each few successfully 5 before the test continues to increase probability, that each node of the\n  // CouchDB/Cloudant cluster is updated.\n  val successfulViewCalls = 5\n\n  /**\n   * Retry an operation 'step()' awaiting its result up to 'timeout'.\n   * Attempt the operation up to 'count' times. The future from the\n   * step is not aborted --- TODO fix this.\n   */\n  def retry[T](step: () => Future[T], timeout: Duration, count: Int = 100): Try[T] = {\n    val graceBeforeRetry = 50.milliseconds\n    val future = step()\n    if (count > 0) try {\n      val result = Await.result(future, timeout)\n      Success(result)\n    } catch {\n      case n: NoDocumentException =>\n        println(\"no document exception, retrying\")\n        Thread.sleep(graceBeforeRetry.toMillis)\n        retry(step, timeout, count - 1)\n      case RetryOp() =>\n        println(\"condition not met, retrying\")\n        Thread.sleep(graceBeforeRetry.toMillis)\n        retry(step, timeout, count - 1)\n      case t: TimeoutException =>\n        println(\"timed out, retrying\")\n        Thread.sleep(graceBeforeRetry.toMillis)\n        retry(step, timeout, count - 1)\n      case t: Throwable =>\n        println(s\"unexpected failure $t\")\n        Failure(t)\n    } else Failure(new NoDocumentException(\"timed out\"))\n  }\n\n  /**\n   * Wait on a view to update with documents added to namespace. This uses retry above,\n   * where the step performs a direct db query to retrieve the view and check the count\n   * matches the given value.\n   */\n  def waitOnView[Au](db: ArtifactStore[Au], namespace: EntityName, count: Int, view: View)(\n    implicit context: ExecutionContext,\n    transid: TransactionId,\n    timeout: Duration): Unit =\n    waitOnViewImpl(db, List(namespace.asString), List(namespace.asString, WhiskQueries.TOP), count, view)\n\n  /**\n   * Wait on a view to update with documents added to namespace. This uses retry above,\n   * where the step performs a direct db query to retrieve the view and check the count\n   * matches the given value.\n   */\n  def waitOnView[Au](db: ArtifactStore[Au], path: EntityPath, count: Int, view: View)(\n    implicit context: ExecutionContext,\n    transid: TransactionId,\n    timeout: Duration): Unit =\n    waitOnViewImpl(db, List(path.asString), List(path.asString, WhiskQueries.TOP), count, view)\n\n  /**\n   * Wait on a view to update with documents added(don't specify the namespace). This uses retry above,\n   * where the step performs a direct db query to retrieve the view and check the count\n   * matches the given value.\n   */\n  def waitOnView[Au](db: ArtifactStore[Au], count: Int, view: View)(implicit context: ExecutionContext,\n                                                                    transid: TransactionId,\n                                                                    timeout: Duration): Unit =\n    waitOnViewImpl(db, List.empty, List.empty, count, view)\n\n  /**\n   * Wait on a view to update with documents added to namespace. This uses retry above,\n   * where the step performs a direct db query to retrieve the view and check the count\n   * matches the given value.\n   */\n  private def waitOnViewImpl[Au](\n    db: ArtifactStore[Au],\n    startKey: List[String],\n    endKey: List[String],\n    count: Int,\n    view: View)(implicit context: ExecutionContext, transid: TransactionId, timeout: Duration): Unit = {\n    // Query the view at least `successfulViewCalls` times successfully, to handle inconsistency between several CouchDB-nodes.\n    (0 until successfulViewCalls).map { _ =>\n      val success = retry(() => {\n        db.query(view.name, startKey, endKey, 0, 0, false, true, false, StaleParameter.No) map { l =>\n          if (l.length != count) {\n            throw RetryOp()\n          } else true\n        }\n      }, timeout)\n      assert(success.isSuccess, \"wait aborted\")\n    }\n  }\n\n  /**\n   * Wait on a view specific to a collection to update with documents added to that collection in namespace.\n   * This uses retry above, where the step performs a collection-specific view query using the collection\n   * factory. The result count from the view is checked against the given value.\n   */\n  def waitOnView(\n    db: EntityStore,\n    factory: WhiskEntityQueries[_],\n    namespace: EntityPath,\n    count: Int,\n    includeDocs: Boolean = false)(implicit context: ExecutionContext, transid: TransactionId, timeout: Duration) = {\n    // Query the view at least `successfulViewCalls` times successfully, to handle inconsistency between several CouchDB-nodes.\n    (0 until successfulViewCalls).map { _ =>\n      val success = retry(() => {\n        factory.listCollectionInNamespace(db, namespace, 0, 0, includeDocs) map { l =>\n          if (l.fold(_.length, _.length) < count) {\n            throw RetryOp()\n          } else true\n        }\n      }, timeout)\n      assert(success.isSuccess, \"wait aborted\")\n    }\n  }\n\n  /**\n   * Wait on view for the authentication table. This is like the other waitOnViews but\n   * specific to the WhiskAuth records.\n   */\n  def waitOnView(db: AuthStore, authkey: BasicAuthenticationAuthKey, count: Int)(implicit context: ExecutionContext,\n                                                                                 transid: TransactionId,\n                                                                                 timeout: Duration) = {\n    // Query the view at least `successfulViewCalls` times successfully, to handle inconsistency between several CouchDB-nodes.\n    (0 until successfulViewCalls).map { _ =>\n      val success = retry(() => {\n        Identity.list(db, List(authkey.uuid.asString, authkey.key.asString)) map { l =>\n          if (l.length != count) {\n            throw RetryOp()\n          } else true\n        }\n      }, timeout)\n      assert(success.isSuccess, \"wait aborted after: \" + timeout + \": \" + success)\n    }\n  }\n\n  /**\n   * Wait on view using the CouchDbRestClient. This is like the other waitOnViews.\n   */\n  def waitOnView(db: CouchDbRestClient, designDocName: String, viewName: String, count: Int)(\n    implicit context: ExecutionContext,\n    timeout: Duration) = {\n    // Query the view at least `successfulViewCalls` times successfully, to handle inconsistency between several CouchDB-nodes.\n    (0 until successfulViewCalls).map { _ =>\n      val success = retry(\n        () => {\n          db.executeView(designDocName, viewName)().map {\n            case Right(doc) =>\n              val length = doc.fields(\"rows\").convertTo[List[JsObject]].length\n              if (length != count) {\n                throw RetryOp()\n              } else true\n            case Left(_) =>\n              throw RetryOp()\n          }\n        },\n        timeout)\n      assert(success.isSuccess, \"wait aborted after: \" + timeout + \": \" + success)\n    }\n  }\n\n  /**\n   * Puts document 'w' in datastore, and add it to gc queue to delete after the test completes.\n   */\n  def put[A, Au >: A](db: ArtifactStore[Au], w: A, garbageCollect: Boolean = true)(\n    implicit transid: TransactionId,\n    timeout: Duration = 10 seconds): DocInfo = {\n    val docFuture = db.put(w)\n    val doc = Await.result(docFuture, timeout)\n    assert(doc != null)\n    if (garbageCollect) docsToDelete += ((db, doc))\n    doc\n  }\n\n  def putAndAttach[A <: DocumentRevisionProvider, Au >: A](\n    db: ArtifactStore[Au],\n    doc: A,\n    update: (A, Attached) => A,\n    contentType: ContentType,\n    docStream: Source[ByteString, _],\n    oldAttachment: Option[Attached],\n    garbageCollect: Boolean = true)(implicit transid: TransactionId, timeout: Duration = 10 seconds): DocInfo = {\n    val docFuture = db.putAndAttach[A](doc, update, contentType, docStream, oldAttachment)\n    val newDoc = Await.result(docFuture, timeout)._1\n    assert(newDoc != null)\n    if (garbageCollect) docsToDelete += ((db, newDoc))\n    newDoc\n  }\n\n  /**\n   * Gets document by id from datastore, and add it to gc queue to delete after the test completes.\n   */\n  def get[A <: DocumentRevisionProvider, Au >: A](db: ArtifactStore[Au],\n                                                  docid: DocId,\n                                                  factory: DocumentFactory[A],\n                                                  garbageCollect: Boolean = true)(implicit transid: TransactionId,\n                                                                                  timeout: Duration = 10 seconds,\n                                                                                  ma: Manifest[A]): A = {\n    val docFuture = factory.get(db, docid)\n    val doc = Await.result(docFuture, timeout)\n    assert(doc != null)\n    if (garbageCollect) docsToDelete += ((db, docid.asDocInfo))\n    doc\n  }\n\n  /**\n   * Deletes document by id from datastore.\n   */\n  def del[A <: WhiskDocument, Au >: A](db: ArtifactStore[Au], docid: DocId, factory: DocumentFactory[A])(\n    implicit transid: TransactionId,\n    timeout: Duration = 10 seconds,\n    ma: Manifest[A]) = {\n    val docFuture = factory.get(db, docid)\n    val doc = Await.result(docFuture, timeout)\n    assert(doc != null)\n    Await.result(db.del(doc.docinfo), timeout)\n  }\n\n  /**\n   * Deletes document by id and revision from datastore.\n   */\n  def delete(db: ArtifactStore[_], docinfo: DocInfo)(implicit transid: TransactionId,\n                                                     timeout: Duration = 10 seconds) = {\n    Await.result(db.del(docinfo), timeout)\n  }\n\n  /**\n   * Puts a document 'entity' into the datastore, then do a get to retrieve it and confirm the identity.\n   */\n  def putGetCheck[A <: DocumentRevisionProvider, Au >: A](db: ArtifactStore[Au],\n                                                          entity: A,\n                                                          factory: DocumentFactory[A],\n                                                          gc: Boolean = true)(implicit transid: TransactionId,\n                                                                              timeout: Duration = 10 seconds,\n                                                                              ma: Manifest[A]): (DocInfo, A) = {\n    val doc = put(db, entity, gc)\n    assert(doc != null && doc.id.asString != null && doc.rev.asString != null)\n    val future = factory.get(db, doc.id, doc.rev)\n    val dbEntity = Await.result(future, timeout)\n    assert(dbEntity != null)\n    assert(dbEntity == entity)\n    (doc, dbEntity)\n  }\n\n  /**\n   * Deletes all documents added to gc queue.\n   */\n  def cleanup()(implicit timeout: Duration = 10 seconds) = {\n    docsToDelete.map { e =>\n      Try {\n        Await.result(e._1.del(e._2)(TransactionId.testing), timeout)\n        Await.result(e._1.deleteAttachments(e._2)(TransactionId.testing), timeout)\n      }\n    }\n    docsToDelete.clear()\n  }\n\n  /**\n   * Generates a Base64 string for code which would not be inlined by the ArtifactStore\n   */\n  def nonInlinedCode(db: ArtifactStore[_]): String = {\n    encodedRandomBytes(nonInlinedAttachmentSize(db))\n  }\n\n  /**\n   * Size in bytes for attachments which would always be inlined.\n   */\n  def inlinedAttachmentSize(db: ArtifactStore[_]): Int = {\n    db match {\n      case inliner: AttachmentSupport[_] =>\n        inliner.maxInlineSize.toBytes.toInt - 1\n      case _ =>\n        throw new IllegalStateException(s\"ArtifactStore does not support attachment inlining $db\")\n    }\n  }\n\n  /**\n   * Size in bytes for attachments which would never be inlined.\n   */\n  def nonInlinedAttachmentSize(db: ArtifactStore[_]): Int = {\n    db match {\n      case inliner: AttachmentSupport[_] =>\n        inliner.maxInlineSize.toBytes.toInt * 2\n      case _ =>\n        42\n    }\n  }\n\n  def assumeAttachmentInliningEnabled(db: ArtifactStore[_]): Unit = {\n    assume(inlinedAttachmentSize(db) > 0, \"Attachment inlining is disabled\")\n  }\n\n  protected def encodedRandomBytes(size: Int): String = Base64.getEncoder.encodeToString(randomBytes(size))\n\n  def isMemoryStore(store: ArtifactStore[_]): Boolean = store.isInstanceOf[MemoryArtifactStore[_]]\n  def isCouchStore(store: ArtifactStore[_]): Boolean = store.isInstanceOf[CouchDbRestStore[_]]\n\n  protected def removeFromCache[A <: DocumentRevisionProvider](entity: WhiskEntity, factory: DocumentFactory[A])(\n    implicit ec: ExecutionContext): Unit = {\n    factory.removeId(CacheKey(entity))\n  }\n\n  protected def randomBytes(size: Int): Array[Byte] = {\n    val arr = new Array[Byte](size)\n    Random.nextBytes(arr)\n    arr\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/DocumentHandlerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport common.WskActorSystem\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.OptionValues\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.SubjectHandler.SubjectView\nimport org.apache.openwhisk.core.database.WhisksHandler.ROOT_NS\nimport org.apache.openwhisk.core.database._\nimport org.apache.openwhisk.core.entity._\n\nimport scala.concurrent.Future\n\n@RunWith(classOf[JUnitRunner])\nclass DocumentHandlerTests extends AnyFlatSpec with Matchers with ScalaFutures with OptionValues with WskActorSystem {\n\n  val cnt = new AtomicInteger(0)\n  def transid() = TransactionId(cnt.incrementAndGet().toString)\n\n  behavior of \"WhisksHandler computeFields\"\n\n  it should \"return empty object when namespace does not exist\" in {\n    WhisksHandler.computedFields(JsObject.empty) shouldBe JsObject.empty\n  }\n\n  it should \"return JsObject when namespace is simple name\" in {\n    WhisksHandler.computedFields(JsObject((\"namespace\", JsString(\"foo\")))) shouldBe JsObject((ROOT_NS, JsString(\"foo\")))\n    WhisksHandler.computedFields(newRule(\"foo\").toDocumentRecord) shouldBe JsObject((ROOT_NS, JsString(\"foo\")))\n  }\n\n  it should \"return JsObject when namespace is path\" in {\n    WhisksHandler.computedFields(JsObject((\"namespace\", JsString(\"foo/bar\")))) shouldBe\n      JsObject((ROOT_NS, JsString(\"foo\")))\n\n    WhisksHandler.computedFields(newRule(\"foo/bar\").toDocumentRecord) shouldBe JsObject((ROOT_NS, JsString(\"foo\")))\n  }\n\n  private def newRule(ns: String): WhiskRule = {\n    WhiskRule(\n      EntityPath(ns),\n      EntityName(\"foo\"),\n      FullyQualifiedEntityName(EntityPath(\"system\"), EntityName(\"bar\")),\n      FullyQualifiedEntityName(EntityPath(\"system\"), EntityName(\"bar\")))\n  }\n\n  behavior of \"WhisksHandler computeView\"\n\n  it should \"include only common fields in trigger view\" in {\n    val js = \"\"\"{\n               |  \"namespace\" : \"foo\",\n               |  \"version\" : 5,\n               |  \"end\"   : 9,\n               |  \"cause\" : 204\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n                   |  \"namespace\" : \"foo\",\n                   |  \"version\" : 5\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    WhisksHandler.computeView(\"foo\", \"triggers\", js) shouldBe result\n  }\n\n  it should \"include false binding in public package view\" in {\n    val js =\n      \"\"\"{\n        |  \"namespace\" : \"foo\",\n        |  \"version\" : 5,\n        |  \"binding\"   : {\"foo\" : \"bar\"},\n        |  \"cause\" : 204\n        |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result =\n      \"\"\"{\n        |  \"namespace\" : \"foo\",\n        |  \"version\" : 5,\n        |  \"binding\" : false\n        |}\"\"\".stripMargin.parseJson.asJsObject\n    WhisksHandler.computeView(\"foo\", \"packages-public\", js) shouldBe result\n  }\n\n  it should \"include actual binding in package view\" in {\n    val js = \"\"\"{\n               |  \"namespace\" : \"foo\",\n               |  \"version\" : 5,\n               |  \"binding\"   : {\"foo\" : \"bar\"},\n               |  \"cause\" : 204\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n                   |  \"namespace\" : \"foo\",\n                   |  \"version\" : 5,\n                   |  \"binding\" : {\"foo\" : \"bar\"}\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    WhisksHandler.computeView(\"foo\", \"packages\", js) shouldBe result\n  }\n\n  it should \"include limits and binary info in action view\" in {\n    val js = \"\"\"{\n               |  \"namespace\" : \"foo\",\n               |  \"version\" : 5,\n               |  \"binding\"   : {\"foo\" : \"bar\"},\n               |  \"limits\" : 204,\n               |  \"exec\" : {\"binary\" : true }\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n                   |  \"namespace\" : \"foo\",\n                   |  \"version\" : 5,\n                   |  \"limits\" : 204,\n                   |  \"exec\" : { \"binary\" : true }\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    WhisksHandler.computeView(\"foo\", \"actions\", js) shouldBe result\n  }\n\n  it should \"include binary as false when exec missing\" in {\n    val js = \"\"\"{\n               |  \"namespace\" : \"foo\",\n               |  \"version\" : 5,\n               |  \"binding\"   : {\"foo\" : \"bar\"},\n               |  \"limits\" : 204\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n                   |  \"namespace\" : \"foo\",\n                   |  \"version\" : 5,\n                   |  \"limits\" : 204,\n                   |  \"exec\" : { \"binary\" : false }\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    WhisksHandler.computeView(\"foo\", \"actions\", js) shouldBe result\n  }\n\n  it should \"include binary as false when exec does not have binary prop\" in {\n    val js = \"\"\"{\n               |  \"namespace\" : \"foo\",\n               |  \"version\" : 5,\n               |  \"binding\"   : {\"foo\" : \"bar\"},\n               |  \"limits\" : 204,\n               |  \"exec\" : { \"code\" : \"stuff\" }\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n                   |  \"namespace\" : \"foo\",\n                   |  \"version\" : 5,\n                   |  \"limits\" : 204,\n                   |  \"exec\" : { \"binary\" : false }\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    WhisksHandler.computeView(\"foo\", \"actions\", js) shouldBe result\n  }\n\n  behavior of \"WhisksHandler fieldsRequiredForView\"\n\n  it should \"match the expected field names\" in {\n    WhisksHandler.fieldsRequiredForView(\"foo\", \"actions\") shouldBe\n      Set(\"namespace\", \"name\", \"version\", \"publish\", \"annotations\", \"updated\", \"limits\", \"exec.binary\")\n\n    WhisksHandler.fieldsRequiredForView(\"foo\", \"packages\") shouldBe\n      Set(\"namespace\", \"name\", \"version\", \"publish\", \"annotations\", \"updated\", \"binding\")\n\n    WhisksHandler.fieldsRequiredForView(\"foo\", \"packages-public\") shouldBe\n      Set(\"namespace\", \"name\", \"version\", \"publish\", \"annotations\", \"updated\")\n\n    WhisksHandler.fieldsRequiredForView(\"foo\", \"rules\") shouldBe\n      Set(\"namespace\", \"name\", \"version\", \"publish\", \"annotations\", \"updated\")\n\n    WhisksHandler.fieldsRequiredForView(\"foo\", \"triggers\") shouldBe\n      Set(\"namespace\", \"name\", \"version\", \"publish\", \"annotations\", \"updated\")\n\n    intercept[UnsupportedView] {\n      WhisksHandler.fieldsRequiredForView(\"foo\", \"unknown\") shouldBe Set.empty\n    }\n  }\n\n  behavior of \"ActivationHandler computeFields\"\n\n  it should \"return default value when no annotation found\" in {\n    val js = \"\"\"{\"foo\" : \"bar\"}\"\"\".parseJson.asJsObject\n    ActivationHandler.annotationValue(js, \"fooKey\", { _.convertTo[String] }, \"barValue\") shouldBe \"barValue\"\n\n    val js2 = \"\"\"{\"foo\" : \"bar\", \"annotations\" : \"a\"}\"\"\".parseJson.asJsObject\n    ActivationHandler.annotationValue(js2, \"fooKey\", { _.convertTo[String] }, \"barValue\") shouldBe \"barValue\"\n\n    val js3 = \"\"\"{\"foo\" : \"bar\", \"annotations\" : [{\"key\" : \"someKey\", \"value\" : \"someValue\"}]}\"\"\".parseJson.asJsObject\n    ActivationHandler.annotationValue(js3, \"fooKey\", { _.convertTo[String] }, \"barValue\") shouldBe \"barValue\"\n  }\n\n  it should \"return transformed value when annotation found\" in {\n    val js = \"\"\"{\n               |  \"foo\": \"bar\",\n               |  \"annotations\": [\n               |    {\n               |      \"key\": \"fooKey\",\n               |      \"value\": \"fooValue\"\n               |    }\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.annotationValue(js, \"fooKey\", { _.convertTo[String] + \"-x\" }, \"barValue\") shouldBe \"fooValue-x\"\n  }\n\n  it should \"computeFields with deleteLogs true\" in {\n    val js = \"\"\"{\n               |  \"foo\": \"bar\",\n               |  \"annotations\": [\n               |    {\n               |      \"key\": \"fooKey\",\n               |      \"value\": \"fooValue\"\n               |    }\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computedFields(js) shouldBe \"\"\"{\"deleteLogs\" : true}\"\"\".parseJson\n  }\n\n  it should \"computeFields with deleteLogs false for sequence kind\" in {\n    val js = \"\"\"{\n               |  \"foo\": \"bar\",\n               |  \"annotations\": [\n               |    {\n               |      \"key\": \"kind\",\n               |      \"value\": \"sequence\"\n               |    }\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computedFields(js) shouldBe \"\"\"{\"deleteLogs\" : false}\"\"\".parseJson\n  }\n\n  it should \"computeFields with nspath as namespace\" in {\n    val js = \"\"\"{\n               |  \"namespace\": \"foons\",\n               |  \"name\":\"bar\",\n               |  \"annotations\": [\n               |    {\n               |      \"key\": \"kind\",\n               |      \"value\": \"action\"\n               |    }\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computedFields(js) shouldBe \"\"\"{\"nspath\": \"foons/bar\", \"deleteLogs\" : true}\"\"\".parseJson\n  }\n\n  it should \"computeFields with nspath as qualified path\" in {\n    val js = \"\"\"{\n               |  \"namespace\": \"foons\",\n               |  \"name\":\"bar\",\n               |  \"annotations\": [\n               |    {\n               |      \"key\": \"path\",\n               |      \"value\": \"barns/barpkg/baraction\"\n               |    }\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computedFields(js) shouldBe \"\"\"{\"nspath\": \"foons/barpkg/bar\", \"deleteLogs\" : true}\"\"\".parseJson\n  }\n\n  it should \"computeFields with nspath as namespace when path value is simple name\" in {\n    val js = \"\"\"{\n               |  \"namespace\": \"foons\",\n               |  \"name\":\"bar\",\n               |  \"annotations\": [\n               |    {\n               |      \"key\": \"path\",\n               |      \"value\": \"baraction\"\n               |    }\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computedFields(js) shouldBe \"\"\"{\"nspath\": \"foons/bar\", \"deleteLogs\" : true}\"\"\".parseJson\n  }\n\n  behavior of \"ActivationHandler computeActivationView\"\n\n  it should \"include only listed fields\" in {\n    val js = \"\"\"{\n               |  \"namespace\" : \"foo\",\n               |  \"extra\" : false,\n               |  \"cause\" : 204\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n               |  \"namespace\" : \"foo\",\n               |  \"cause\" : 204\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computeView(\"foo\", \"activations\", js) shouldBe result\n  }\n\n  it should \"include duration when end is non zero\" in {\n    val js = \"\"\"{\n               |  \"namespace\" : \"foo\",\n               |  \"start\" : 5,\n               |  \"end\"   : 9,\n               |  \"cause\" : 204\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n                   |  \"namespace\" : \"foo\",\n                   |  \"start\" : 5,\n                   |  \"end\"   : 9,\n                   |  \"duration\" : 4,\n                   |  \"cause\" : 204\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computeView(\"foo\", \"activations\", js) shouldBe result\n  }\n\n  it should \"not include duration when end is zero\" in {\n    val js = \"\"\"{\n               |  \"namespace\" : \"foo\",\n               |  \"start\" : 5,\n               |  \"end\"   : 0,\n               |  \"cause\" : 204\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n                   |  \"namespace\" : \"foo\",\n                   |  \"start\" : 5,\n                   |  \"cause\" : 204\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computeView(\"foo\", \"activations\", js) shouldBe result\n  }\n\n  it should \"include statusCode\" in {\n    val js = \"\"\"{\n               |  \"namespace\": \"foo\",\n               |  \"response\": {\"statusCode\" : 404}\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n                   |  \"namespace\": \"foo\",\n                   |  \"statusCode\" : 404\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computeView(\"foo\", \"activations\", js) shouldBe result\n  }\n\n  it should \"not include statusCode\" in {\n    val js = \"\"\"{\n               |  \"namespace\": \"foo\",\n               |  \"response\": {\"status\" : 404}\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val result = \"\"\"{\n                   |  \"namespace\": \"foo\"\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    ActivationHandler.computeView(\"foo\", \"activations\", js) shouldBe result\n  }\n\n  behavior of \"ActivationHandler fieldsRequiredForView\"\n\n  it should \"match the expected field names\" in {\n    ActivationHandler.fieldsRequiredForView(\"foo\", \"activations\") shouldBe\n      Set(\n        \"namespace\",\n        \"name\",\n        \"version\",\n        \"publish\",\n        \"annotations\",\n        \"activationId\",\n        \"start\",\n        \"cause\",\n        \"end\",\n        \"response.statusCode\")\n  }\n\n  it should \"throw UnsupportedView exception\" in {\n    intercept[UnsupportedView] {\n      ActivationHandler.fieldsRequiredForView(\"foo\", \"unknown\")\n    }\n  }\n\n  behavior of \"SubjectHandler computeSubjectView\"\n\n  it should \"match subject with namespace\" in {\n    val js = \"\"\"{\n               |  \"subject\": \"foo\",\n               |  \"uuid\": \"u1\",\n               |  \"key\" : \"k1\"\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    SubjectHandler.findMatchingSubject(List(\"foo\"), js).value shouldBe SubjectView(\"foo\", \"u1\", \"k1\")\n  }\n\n  it should \"match subject with child namespace\" in {\n    val js = \"\"\"{\n               |  \"subject\": \"bar\",\n               |  \"uuid\": \"u1\",\n               |  \"key\" : \"k1\",\n               |  \"namespaces\" : [\n               |    {\"name\": \"foo\", \"uuid\":\"u2\", \"key\":\"k2\"}\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    SubjectHandler.findMatchingSubject(List(\"foo\"), js).value shouldBe\n      SubjectView(\"foo\", \"u2\", \"k2\", matchInNamespace = true)\n  }\n\n  it should \"match subject with uuid and key\" in {\n    val js = \"\"\"{\n               |  \"subject\": \"foo\",\n               |  \"uuid\": \"u1\",\n               |  \"key\" : \"k1\"\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    SubjectHandler.findMatchingSubject(List(\"u1\", \"k1\"), js).value shouldBe\n      SubjectView(\"foo\", \"u1\", \"k1\")\n  }\n\n  it should \"match subject with child namespace with uuid and key\" in {\n    val js = \"\"\"{\n               |  \"subject\": \"bar\",\n               |  \"uuid\": \"u1\",\n               |  \"key\" : \"k1\",\n               |  \"namespaces\" : [\n               |    {\"name\": \"foo\", \"uuid\":\"u2\", \"key\":\"k2\"}\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    SubjectHandler.findMatchingSubject(List(\"u2\", \"k2\"), js).value shouldBe\n      SubjectView(\"foo\", \"u2\", \"k2\", matchInNamespace = true)\n  }\n\n  it should \"throw exception when namespace match but key missing\" in {\n    val js = \"\"\"{\n               |  \"subject\": \"foo\",\n               |  \"uuid\": \"u1\",\n               |  \"blocked\" : true\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    SubjectHandler.findMatchingSubject(List(\"foo\"), js) shouldBe empty\n  }\n\n  behavior of \"SubjectHandler transformViewResult\"\n\n  it should \"json should match format of CouchDB response\" in {\n    implicit val tid: TransactionId = transid()\n    val js = \"\"\"{\n               |  \"_id\": \"bar\",\n               |  \"subject\": \"bar\",\n               |  \"uuid\": \"u1\",\n               |  \"key\" : \"k1\",\n               |  \"namespaces\" : [\n               |    {\"name\": \"foo\", \"uuid\":\"u2\", \"key\":\"k2\"}\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    val result = \"\"\"{\n                   |  \"id\": \"bar\",\n                   |  \"key\": [\n                   |    \"u2\",\n                   |    \"k2\"\n                   |  ],\n                   |  \"value\": {\n                   |    \"_id\": \"foo/limits\",\n                   |    \"namespace\": \"foo\",\n                   |    \"uuid\": \"u2\",\n                   |    \"key\": \"k2\"\n                   |  },\n                   |  \"doc\": null\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    val queryKey = List(\"u2\", \"k2\")\n    SubjectHandler\n      .transformViewResult(\"subjects\", \"identities\", queryKey, queryKey, includeDocs = true, js, TestDocumentProvider())\n      .futureValue shouldBe Seq(result)\n  }\n\n  it should \"should return none when passed object does not passes view criteria\" in {\n    implicit val tid: TransactionId = transid()\n    val js = \"\"\"{\n               |  \"_id\": \"bar\",\n               |  \"subject\": \"bar\",\n               |  \"uuid\": \"u1\",\n               |  \"key\" : \"k1\",\n               |  \"namespaces\" : [\n               |    {\"name\": \"foo\", \"uuid\":\"u2\", \"key\":\"k2\"},\n               |    {\"name\": \"foo2\", \"uuid\":\"u3\", \"key\":\"k3\"}\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    val queryKey = List(\"u2\", \"k3\")\n    SubjectHandler\n      .transformViewResult(\"subjects\", \"identities\", queryKey, queryKey, includeDocs = true, js, TestDocumentProvider())\n      .futureValue shouldBe empty\n  }\n\n  behavior of \"SubjectHandler blacklisted namespaces\"\n\n  it should \"match limits with 0 concurrentInvocations\" in {\n    val js = \"\"\"{\n               |  \"_id\": \"bar/limits\",\n               |  \"concurrentInvocations\": 0\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    val result = \"\"\"{\n                   |  \"id\": \"bar/limits\",\n                   |  \"key\": \"bar\",\n                   |  \"value\": 1\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    blacklistingResults(js) shouldBe Seq(result)\n  }\n\n  it should \"match limits with 0 invocationsPerMinute\" in {\n    val js = \"\"\"{\n               |  \"_id\": \"bar/limits\",\n               |  \"concurrentInvocations\": 10,\n               |  \"invocationsPerMinute\": 0\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    val result = \"\"\"{\n                   |  \"id\": \"bar/limits\",\n                   |  \"key\": \"bar\",\n                   |  \"value\": 1\n                   |}\"\"\".stripMargin.parseJson.asJsObject\n    blacklistingResults(js) shouldBe Seq(result)\n  }\n\n  it should \"not match limits with invocationsPerMinute and concurrentInvocations defined\" in {\n    val js = \"\"\"{\n               |  \"_id\": \"bar/limits\",\n               |  \"concurrentInvocations\": 10,\n               |  \"invocationsPerMinute\": 40\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    blacklistingResults(js) shouldBe empty\n  }\n\n  it should \"list all namespaces of blocked subject\" in {\n    val js = \"\"\"{\n               |  \"_id\": \"bar\",\n               |  \"blocked\": true,\n               |  \"subject\": \"bar\",\n               |  \"namespaces\" : [\n               |    {\"name\": \"foo\", \"uuid\":\"u2\", \"key\":\"k2\"},\n               |    {\"name\": \"foo2\", \"uuid\":\"u3\", \"key\":\"k3\"}\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    val r1 = \"\"\"{\n               |  \"id\": \"bar\",\n               |  \"key\": \"foo\",\n               |  \"value\": 1\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    val r2 = \"\"\"{\n               |  \"id\": \"bar\",\n               |  \"key\": \"foo2\",\n               |  \"value\": 1\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    blacklistingResults(js).toSet shouldBe Set(r1, r2)\n  }\n\n  it should \"list no namespace of unblocked subject\" in {\n    val js = \"\"\"{\n               |  \"_id\": \"bar\",\n               |  \"subject\": \"bar\",\n               |  \"namespaces\" : [\n               |    {\"name\": \"foo\", \"uuid\":\"u2\", \"key\":\"k2\"},\n               |    {\"name\": \"foo2\", \"uuid\":\"u3\", \"key\":\"k3\"}\n               |  ]\n               |}\"\"\".stripMargin.parseJson.asJsObject\n\n    blacklistingResults(js) shouldBe empty\n  }\n\n  private def blacklistingResults(js: JsObject) = {\n    implicit val tid: TransactionId = transid()\n    SubjectHandler\n      .transformViewResult(\n        \"namespaceThrottlings\",\n        \"blockedNamespaces\",\n        List.empty,\n        List.empty,\n        includeDocs = false,\n        js,\n        TestDocumentProvider())\n      .futureValue\n  }\n\n  private case class TestDocumentProvider(js: Option[JsObject]) extends DocumentProvider {\n    override protected[database] def get(id: DocId)(implicit transid: TransactionId) = Future.successful(js)\n  }\n\n  private object TestDocumentProvider {\n    def apply(js: JsObject): DocumentProvider = new TestDocumentProvider(Some(js))\n    def apply(): DocumentProvider = new TestDocumentProvider(None)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/ExtendedCouchDbRestClient.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport scala.concurrent.Future\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.model._\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\nimport org.apache.openwhisk.common.Logging\nimport org.apache.openwhisk.core.database.CouchDbRestClient\nimport org.apache.openwhisk.http.PoolingRestClient._\n\n/**\n * Implementation of additional endpoints that should only be used in testing.\n */\nclass ExtendedCouchDbRestClient(protocol: String,\n                                host: String,\n                                port: Int,\n                                username: String,\n                                password: String,\n                                db: String)(implicit system: ActorSystem, logging: Logging)\n    extends CouchDbRestClient(protocol, host, port, username, password, db) {\n\n  // http://docs.couchdb.org/en/1.6.1/api/server/common.html#get--\n  def instanceInfo(): Future[Either[StatusCode, JsObject]] =\n    requestJson[JsObject](mkRequest(HttpMethods.GET, Uri./, headers = baseHeaders))\n\n  // http://docs.couchdb.org/en/1.6.1/api/server/common.html#all-dbs\n  def dbs(): Future[Either[StatusCode, List[String]]] = {\n    requestJson[JsArray](mkRequest(HttpMethods.GET, uri(\"_all_dbs\"), headers = baseHeaders))\n      .map(_.map(_.convertTo[List[String]]))\n  }\n\n  // http://docs.couchdb.org/en/1.6.1/api/database/common.html#put--db\n  def createDb(): Future[Either[StatusCode, JsObject]] =\n    requestJson[JsObject](mkRequest(HttpMethods.PUT, uri(db), headers = baseHeaders))\n\n  // http://docs.couchdb.org/en/1.6.1/api/database/common.html#delete--db\n  def deleteDb(): Future[Either[StatusCode, JsObject]] =\n    requestJson[JsObject](mkRequest(HttpMethods.DELETE, uri(db), headers = baseHeaders))\n\n  // http://docs.couchdb.org/en/1.6.1/api/database/bulk-api.html#get--db-_all_docs\n  def getAllDocs(skip: Option[Int] = None,\n                 limit: Option[Int] = None,\n                 includeDocs: Option[Boolean] = None,\n                 keys: Option[List[String]] = None): Future[Either[StatusCode, JsObject]] = {\n    val args = Seq[(String, Option[String])](\n      \"skip\" -> skip.filter(_ > 0).map(_.toString),\n      \"limit\" -> limit.filter(_ > 0).map(_.toString),\n      \"include_docs\" -> includeDocs.map(_.toString),\n      \"keys\" -> keys.map(_.toJson.compactPrint))\n\n    // Throw out all undefined arguments.\n    val argMap: Map[String, String] = args\n      .collect({\n        case (l, Some(r)) => (l, r)\n      })\n      .toMap\n\n    val url = uri(db, \"_all_docs\").withQuery(Uri.Query(argMap))\n    requestJson[JsObject](mkRequest(HttpMethods.GET, url, headers = baseHeaders))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/MultipleReadersSingleWriterCacheTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport scala.concurrent.Await\nimport scala.concurrent.Future\nimport scala.concurrent.duration.DurationInt\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.StreamLogging\nimport common.WskActorSystem\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.CacheChangeNotification\nimport org.apache.openwhisk.core.database.MultipleReadersSingleWriterCache\nimport org.apache.openwhisk.core.entity.CacheKey\n\n@RunWith(classOf[JUnitRunner])\nclass MultipleReadersSingleWriterCacheTests\n    extends AnyFlatSpec\n    with Matchers\n    with MultipleReadersSingleWriterCache[String, String]\n    with WskActorSystem\n    with StreamLogging {\n\n  behavior of \"the cache\"\n\n  it should \"execute the callback on invalidating and updating an entry\" in {\n    val ctr = new AtomicInteger(0)\n    val key = CacheKey(\"key\")\n\n    implicit val transId = TransactionId.testing\n    lazy implicit val cacheUpdateNotifier = Some {\n      new CacheChangeNotification {\n        override def apply(key: CacheKey) = {\n          ctr.incrementAndGet()\n          Future.successful(())\n        }\n      }\n    }\n\n    // Create an cache entry\n    Await.ready(cacheUpdate(\"doc\", key, Future.successful(\"db save successful\")), 10.seconds)\n    ctr.get shouldBe 1\n\n    // Callback should be called if entry exists\n    Await.ready(cacheInvalidate(key, Future.successful(())), 10.seconds)\n    ctr.get shouldBe 2\n    Await.ready(cacheUpdate(\"docdoc\", key, Future.successful(\"update in db successful\")), 10.seconds)\n    ctr.get shouldBe 3\n\n    // Callback should be called if entry does not exist\n    Await.ready(cacheInvalidate(CacheKey(\"abc\"), Future.successful(())), 10.seconds)\n    ctr.get shouldBe 4\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/RemoveLogsTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport java.io.File\nimport java.time.Instant\nimport java.time.temporal.ChronoUnit\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.StreamLogging\nimport common.TestUtils\nimport common.WhiskProperties\nimport common.WskActorSystem\nimport org.apache.openwhisk.core.entity.ActivationId\nimport org.apache.openwhisk.core.entity.EntityName\nimport org.apache.openwhisk.core.entity.EntityPath\nimport org.apache.openwhisk.core.entity.Subject\nimport org.apache.openwhisk.core.entity.WhiskActivation\nimport org.apache.openwhisk.core.entity.ActivationLogs\n\n@RunWith(classOf[JUnitRunner])\nclass RemoveLogsTests extends AnyFlatSpec with DatabaseScriptTestUtils with StreamLogging with WskActorSystem {\n\n  val designDocPath = WhiskProperties\n    .getFileRelativeToWhiskHome(\"ansible/files/logCleanup_design_document_for_activations_db.json\")\n    .getAbsolutePath\n  val removeLogsToolPath =\n    WhiskProperties.getFileRelativeToWhiskHome(\"tools/db/deleteLogsFromActivations.py\").getAbsolutePath\n\n  /** Runs the clean up script to delete old activations */\n  def removeLogsTool(dbUrl: DatabaseUrl, dbName: String, days: Int, docsPerRequest: Int = 20) = {\n    println(s\"Running removeLogs tool: ${dbUrl.safeUrl}, $dbName, $days, $docsPerRequest\")\n\n    val cmd = Seq(\n      python,\n      removeLogsToolPath,\n      \"--dbUrl\",\n      dbUrl.url,\n      \"--dbName\",\n      dbName,\n      \"--days\",\n      days.toString,\n      \"--docsPerRequest\",\n      docsPerRequest.toString)\n    val rr = TestUtils.runCmd(0, new File(\".\"), cmd: _*)\n  }\n\n  behavior of \"Activation Log Cleanup Script\"\n\n  it should \"delete logs in old activation and keep log in new activation\" in {\n    val dbName = dbPrefix + \"test_log_cleanup_1\"\n    val db = createDatabase(dbName, Some(designDocPath))\n\n    try {\n      // Create an old and a new activation\n      val oldActivation = WhiskActivation(\n        namespace = EntityPath(\"testns1\"),\n        name = EntityName(\"testname1\"),\n        subject = Subject(\"test-sub1\"),\n        activationId = ActivationId.generate(),\n        start = Instant.now.minus(2, ChronoUnit.DAYS),\n        end = Instant.now,\n        logs = ActivationLogs(Vector(\"first line1\", \"second line1\")))\n\n      val newActivation = WhiskActivation(\n        namespace = EntityPath(\"testns2\"),\n        name = EntityName(\"testname2\"),\n        subject = Subject(\"test-sub2\"),\n        activationId = ActivationId.generate(),\n        start = Instant.now,\n        end = Instant.now,\n        logs = ActivationLogs(Vector(\"first line2\", \"second line2\")))\n\n      db.putDoc(oldActivation.docid.asString, oldActivation.toJson).futureValue shouldBe 'right\n      db.putDoc(newActivation.docid.asString, newActivation.toJson).futureValue shouldBe 'right\n\n      // Run the tool to delete logs from activations that are older than 1 day\n      removeLogsTool(dbUrl, dbName, 1)\n\n      // Check, that the new activation is untouched and the old activation is without logs now\n      val newActivationRequest = db.getDoc(newActivation.docid.asString).futureValue\n      newActivationRequest shouldBe 'right\n      val newActivationFromDb = newActivationRequest.right.get.convertTo[WhiskActivation]\n      // Compare the Json of the Whiskactivations in case the test fails -> better log output\n      newActivationFromDb.toJson shouldBe newActivation.toJson\n\n      val oldActivationRequest = db.getDoc(oldActivation.docid.asString).futureValue\n      oldActivationRequest shouldBe 'right\n      val oldActivationFromDb = oldActivationRequest.right.get.convertTo[WhiskActivation]\n      // Compare the Json of the Whiskactivations in case the test fails -> better log output\n      oldActivationFromDb.toJson shouldBe oldActivation.withoutLogs.toJson\n    } finally {\n      removeDatabase(dbName)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/ReplicatorTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test\n\nimport java.io.File\nimport java.time.Instant\n\nimport scala.concurrent.duration._\nimport scala.language.implicitConversions\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\nimport common.StreamLogging\nimport common.TestUtils\nimport common.WaitFor\nimport common.WhiskProperties\nimport common.WskActorSystem\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n@RunWith(classOf[JUnitRunner])\nclass ReplicatorTests\n    extends AnyFlatSpec\n    with Matchers\n    with ScalaFutures\n    with WskActorSystem\n    with WaitFor\n    with StreamLogging\n    with DatabaseScriptTestUtils {\n\n  val testDbPrefix = s\"replicatortest_$dbPrefix\"\n\n  val replicatorClient =\n    new ExtendedCouchDbRestClient(dbProtocol, dbHost, dbPort.toInt, dbUsername, dbPassword, \"_replicator\")\n  replicatorClient.createDb().futureValue\n\n  val replicator = WhiskProperties.getFileRelativeToWhiskHome(\"tools/db/replicateDbs.py\").getAbsolutePath\n  val designDocPath =\n    WhiskProperties.getFileRelativeToWhiskHome(\"ansible/files/filter_design_document.json\").getAbsolutePath\n\n  implicit def toDuration(dur: FiniteDuration) = java.time.Duration.ofMillis(dur.toMillis)\n  def toEpochSeconds(i: Instant) = i.toEpochMilli / 1000\n\n  /** Removes a document from the _replicator-database */\n  def removeReplicationDoc(id: String) = {\n    println(s\"Removing replication doc: $id\")\n    val response = replicatorClient.getDoc(id).futureValue\n    val rev = response.right.toOption.map(_.fields(\"_rev\").convertTo[String])\n    rev.map(replicatorClient.deleteDoc(id, _).futureValue)\n  }\n\n  /** Runs the replicator script to replicate databases */\n  def runReplicator(sourceDbUrl: DatabaseUrl,\n                    targetDbUrl: DatabaseUrl,\n                    dbPrefix: String,\n                    expires: FiniteDuration,\n                    continuous: Boolean = false,\n                    exclude: List[String] = List.empty,\n                    excludeBaseName: List[String] = List.empty) = {\n    println(\n      s\"Running replicator: ${sourceDbUrl.safeUrl}, ${targetDbUrl.safeUrl}, $dbPrefix, $expires, $continuous, $exclude, $excludeBaseName\")\n\n    val continuousFlag = if (continuous) Some(\"--continuous\") else None\n    val excludeFlag = Seq(exclude.mkString(\",\")).filter(_.nonEmpty).flatMap(ex => Seq(\"--exclude\", ex))\n    val excludeBaseNameFlag =\n      Seq(excludeBaseName.mkString(\",\")).filter(_.nonEmpty).flatMap(ex => Seq(\"--excludeBaseName\", ex))\n    val cmd = Seq(\n      python,\n      replicator,\n      \"--sourceDbUrl\",\n      sourceDbUrl.url,\n      \"--targetDbUrl\",\n      targetDbUrl.url,\n      \"replicate\",\n      \"--dbPrefix\",\n      dbPrefix,\n      \"--expires\",\n      expires.toSeconds.toString) ++ continuousFlag ++ excludeFlag ++ excludeBaseNameFlag\n    val rr = TestUtils.runCmd(0, new File(\".\"), cmd: _*)\n\n    val Seq(created, deletedDoc, deleted) =\n      Seq(\"create backup: \", \"deleting backup document: \", \"deleting backup: \").map { prefix =>\n        rr.stdout.linesIterator.collect {\n          case line if line.startsWith(prefix) => line.replace(prefix, \"\")\n        }.toList\n      }\n\n    println(s\"Created: $created\")\n    println(s\"DeletedDocs: $deletedDoc\")\n    println(s\"Deleted: $deleted\")\n\n    (created, deletedDoc, deleted)\n  }\n\n  /** Runs the replicator script to replay databases */\n  def runReplay(sourceDbUrl: DatabaseUrl, targetDbUrl: DatabaseUrl, dbPrefix: String) = {\n    println(s\"Running replay: ${sourceDbUrl.safeUrl}, ${targetDbUrl.safeUrl}, $dbPrefix\")\n    val rr = TestUtils.runCmd(\n      0,\n      new File(\".\"),\n      WhiskProperties.python,\n      WhiskProperties.getFileRelativeToWhiskHome(\"tools/db/replicateDbs.py\").getAbsolutePath,\n      \"--sourceDbUrl\",\n      sourceDbUrl.url,\n      \"--targetDbUrl\",\n      targetDbUrl.url,\n      \"replay\",\n      \"--dbPrefix\",\n      dbPrefix)\n\n    val line = \"\"\"([\\w-]+) -> ([\\w-]+) \\(([\\w-]+)\\)\"\"\".r.unanchored\n    val replays = rr.stdout.linesIterator.collect {\n      case line(backup, target, id) => (backup, target, id)\n    }.toList\n\n    println(s\"Replays created: $replays\")\n\n    replays\n  }\n\n  /** Wait for a replication to finish */\n  def waitForReplication(dbName: String) = {\n    val timeout = 5.minutes\n    val replicationResult = waitfor(() => {\n      val replicatorDoc = replicatorClient.getDoc(dbName).futureValue\n      replicatorDoc shouldBe 'right\n\n      val state = replicatorDoc.right.get.fields.get(\"_replication_state\")\n      println(s\"Waiting for replication, state: $state\")\n\n      state.contains(\"completed\".toJson)\n    }, totalWait = timeout)\n\n    assert(replicationResult, s\"replication did not finish in $timeout\")\n  }\n\n  /** Compares to databases to full equality */\n  def compareDatabases(sourceDb: String, targetDb: String, filterUsed: Boolean) = {\n    val originalDocs = getAllDocs(sourceDb)\n    val replicatedDocs = getAllDocs(targetDb)\n\n    if (!filterUsed) {\n      replicatedDocs shouldBe originalDocs\n    } else {\n      val filteredReplicatedDocs = replicatedDocs.fields(\"rows\").convertTo[List[JsObject]]\n      val filteredOriginalDocs = originalDocs\n        .fields(\"rows\")\n        .convertTo[List[JsObject]]\n        .filterNot(_.fields(\"id\").convertTo[String].startsWith(\"_design/\"))\n\n      filteredReplicatedDocs shouldBe filteredOriginalDocs\n    }\n  }\n\n  // Do cleanups of possibly existing databases\n  val dbs = replicatorClient.dbs().futureValue\n  dbs shouldBe 'right\n  dbs.right.get.filter(dbname => dbname.contains(testDbPrefix)).map(dbName => removeDatabase(dbName))\n\n  val docs = replicatorClient.getAllDocs().futureValue\n  docs shouldBe 'right\n  docs.right.get\n    .fields(\"rows\")\n    .convertTo[List[JsObject]]\n    .filter(_.fields(\"id\").convertTo[String].contains(testDbPrefix))\n    .map(doc =>\n      replicatorClient\n        .deleteDoc(doc.fields(\"id\").convertTo[String], doc.fields(\"value\").asJsObject.fields(\"rev\").convertTo[String]))\n\n  behavior of \"Database replication script\"\n\n  it should \"replicate a database (snapshot)\" in {\n    // Create a database to backup\n    val dbName = testDbPrefix + \"database_for_single_replication\"\n    val client = createDatabase(dbName, Some(designDocPath))\n\n    println(s\"Creating testdocument\")\n    val testDocument = JsObject(\"testKey\" -> \"testValue\".toJson)\n    client.putDoc(\"testId\", testDocument).futureValue\n\n    // Trigger replication and verify the created databases have the correct format\n    val (createdBackupDbs, _, _) = runReplicator(dbUrl, dbUrl, testDbPrefix, 10.minutes)\n    createdBackupDbs should have size 1\n    val backupDbName = createdBackupDbs.head\n    backupDbName should fullyMatch regex s\"backup_\\\\d+_$dbName\"\n\n    // Wait for the replication to finish\n    waitForReplication(backupDbName)\n\n    // Verify the replicated database is equal to the original database\n    compareDatabases(dbName, backupDbName, filterUsed = true)\n\n    // Remove all created databases\n    createdBackupDbs.foreach(removeDatabase(_))\n    createdBackupDbs.foreach(removeReplicationDoc)\n    removeDatabase(dbName)\n  }\n\n  it should \"do not replicate a database that is excluded\" in {\n    // Create a database to backup\n    val dbNameToBackup = testDbPrefix + \"database_for_single_replication_with_exclude\"\n    val nExClient = createDatabase(dbNameToBackup, Some(designDocPath))\n\n    val excludedName = \"some_excluded_name\"\n    val exClient = createDatabase(testDbPrefix + excludedName, Some(designDocPath))\n\n    // Trigger replication and verify the created databases have the correct format\n    val (createdBackupDbs, _, _) = runReplicator(dbUrl, dbUrl, testDbPrefix, 10.minutes, exclude = List(excludedName))\n    createdBackupDbs should have size 1\n    val backupDbName = createdBackupDbs.head\n    backupDbName should fullyMatch regex s\"backup_\\\\d+_$dbNameToBackup\"\n\n    // Wait for the replication to finish\n    waitForReplication(backupDbName)\n\n    // Remove all created databases\n    createdBackupDbs.foreach(removeDatabase(_))\n    createdBackupDbs.foreach(removeReplicationDoc)\n    removeDatabase(dbNameToBackup)\n    removeDatabase(testDbPrefix + excludedName)\n  }\n\n  it should \"not replicate a database that basename is excluded\" in {\n    // Create a database to backup\n    val dbNameToBackup = testDbPrefix + \"database_for_single_replication_with_exclude_basename\"\n    createDatabase(dbNameToBackup, Some(designDocPath))\n\n    val excludedName = \"some_excluded_name\"\n    createDatabase(testDbPrefix + excludedName + \"-postfix123\", Some(designDocPath))\n\n    // Trigger replication and verify the created databases have the correct format\n    val (createdBackupDbs, _, _) =\n      runReplicator(dbUrl, dbUrl, testDbPrefix, 10.minutes, excludeBaseName = List(excludedName))\n    createdBackupDbs should have size 1\n    val backupDbName = createdBackupDbs.head\n    backupDbName should fullyMatch regex s\"backup_\\\\d+_$dbNameToBackup\"\n\n    // Wait for the replication to finish\n    waitForReplication(backupDbName)\n\n    // Remove all created databases\n    createdBackupDbs.foreach(removeDatabase(_))\n    createdBackupDbs.foreach(removeReplicationDoc)\n    removeDatabase(dbNameToBackup)\n    removeDatabase(testDbPrefix + excludedName + \"-postfix123\")\n  }\n\n  it should \"replicate a database (snapshot) even if the filter is not available\" in {\n    // Create a db to backup\n    val dbName = testDbPrefix + \"database_for_snapshout_without_filter\"\n    val client = createDatabase(dbName, None)\n\n    println(\"Creating testdocuments\")\n    val testDocuments = Seq(\n      JsObject(\"testKey\" -> \"testValue\".toJson, \"_id\" -> \"doc1\".toJson),\n      JsObject(\"testKey\" -> \"testValue\".toJson, \"_id\" -> \"_design/doc1\".toJson))\n    val documents = testDocuments.map { doc =>\n      val res = client.putDoc(doc.fields(\"_id\").convertTo[String], doc).futureValue\n      res shouldBe 'right\n      res.right.get\n    }\n\n    // Trigger replication and verify the created databases have the correct format\n    val (createdBackupDbs, _, _) = runReplicator(dbUrl, dbUrl, testDbPrefix, 10.minutes)\n    createdBackupDbs should have size 1\n    val backupDbName = createdBackupDbs.head\n    backupDbName should fullyMatch regex s\"backup_\\\\d+_$dbName\"\n\n    // Wait for the replication to finish\n    waitForReplication(backupDbName)\n\n    // Verify the replicated database is equal to the original database\n    compareDatabases(dbName, backupDbName, filterUsed = false)\n\n    // Remove all created databases\n    createdBackupDbs.foreach(removeDatabase(_))\n    createdBackupDbs.foreach(removeReplicationDoc)\n    removeDatabase(dbName)\n  }\n\n  it should \"replicate a database (snapshot) and deleted documents and design documents should not be in the snapshot\" in {\n    // Create a database to backup\n    val dbName = testDbPrefix + \"database_for_single_replication_design_and_deleted_docs\"\n    val client = createDatabase(dbName, Some(designDocPath))\n\n    println(s\"Creating testdocument\")\n    val testDocuments = Seq(\n      JsObject(\"testKey\" -> \"testValue\".toJson, \"_id\" -> \"doc1\".toJson),\n      JsObject(\"testKey\" -> \"testValue\".toJson, \"_id\" -> \"doc2\".toJson),\n      JsObject(\"testKey\" -> \"testValue\".toJson, \"_id\" -> \"_design/doc1\".toJson))\n    val documents = testDocuments.map { doc =>\n      val res = client.putDoc(doc.fields(\"_id\").convertTo[String], doc).futureValue\n      res shouldBe 'right\n      res.right.get\n    }\n\n    // Delete second document again\n    val indexOfDocumentToDelete = 1\n    val idOfDeletedDocument = documents(indexOfDocumentToDelete).fields(\"id\").convertTo[String]\n    client.deleteDoc(idOfDeletedDocument, documents(indexOfDocumentToDelete).fields(\"rev\").convertTo[String])\n\n    // Trigger replication and verify the created databases have the correct format\n    val (createdBackupDbs, _, _) = runReplicator(dbUrl, dbUrl, testDbPrefix, 10.minutes)\n    createdBackupDbs should have size 1\n    val backupDbName = createdBackupDbs.head\n    backupDbName should fullyMatch regex s\"backup_\\\\d+_$dbName\"\n\n    // Wait for the replication to finish\n    waitForReplication(backupDbName)\n\n    // Verify the replicated database is equal to the original database\n    compareDatabases(dbName, backupDbName, filterUsed = true)\n\n    // Check that deleted doc has not been copied to snapshot\n    val snapshotClient =\n      new ExtendedCouchDbRestClient(dbProtocol, dbHost, dbPort.toInt, dbUsername, dbPassword, backupDbName)\n    val snapshotResponse = snapshotClient.getAllDocs(keys = Some(List(idOfDeletedDocument))).futureValue\n    snapshotResponse shouldBe 'right\n    val results = snapshotResponse.right.get.fields(\"rows\").convertTo[List[JsObject]]\n    results should have size 1\n    // If deleted doc would be in db, the document id and rev would have been returned\n    results.head shouldBe JsObject(\"key\" -> idOfDeletedDocument.toJson, \"error\" -> \"not_found\".toJson)\n\n    // Remove all created databases\n    createdBackupDbs.foreach(removeDatabase(_))\n    createdBackupDbs.foreach(removeReplicationDoc)\n    removeDatabase(dbName)\n  }\n\n  it should \"continuously update a database\" in {\n    // Create a database to backup\n    val dbName = testDbPrefix + \"database_for_continuous_replication\"\n    val backupDbName = s\"continuous_$dbName\"\n\n    // Pre-test cleanup of previously created entities\n    removeDatabase(backupDbName, true)\n    removeReplicationDoc(backupDbName)\n\n    val client = createDatabase(dbName, Some(designDocPath))\n\n    // Trigger replication and verify the created databases have the correct format\n    val (createdBackupDbs, _, _) = runReplicator(dbUrl, dbUrl, testDbPrefix, 10.minutes, continuous = true)\n    createdBackupDbs should have size 1\n    createdBackupDbs.head shouldBe backupDbName\n\n    // Wait for the replicated database to appear\n    val backupClient = waitForDatabase(backupDbName)\n\n    // Create a document in the old database\n    println(s\"Creating testdocument\")\n    val docId = \"testId\"\n    val testDocument = JsObject(\"testKey\" -> \"testValue\".toJson)\n    client.putDoc(docId, testDocument).futureValue\n\n    // Wait for the document to appear\n    waitForDocument(backupClient, docId)\n\n    // Verify the replicated database is equal to the original database\n    compareDatabases(backupDbName, dbName, filterUsed = false)\n\n    // Stop the replication\n    val replication = replicatorClient.getDoc(backupDbName).futureValue\n    replication shouldBe 'right\n    val replicationDoc = replication.right.get\n    replicatorClient.deleteDoc(\n      replicationDoc.fields(\"_id\").convertTo[String],\n      replicationDoc.fields(\"_rev\").convertTo[String])\n\n    // Remove all created databases\n    createdBackupDbs.foreach(removeDatabase(_))\n    createdBackupDbs.foreach(removeReplicationDoc)\n    removeDatabase(dbName)\n  }\n\n  it should \"remove outdated databases and replicationDocs\" in {\n    val now = Instant.now()\n    val expires = 10.minutes\n\n    println(s\"Now is: ${toEpochSeconds(now)}\")\n\n    // Create a database that is already expired\n    val expired = now.minus(expires + 5.minutes)\n    val expiredName = s\"backup_${toEpochSeconds(expired)}_${testDbPrefix}expired_backup\"\n    val expiredClient = createDatabase(expiredName, Some(designDocPath))\n    replicatorClient.putDoc(expiredName, JsObject(\"source\" -> \"\".toJson, \"target\" -> \"\".toJson)).futureValue\n\n    // Create a database that is not yet expired\n    val notExpired = now.plus(expires - 5.minutes)\n    val notExpiredName = s\"backup_${toEpochSeconds(notExpired)}_${testDbPrefix}notexpired_backup\"\n    val notExpiredClient = createDatabase(notExpiredName, Some(designDocPath))\n    replicatorClient.putDoc(notExpiredName, JsObject(\"source\" -> \"\".toJson, \"target\" -> \"\".toJson)).futureValue\n\n    // Trigger replication and verify the expired database is deleted while the unexpired one is kept\n    val (createdDatabases, deletedReplicationDocs, deletedDatabases) =\n      runReplicator(dbUrl, dbUrl, testDbPrefix, expires)\n    deletedReplicationDocs should (contain(expiredName) and not contain notExpiredName)\n    deletedDatabases should (contain(expiredName) and not contain notExpiredName)\n\n    expiredClient.getAllDocs().futureValue shouldBe Left(StatusCodes.NotFound)\n    notExpiredClient.getAllDocs().futureValue shouldBe 'right\n\n    // Cleanup backup database\n    createdDatabases.foreach(removeDatabase(_))\n    createdDatabases.foreach(removeReplicationDoc)\n    removeReplicationDoc(notExpiredName)\n    removeDatabase(notExpiredName)\n  }\n\n  it should \"not remove outdated databases with other prefix\" in {\n    val now = Instant.now()\n    val expires = 10.minutes\n\n    println(s\"Now is: ${toEpochSeconds(now)}\")\n\n    val expired = now.minus(expires + 5.minutes)\n\n    // Create a database that is expired with correct prefix\n    val correctPrefixName = s\"backup_${toEpochSeconds(expired)}_${testDbPrefix}expired_backup_correct_prefix\"\n    val correctPrefixClient = createDatabase(correctPrefixName, Some(designDocPath))\n    replicatorClient.putDoc(correctPrefixName, JsObject(\"source\" -> \"\".toJson, \"target\" -> \"\".toJson)).futureValue\n\n    // Create a database that is expired with wrong prefix\n    val wrongPrefix = s\"replicatortest_wrongprefix_$dbPrefix\"\n    val wrongPrefixName = s\"backup_${toEpochSeconds(expired)}_${wrongPrefix}expired_backup_wrong_prefix\"\n    val wrongPrefixClient = createDatabase(wrongPrefixName, Some(designDocPath))\n    replicatorClient.putDoc(wrongPrefixName, JsObject(\"source\" -> \"\".toJson, \"target\" -> \"\".toJson)).futureValue\n\n    // Trigger replication and verify the expired database with correct prefix is deleted while the db with the wrong prefix is kept\n    val (createdDatabases, deletedReplicationDocs, deletedDatabases) =\n      runReplicator(dbUrl, dbUrl, testDbPrefix, expires)\n    deletedReplicationDocs should (contain(correctPrefixName) and not contain wrongPrefixName)\n    deletedDatabases should (contain(correctPrefixName) and not contain wrongPrefixName)\n\n    correctPrefixClient.getAllDocs().futureValue shouldBe Left(StatusCodes.NotFound)\n    wrongPrefixClient.getAllDocs().futureValue shouldBe 'right\n\n    // Cleanup backup database\n    createdDatabases.foreach(removeDatabase(_))\n    createdDatabases.foreach(removeReplicationDoc)\n    removeReplicationDoc(wrongPrefixName)\n    removeDatabase(wrongPrefixName)\n  }\n\n  it should \"replay a database\" in {\n    val now = Instant.now()\n    val dbName = testDbPrefix + \"database_to_be_restored\"\n    val backupPrefix = s\"backup_${toEpochSeconds(now)}_\"\n    val backupDbName = backupPrefix + dbName\n\n    // Create a database that looks like a backup\n    val backupClient = createDatabase(backupDbName, Some(designDocPath))\n    println(s\"Creating testdocument\")\n    backupClient.putDoc(\"testId\", JsObject(\"testKey\" -> \"testValue\".toJson)).futureValue\n\n    // Run the replay script\n    val (_, _, replicationId) = runReplay(dbUrl, dbUrl, backupPrefix).head\n\n    // Wait for the replication to finish\n    waitForReplication(replicationId)\n\n    // Verify the replicated database is equal to the original database\n    compareDatabases(backupDbName, dbName, filterUsed = false)\n\n    // Cleanup databases\n    removeReplicationDoc(replicationId)\n    removeDatabase(backupDbName)\n    removeDatabase(dbName)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ActivationStoreBehavior.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\ntrait ActivationStoreBehavior\n    extends ActivationStoreBehaviorBase\n    with ActivationStoreCRUDBehaviors\n    with ActivationStoreQueryBehaviors\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ActivationStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport java.time.Instant\n\nimport common.{StreamLogging, WskActorSystem}\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.{ActivationStore, CacheChangeNotification, UserContext}\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreTestUtil.storeAvailable\nimport org.apache.openwhisk.core.entity._\nimport org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}\nimport org.scalatest.{BeforeAndAfterEach, Outcome}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.collection.mutable.ListBuffer\nimport scala.concurrent.Await\nimport scala.concurrent.duration.Duration\nimport scala.concurrent.duration.DurationInt\nimport scala.language.postfixOps\nimport scala.util.{Random, Try}\n\ntrait ActivationStoreBehaviorBase\n    extends AnyFlatSpec\n    with ScalaFutures\n    with Matchers\n    with StreamLogging\n    with WskActorSystem\n    with IntegrationPatience\n    with BeforeAndAfterEach {\n\n  protected implicit val notifier: Option[CacheChangeNotification] = None\n\n  def context: UserContext\n  def activationStore: ActivationStore\n  private val docsToDelete = ListBuffer[(UserContext, ActivationId)]()\n\n  def storeType: String\n\n  protected def transId() = TransactionId(Random.alphanumeric.take(32).mkString)\n\n  override def afterEach(): Unit = {\n    cleanup()\n    stream.reset()\n  }\n\n  override protected def withFixture(test: NoArgTest): Outcome = {\n    assume(storeAvailable(storeAvailableCheck), s\"$storeType not configured or available\")\n    val outcome = super.withFixture(test)\n    if (outcome.isFailed) {\n      println(logLines.mkString(\"\\n\"))\n    }\n    outcome\n  }\n\n  protected def storeAvailableCheck: Try[Any] = Try(true)\n  //~----------------------------------------< utility methods >\n\n  protected def store(activation: WhiskActivation, context: UserContext)(\n    implicit transid: TransactionId,\n    notifier: Option[CacheChangeNotification]): DocInfo = {\n    val doc = activationStore.store(activation, context).futureValue\n    docsToDelete.append((context, ActivationId(activation.docid.asString)))\n    doc\n  }\n\n  protected def newActivation(ns: String, actionName: String, start: Long): WhiskActivation = {\n    WhiskActivation(\n      EntityPath(ns),\n      EntityName(actionName),\n      Subject(),\n      ActivationId.generate(),\n      Instant.ofEpochMilli(start),\n      Instant.ofEpochMilli(start + 1000))\n  }\n\n  protected def newBindingActivation(ns: String, actionName: String, binding: String, start: Long): WhiskActivation = {\n    WhiskActivation(\n      EntityPath(ns),\n      EntityName(actionName),\n      Subject(),\n      ActivationId.generate(),\n      Instant.ofEpochMilli(start),\n      Instant.ofEpochMilli(start + 1000),\n      annotations = Parameters(WhiskActivation.bindingAnnotation, binding))\n  }\n\n  /**\n   * Deletes all documents added to gc queue.\n   */\n  def cleanup()(implicit timeout: Duration = 10 seconds): Unit = {\n    implicit val tid: TransactionId = transId()\n    docsToDelete.map { e =>\n      Try {\n        Await.result(activationStore.delete(e._2, e._1), timeout)\n      }\n    }\n    docsToDelete.clear()\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ActivationStoreCRUDBehaviors.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.NoDocumentException\nimport org.apache.openwhisk.core.entity.{ActivationId, WhiskActivation}\n\nimport scala.util.Random\n\ntrait ActivationStoreCRUDBehaviors extends ActivationStoreBehaviorBase {\n\n  protected def checkStoreActivation(activation: WhiskActivation)(implicit transid: TransactionId): Unit = {\n    store(activation, context) shouldBe activation.docinfo\n  }\n\n  protected def checkDeleteActivation(activation: WhiskActivation)(implicit transid: TransactionId): Unit = {\n    activationStore.delete(ActivationId(activation.docid.asString), context).futureValue shouldBe true\n  }\n\n  protected def checkGetActivation(activation: WhiskActivation)(implicit transid: TransactionId): Unit = {\n    activationStore.get(ActivationId(activation.docid.asString), context).futureValue shouldBe activation\n  }\n\n  behavior of s\"${storeType}ActivationStore store\"\n\n  it should \"put activation and get docinfo\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n    val activation = newActivation(namespace, action, 1L)\n    checkStoreActivation(activation)\n  }\n\n  behavior of s\"${storeType}ActivationStore delete\"\n\n  it should \"deletes existing activation\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n    val activation = newActivation(namespace, action, 1L)\n    store(activation, context)\n    checkDeleteActivation(activation)\n  }\n\n  it should \"throws NoDocumentException when activation does not exist\" in {\n    implicit val tid: TransactionId = transId()\n    activationStore.delete(ActivationId(\"non-existing-doc\"), context).failed.futureValue shouldBe a[NoDocumentException]\n  }\n\n  behavior of s\"${storeType}ActivationStore get\"\n\n  it should \"get existing activation matching id\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n    val activation = newActivation(namespace, action, 1L)\n    store(activation, context)\n    checkGetActivation(activation)\n  }\n\n  it should \"throws NoDocumentException when activation does not exist\" in {\n    implicit val tid: TransactionId = transId()\n    activationStore.get(ActivationId(\"non-existing-doc\"), context).failed.futureValue shouldBe a[NoDocumentException]\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ActivationStoreQueryBehaviors.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport java.time.Instant\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.UserContext\nimport org.apache.openwhisk.core.entity.{EntityPath, WhiskActivation}\nimport spray.json.{JsNumber, JsObject}\n\nimport scala.util.Random\n\ntrait ActivationStoreQueryBehaviors extends ActivationStoreBehaviorBase {\n\n  protected def checkQueryActivations(namespace: String,\n                                      name: Option[String] = None,\n                                      skip: Int = 0,\n                                      limit: Int = 1000,\n                                      includeDocs: Boolean = false,\n                                      since: Option[Instant] = None,\n                                      upto: Option[Instant] = None,\n                                      context: UserContext,\n                                      expected: IndexedSeq[WhiskActivation])(implicit transid: TransactionId): Unit = {\n    val result =\n      name\n        .map { n =>\n          activationStore\n            .listActivationsMatchingName(\n              EntityPath(namespace),\n              EntityPath(n),\n              skip = skip,\n              limit = limit,\n              includeDocs = includeDocs,\n              since = since,\n              upto = upto,\n              context = context)\n        }\n        .getOrElse {\n          activationStore\n            .listActivationsInNamespace(\n              EntityPath(namespace),\n              skip = skip,\n              limit = limit,\n              includeDocs = includeDocs,\n              since = since,\n              upto = upto,\n              context = context)\n        }\n        .map { r =>\n          r.fold(left => left, right => right.map(wa => if (includeDocs) wa.toExtendedJson() else wa.summaryAsJson))\n        }\n        .futureValue\n\n    result should contain theSameElementsAs expected.map(wa =>\n      if (includeDocs) wa.toExtendedJson() else wa.summaryAsJson)\n  }\n\n  protected def checkCountActivations(namespace: String,\n                                      name: Option[EntityPath] = None,\n                                      skip: Int = 0,\n                                      since: Option[Instant] = None,\n                                      upto: Option[Instant] = None,\n                                      context: UserContext,\n                                      expected: Long)(implicit transid: TransactionId): Unit = {\n    val result = activationStore\n      .countActivationsInNamespace(\n        EntityPath(namespace),\n        name = name,\n        skip = skip,\n        since = since,\n        upto = upto,\n        context = context)\n      .futureValue\n\n    result shouldBe JsObject(WhiskActivation.collectionName -> JsNumber(expected))\n  }\n\n  behavior of s\"${storeType}ActivationStore listActivationsInNamespace\"\n\n  it should \"find all entities\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n    val action2 = s\"action2_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    val activations2 = (1000 until 1100 by 10).map(newActivation(namespace, action2, _))\n    activations2 foreach (store(_, context))\n\n    checkQueryActivations(namespace, context = context, expected = activations ++ activations2)\n  }\n\n  it should \"support since and upto filters\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkQueryActivations(\n      namespace,\n      since = Some(Instant.ofEpochMilli(1060)),\n      context = context,\n      expected = activations.takeRight(4))\n\n    checkQueryActivations(\n      namespace,\n      upto = Some(Instant.ofEpochMilli(1060)),\n      context = context,\n      expected = activations.take(7))\n\n    checkQueryActivations(\n      namespace,\n      since = Some(Instant.ofEpochMilli(1030)),\n      upto = Some(Instant.ofEpochMilli(1060)),\n      context = context,\n      expected = activations.take(7).takeRight(4))\n  }\n\n  it should \"support skipping results\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkQueryActivations(namespace, skip = 5, context = context, expected = activations.take(5))\n  }\n\n  it should \"support limiting results\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkQueryActivations(namespace, limit = 5, context = context, expected = activations.takeRight(5))\n  }\n\n  it should \"support including complete docs\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkQueryActivations(namespace, includeDocs = true, context = context, expected = activations)\n  }\n\n  it should \"throw exception for negative limits and skip\" in {\n    implicit val tid: TransactionId = transId()\n\n    a[IllegalArgumentException] should be thrownBy activationStore\n      .listActivationsInNamespace(EntityPath(\"test\"), skip = -1, limit = 10, context = context)\n\n    a[IllegalArgumentException] should be thrownBy activationStore\n      .listActivationsInNamespace(EntityPath(\"test\"), skip = 0, limit = -1, context = context)\n  }\n\n  behavior of s\"${storeType}ActivationStore listActivationsMatchingName\"\n\n  it should \"find all entities matching name\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n    val action2 = s\"action2_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    val activations2 = (1000 until 1100 by 10).map(newActivation(namespace, action2, _))\n    activations2 foreach (store(_, context))\n\n    checkQueryActivations(namespace, Some(action1), context = context, expected = activations)\n  }\n\n  it should \"find all binding entities matching name\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val package1 = s\"package1_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val binding = s\"$namespace/$package1\"\n\n    val activations = (1000 until 1100 by 10).map(newBindingActivation(namespace, action1, binding, _))\n    activations foreach (store(_, context))\n\n    val activations2 = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations2 foreach (store(_, context))\n\n    checkQueryActivations(namespace, Some(s\"$package1/$action1\"), context = context, expected = activations)\n  }\n\n  it should \"support since and upto filters\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkQueryActivations(\n      namespace,\n      Some(action1),\n      since = Some(Instant.ofEpochMilli(1060)),\n      context = context,\n      expected = activations.takeRight(4))\n\n    checkQueryActivations(\n      namespace,\n      Some(action1),\n      upto = Some(Instant.ofEpochMilli(1060)),\n      context = context,\n      expected = activations.take(7))\n\n    checkQueryActivations(\n      namespace,\n      Some(action1),\n      since = Some(Instant.ofEpochMilli(1030)),\n      upto = Some(Instant.ofEpochMilli(1060)),\n      context = context,\n      expected = activations.take(7).takeRight(4))\n  }\n\n  it should \"support skipping results\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkQueryActivations(namespace, Some(action1), skip = 5, context = context, expected = activations.take(5))\n  }\n\n  it should \"support limiting results\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkQueryActivations(namespace, Some(action1), limit = 5, context = context, expected = activations.takeRight(5))\n  }\n\n  it should \"support including complete docs\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkQueryActivations(namespace, Some(action1), includeDocs = true, context = context, expected = activations)\n  }\n\n  it should \"throw exception for negative limits and skip\" in {\n    implicit val tid: TransactionId = transId()\n\n    a[IllegalArgumentException] should be thrownBy activationStore.listActivationsMatchingName(\n      EntityPath(\"test\"),\n      name = EntityPath(\"testact\"),\n      skip = -1,\n      limit = 10,\n      context = context)\n\n    a[IllegalArgumentException] should be thrownBy activationStore.listActivationsMatchingName(\n      EntityPath(\"test\"),\n      name = EntityPath(\"testact\"),\n      skip = 0,\n      limit = -1,\n      context = context)\n  }\n\n  behavior of s\"${storeType}ActivationStore countActivationsInNamespace\"\n\n  it should \"should count all created activations\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkCountActivations(namespace, None, context = context, expected = 10)\n  }\n\n  it should \"count with option name\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n    val action2 = s\"action2_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    val activations2 = (1000 until 1100 by 10).map(newActivation(namespace, action2, _))\n    activations2 foreach (store(_, context))\n\n    checkCountActivations(namespace, Some(EntityPath(action1)), context = context, expected = 10)\n\n    checkCountActivations(namespace, Some(EntityPath(action2)), context = context, expected = 10)\n  }\n\n  it should \"count with since and upto\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkCountActivations(namespace, None, since = Some(Instant.ofEpochMilli(1060L)), context = context, expected = 4)\n\n    checkCountActivations(namespace, None, upto = Some(Instant.ofEpochMilli(1060L)), context = context, expected = 7)\n\n    checkCountActivations(\n      namespace,\n      None,\n      since = Some(Instant.ofEpochMilli(1030L)),\n      upto = Some(Instant.ofEpochMilli(1060L)),\n      context = context,\n      expected = 4)\n  }\n\n  it should \"count with skip\" in {\n    implicit val tid: TransactionId = transId()\n    val namespace = s\"ns_${Random.alphanumeric.take(4).mkString}\"\n    val action1 = s\"action1_${Random.alphanumeric.take(4).mkString}\"\n\n    val activations = (1000 until 1100 by 10).map(newActivation(namespace, action1, _))\n    activations foreach (store(_, context))\n\n    checkCountActivations(namespace, None, skip = 4, context = context, expected = 10 - 4)\n    checkCountActivations(namespace, None, skip = 1000, context = context, expected = 0)\n  }\n\n  it should \"throw exception for negative skip\" in {\n    implicit val tid: TransactionId = transId()\n\n    a[IllegalArgumentException] should be thrownBy activationStore.countActivationsInNamespace(\n      namespace = EntityPath(\"test\"),\n      name = None,\n      skip = -1,\n      context = context)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ArtifactStoreActivationsQueryBehaviors.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.WhiskQueries.TOP\nimport org.apache.openwhisk.core.entity.{EntityPath, WhiskActivation}\n\ntrait ArtifactStoreActivationsQueryBehaviors extends ArtifactStoreBehaviorBase {\n\n  it should \"list activations between given times\" in {\n    implicit val tid: TransactionId = transid()\n    val ns = newNS()\n    val activations = (1000 until 1100 by 10).map(newActivation(ns.asString, \"testact\", _))\n    activations foreach (put(activationStore, _))\n\n    val entityPath = s\"${ns.asString}/testact\"\n    waitOnView(activationStore, EntityPath(entityPath), activations.size, WhiskActivation.filtersView)\n\n    val resultSince = query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 1050),\n      List(entityPath, TOP, TOP))\n\n    resultSince.map(_.fields(\"value\")) shouldBe activations.reverse\n      .filter(_.start.toEpochMilli >= 1050)\n      .map(_.summaryAsJson)\n\n    val resultBetween = query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 1060),\n      List(entityPath, 1090, TOP))\n\n    resultBetween.map(_.fields(\"value\")) shouldBe activations.reverse\n      .filter(a => a.start.toEpochMilli >= 1060 && a.start.toEpochMilli <= 1090)\n      .map(_.summaryAsJson)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ArtifactStoreAttachmentBehaviors.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport java.io.ByteArrayOutputStream\nimport java.util.Base64\n\nimport org.apache.pekko.http.scaladsl.model.{ContentTypes, Uri}\nimport org.apache.pekko.stream.IOResult\nimport org.apache.pekko.stream.scaladsl.{Sink, StreamConverters}\nimport org.apache.pekko.util.{ByteString, ByteStringBuilder}\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.{AttachmentSupport, CacheChangeNotification, NoDocumentException}\nimport org.apache.openwhisk.core.entity.Attachments.{Attached, Attachment, Inline}\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.apache.openwhisk.core.entity.{CodeExec, DocInfo, EntityName, WhiskAction}\n\nimport scala.concurrent.duration.DurationInt\n\ntrait ArtifactStoreAttachmentBehaviors extends ArtifactStoreBehaviorBase with ExecHelpers {\n  behavior of s\"${storeType}ArtifactStore attachments\"\n\n  private val namespace = newNS()\n  private val attachmentHandler = Some(WhiskAction.attachmentHandler _)\n  private implicit val cacheUpdateNotifier: Option[CacheChangeNotification] = None\n\n  it should \"generate different attachment name on update\" in {\n    implicit val tid: TransactionId = transid()\n    val exec = javaDefault(nonInlinedCode(entityStore), Some(\"hello\"))\n    val javaAction =\n      WhiskAction(namespace, EntityName(\"attachment_unique\"), exec)\n\n    val i1 = WhiskAction.put(entityStore, javaAction, old = None).futureValue\n    val action2 = entityStore.get[WhiskAction](i1, attachmentHandler).futureValue\n\n    //Change attachment to inline one otherwise WhiskAction would not go for putAndAttach\n    val action2Updated = action2.copy(exec = exec).revision[WhiskAction](i1.rev)\n    val i2 = WhiskAction.put(entityStore, action2Updated, old = Some(action2)).futureValue\n    val action3 = entityStore.get[WhiskAction](i2, attachmentHandler).futureValue\n\n    docsToDelete += ((entityStore, i2))\n\n    attached(action2).attachmentName should not be attached(action3).attachmentName\n\n    //Check that attachment name is actually a uri\n    val attachmentUri = Uri(attached(action2).attachmentName)\n    attachmentUri.isAbsolute shouldBe true\n  }\n\n  /**\n   * This test asserts that old attachments are deleted and cannot be read again\n   */\n  it should \"fail on reading with old non inlined attachment\" in {\n    implicit val tid: TransactionId = transid()\n    val code1 = nonInlinedCode(entityStore)\n    val exec = javaDefault(code1, Some(\"hello\"))\n    val javaAction =\n      WhiskAction(namespace, EntityName(\"attachment_update_2\"), exec)\n\n    val i1 = WhiskAction.put(entityStore, javaAction, old = None).futureValue\n\n    val action2 = entityStore.get[WhiskAction](i1, attachmentHandler).futureValue\n    val code2 = nonInlinedCode(entityStore)\n    val exec2 = javaDefault(code2, Some(\"hello\"))\n    val action2Updated = action2.copy(exec = exec2).revision[WhiskAction](i1.rev)\n\n    val i2 = WhiskAction.put(entityStore, action2Updated, old = Some(action2)).futureValue\n    val action3 = entityStore.get[WhiskAction](i2, attachmentHandler).futureValue\n\n    docsToDelete += ((entityStore, i2))\n    getAttachmentBytes(i2, attached(action3)).futureValue.result() shouldBe decode(code2)\n    getAttachmentBytes(i1, attached(action2)).failed.futureValue shouldBe a[NoDocumentException]\n  }\n\n  /**\n   * Variant of previous test where read with old attachment should still work\n   * if attachment is inlined\n   */\n  it should \"work on reading with old inlined attachment\" in {\n    assumeAttachmentInliningEnabled(entityStore)\n    implicit val tid: TransactionId = transid()\n    val code1 = encodedRandomBytes(inlinedAttachmentSize(entityStore))\n    val exec = javaDefault(code1, Some(\"hello\"))\n    val javaAction =\n      WhiskAction(namespace, EntityName(\"attachment_update_2\"), exec)\n\n    val i1 = WhiskAction.put(entityStore, javaAction, old = None).futureValue\n\n    val action2 = entityStore.get[WhiskAction](i1, attachmentHandler).futureValue\n    val code2 = nonInlinedCode(entityStore)\n    val exec2 = javaDefault(code2, Some(\"hello\"))\n    val action2Updated = action2.copy(exec = exec2).revision[WhiskAction](i1.rev)\n\n    val i2 = WhiskAction.put(entityStore, action2Updated, old = Some(action2)).futureValue\n    val action3 = entityStore.get[WhiskAction](i2, attachmentHandler).futureValue\n\n    docsToDelete += ((entityStore, i2))\n    getAttachmentBytes(i2, attached(action3)).futureValue.result() shouldBe decode(code2)\n    getAttachmentBytes(i2, attached(action2)).futureValue.result() shouldBe decode(code1)\n  }\n\n  it should \"put and read large attachment\" in {\n    implicit val tid: TransactionId = transid()\n    val size = Math.max(nonInlinedAttachmentSize(entityStore), getAttachmentSizeForTest(entityStore))\n    val base64 = encodedRandomBytes(size)\n\n    val exec = javaDefault(base64, Some(\"hello\"))\n    val javaAction =\n      WhiskAction(namespace, EntityName(\"attachment_large\"), exec)\n\n    //Have more patience as reading large attachments take time specially for remote\n    //storage like Cosmos\n    implicit val patienceConfig: PatienceConfig = PatienceConfig(timeout = 1.minute)\n\n    val i1 = WhiskAction.put(entityStore, javaAction, old = None).futureValue\n    val action2 = entityStore.get[WhiskAction](i1, attachmentHandler).futureValue\n\n    val action3 = WhiskAction.get(entityStore, i1.id, i1.rev).futureValue\n\n    docsToDelete += ((entityStore, i1))\n\n    attached(action2).attachmentType shouldBe ContentTypes.`application/octet-stream`\n    attached(action2).length shouldBe Some(size)\n    attached(action2).digest should not be empty\n\n    action3.exec shouldBe exec\n    inlined(action3).value shouldBe base64\n  }\n\n  it should \"inline small attachments\" in {\n    assumeAttachmentInliningEnabled(entityStore)\n    implicit val tid: TransactionId = transid()\n    val attachmentSize = inlinedAttachmentSize(entityStore) - 1\n    val base64 = encodedRandomBytes(attachmentSize)\n\n    val exec = javaDefault(base64, Some(\"hello\"))\n    val javaAction = WhiskAction(namespace, EntityName(\"attachment_inline\"), exec)\n\n    val i1 = WhiskAction.put(entityStore, javaAction, old = None).futureValue\n    val action2 = entityStore.get[WhiskAction](i1, attachmentHandler).futureValue\n    val action3 = WhiskAction.get(entityStore, i1.id, i1.rev).futureValue\n\n    docsToDelete += ((entityStore, i1))\n\n    action3.exec shouldBe exec\n    inlined(action3).value shouldBe base64\n\n    val a = attached(action2)\n\n    val attachmentUri = Uri(a.attachmentName)\n    attachmentUri.scheme shouldBe AttachmentSupport.MemScheme\n    a.length shouldBe Some(attachmentSize)\n    a.digest should not be empty\n  }\n\n  it should \"throw NoDocumentException for non existing attachment\" in {\n    implicit val tid: TransactionId = transid()\n    val attachmentName = \"foo\"\n    val attachmentId =\n      getAttachmentStore(entityStore).map(s => s\"${s.scheme}:$attachmentName\").getOrElse(attachmentName)\n\n    val sink = StreamConverters.fromOutputStream(() => new ByteArrayOutputStream())\n    entityStore\n      .readAttachment[IOResult](\n        DocInfo ! (\"non-existing-doc\", \"42\"),\n        Attached(attachmentId, ContentTypes.`application/octet-stream`),\n        sink)\n      .failed\n      .futureValue shouldBe a[NoDocumentException]\n  }\n\n  it should \"delete attachment on document delete\" in {\n    val attachmentStore = getAttachmentStore(entityStore)\n    assume(attachmentStore.isDefined, \"ArtifactStore does not have attachmentStore configured\")\n\n    implicit val tid: TransactionId = transid()\n    val size = nonInlinedAttachmentSize(entityStore)\n    val base64 = encodedRandomBytes(size)\n\n    val exec = javaDefault(base64, Some(\"hello\"))\n    val javaAction =\n      WhiskAction(namespace, EntityName(\"attachment_unique\"), exec)\n\n    val i1 = WhiskAction.put(entityStore, javaAction, old = None).futureValue\n    val action2 = entityStore.get[WhiskAction](i1, attachmentHandler).futureValue\n\n    WhiskAction.del(entityStore, i1).futureValue shouldBe true\n\n    val attachmentName = Uri(attached(action2).attachmentName).path.toString\n    attachmentStore.get\n      .readAttachment(i1.id, attachmentName, byteStringSink())\n      .failed\n      .futureValue shouldBe a[NoDocumentException]\n  }\n\n  private def attached(a: WhiskAction): Attached =\n    a.exec.asInstanceOf[CodeExec[Attachment[Nothing]]].code.asInstanceOf[Attached]\n\n  private def inlined(a: WhiskAction): Inline[String] =\n    a.exec.asInstanceOf[CodeExec[Attachment[String]]].code.asInstanceOf[Inline[String]]\n\n  private def getAttachmentBytes(docInfo: DocInfo, attached: Attached) = {\n    implicit val tid: TransactionId = transid()\n    entityStore.readAttachment(docInfo, attached, byteStringSink())\n  }\n\n  private def byteStringSink() = {\n    Sink.fold[ByteStringBuilder, ByteString](new ByteStringBuilder)((builder, b) => builder ++= b)\n  }\n\n  private def decode(s: String): ByteString = ByteString(Base64.getDecoder.decode(s))\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ArtifactStoreBehavior.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\ntrait ArtifactStoreBehavior\n    extends ArtifactStoreBehaviorBase\n    with ArtifactStoreQueryBehaviors\n    with ArtifactStoreCRUDBehaviors\n    with ArtifactStoreSubjectQueryBehaviors\n    with ArtifactStoreWhisksQueryBehaviors\n    with ArtifactStoreActivationsQueryBehaviors\n    with ArtifactStoreAttachmentBehaviors\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ArtifactStoreBehaviorBase.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport java.time.Instant\n\nimport common.{StreamLogging, WskActorSystem}\nimport org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.{JsObject, JsValue}\nimport org.apache.openwhisk.common.{TransactionId, WhiskInstants}\nimport org.apache.openwhisk.core.database.memory.MemoryAttachmentStore\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.database.test.behavior.ArtifactStoreTestUtil.storeAvailable\nimport org.apache.openwhisk.core.database.{ArtifactStore, AttachmentStore, StaleParameter}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.utils.JsHelpers\n\nimport scala.util.{Random, Try}\n\ntrait ArtifactStoreBehaviorBase\n    extends AnyFlatSpec\n    with ScalaFutures\n    with Matchers\n    with StreamLogging\n    with DbUtils\n    with WskActorSystem\n    with IntegrationPatience\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with WhiskInstants {\n\n  //Bring in sync the timeout used by ScalaFutures and DBUtils\n  implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = dbOpTimeout)\n\n  protected val prefix = s\"artifactTCK_${Random.alphanumeric.take(4).mkString}\"\n\n  def authStore: ArtifactStore[WhiskAuth]\n  def entityStore: ArtifactStore[WhiskEntity]\n  def activationStore: ArtifactStore[WhiskActivation]\n\n  def storeType: String\n\n  override def afterEach(): Unit = {\n    cleanup()\n    stream.reset()\n  }\n\n  override def afterAll(): Unit = {\n    assertAttachmentStoreIsEmpty()\n    println(\"Shutting down store connections\")\n    authStore.shutdown()\n    entityStore.shutdown()\n    activationStore.shutdown()\n    super.afterAll()\n    assertAttachmentStoresAreClosed()\n  }\n\n  override protected def withFixture(test: NoArgTest) = {\n    assume(storeAvailable(storeAvailableCheck), s\"$storeType not configured or available\")\n    val outcome = super.withFixture(test)\n    if (outcome.isFailed) {\n      println(logLines.mkString(\"\\n\"))\n    }\n    outcome\n  }\n\n  protected def storeAvailableCheck: Try[Any] = Try(true)\n  //~----------------------------------------< utility methods >\n\n  protected def query[A <: WhiskEntity](\n    db: ArtifactStore[A],\n    table: String,\n    startKey: List[Any],\n    endKey: List[Any],\n    skip: Int = 0,\n    limit: Int = 0,\n    includeDocs: Boolean = false,\n    descending: Boolean = true,\n    reduce: Boolean = false,\n    stale: StaleParameter = StaleParameter.No)(implicit transid: TransactionId): List[JsObject] = {\n    db.query(table, startKey, endKey, skip, limit, includeDocs, descending, reduce, stale).futureValue\n  }\n\n  protected def count[A <: WhiskEntity](\n    db: ArtifactStore[A],\n    table: String,\n    startKey: List[Any],\n    endKey: List[Any],\n    skip: Int = 0,\n    stale: StaleParameter = StaleParameter.No)(implicit transid: TransactionId): Long = {\n    db.count(table, startKey, endKey, skip, stale).futureValue\n  }\n\n  protected def getWhiskAuth(doc: DocInfo)(implicit transid: TransactionId) = {\n    authStore.get[WhiskAuth](doc).futureValue\n  }\n\n  protected def newAuth() = {\n    val subject = Subject()\n    val namespaces = Set(wskNS(\"foo\"))\n    WhiskAuth(subject, namespaces)\n  }\n\n  protected def wskNS(name: String) = {\n    val uuid = UUID()\n    WhiskNamespace(Namespace(EntityName(name), uuid), BasicAuthenticationAuthKey(uuid, Secret()))\n  }\n\n  private val exec = BlackBoxExec(ExecManifest.ImageName(\"image\"), None, None, native = false, binary = false)\n\n  protected def newAction(ns: EntityPath): WhiskAction = {\n    WhiskAction(ns, aname(), exec)\n  }\n\n  protected def newActivation(ns: String, actionName: String, start: Long): WhiskActivation = {\n    WhiskActivation(\n      EntityPath(ns),\n      EntityName(actionName),\n      Subject(),\n      ActivationId.generate(),\n      Instant.ofEpochMilli(start),\n      Instant.ofEpochMilli(start + 1000))\n  }\n\n  protected def aname() = EntityName(s\"${prefix}_name_${randomString()}\")\n\n  protected def newNS() = EntityPath(s\"${prefix}_ns_${randomString()}\")\n\n  private def randomString() = Random.alphanumeric.take(5).mkString\n\n  protected def getJsObject(js: JsObject, fields: String*): JsObject = {\n    JsHelpers.getFieldPath(js, fields: _*).get.asJsObject\n  }\n\n  protected def getJsField(js: JsObject, subObject: String, fieldName: String): JsValue = {\n    js.fields(subObject).asJsObject().fields(fieldName)\n  }\n\n  protected def getAttachmentStore(store: ArtifactStore[_]): Option[AttachmentStore]\n\n  protected def getAttachmentCount(store: AttachmentStore): Option[Int] = store match {\n    case s: MemoryAttachmentStore => Some(s.attachmentCount)\n    case _                        => None\n  }\n\n  protected def getAttachmentSizeForTest(store: ArtifactStore[_]): Int = {\n    val mb = getAttachmentStore(store).map(_ => 5.MB).getOrElse(maxAttachmentSizeWithoutAttachmentStore)\n    mb.toBytes.toInt\n  }\n\n  protected def maxAttachmentSizeWithoutAttachmentStore: ByteSize = 5.MB\n\n  private def assertAttachmentStoreIsEmpty(): Unit = {\n    Seq(authStore, entityStore, activationStore).foreach { s =>\n      for {\n        as <- getAttachmentStore(s)\n        count <- getAttachmentCount(as)\n      } require(count == 0, s\"AttachmentStore not empty after all runs - $count\")\n    }\n  }\n\n  private def assertAttachmentStoresAreClosed(): Unit = {\n    Seq(authStore, entityStore, activationStore).foreach { s =>\n      getAttachmentStore(s).foreach {\n        case s: MemoryAttachmentStore => require(s.isClosed, \"AttachmentStore was not closed\")\n        case _                        =>\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ArtifactStoreCRUDBehaviors.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport java.time.Instant\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.{\n  DocumentConflictException,\n  DocumentProvider,\n  DocumentRevisionMismatchException,\n  NoDocumentException\n}\nimport org.apache.openwhisk.core.entity._\n\ntrait ArtifactStoreCRUDBehaviors extends ArtifactStoreBehaviorBase {\n\n  behavior of s\"${storeType}ArtifactStore put\"\n\n  it should \"put document and get a revision 1\" in {\n    implicit val tid: TransactionId = transid()\n    val doc = put(authStore, newAuth())\n    doc.rev.empty shouldBe false\n  }\n\n  it should \"put and update document\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    val doc = put(authStore, auth)\n\n    val auth2 =\n      getWhiskAuth(doc)\n        .copy(namespaces = Set(wskNS(\"foo1\")))\n        .revision[WhiskAuth](doc.rev)\n    val doc2 = put(authStore, auth2)\n\n    doc2.rev should not be doc.rev\n    doc2.rev.empty shouldBe false\n  }\n\n  it should \"put delete and then recreate document with same id with different rev\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    val doc = put(authStore, auth)\n\n    delete(authStore, doc) shouldBe true\n\n    val auth2 = auth.copy(namespaces = Set(wskNS(\"foo1\")))\n    val doc2 = put(authStore, auth2)\n\n    doc2.rev should not be doc.rev\n    doc2.rev.empty shouldBe false\n  }\n\n  it should \"throw DocumentConflictException when updated with old revision\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    val doc = put(authStore, auth)\n\n    val auth2 = getWhiskAuth(doc).copy(namespaces = Set(wskNS(\"foo1\"))).revision[WhiskAuth](doc.rev)\n    val doc2 = put(authStore, auth2)\n\n    //Updated with _rev set to older one\n    val auth3 = getWhiskAuth(doc2).copy(namespaces = Set(wskNS(\"foo2\"))).revision[WhiskAuth](doc.rev)\n    intercept[DocumentConflictException] {\n      put(authStore, auth3)\n    }\n  }\n\n  it should \"throw DocumentConflictException if document with same id is inserted twice\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    val doc = put(authStore, auth)\n\n    intercept[DocumentConflictException] {\n      put(authStore, auth)\n    }\n  }\n\n  it should \"work if same document was deleted earlier\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    //1. Create a document\n    val doc = put(authStore, auth)\n\n    //2. Now delete the document\n    delete(authStore, doc) shouldBe true\n\n    //3. Now recreate the same document.\n    val doc2 = put(authStore, auth)\n\n    //Recreating a deleted document should work\n    doc2.rev.empty shouldBe false\n  }\n\n  behavior of s\"${storeType}ArtifactStore delete\"\n\n  it should \"deletes existing document\" in {\n    implicit val tid: TransactionId = transid()\n    val doc = put(authStore, newAuth())\n    delete(authStore, doc) shouldBe true\n  }\n\n  it should \"throws IllegalArgumentException when deleting without revision\" in {\n    intercept[IllegalArgumentException] {\n      implicit val tid: TransactionId = transid()\n      delete(authStore, DocInfo(\"doc-with-empty-revision\"))\n    }\n  }\n\n  it should \"throws NoDocumentException when document does not exist\" in {\n    intercept[NoDocumentException] {\n      implicit val tid: TransactionId = transid()\n      delete(authStore, DocInfo ! (\"non-existing-doc\", \"42\"))\n    }\n  }\n\n  it should \"throws DocumentConflictException when revision does not match\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    val doc = put(authStore, auth)\n\n    val auth2 = getWhiskAuth(doc).copy(namespaces = Set(wskNS(\"foo1\"))).revision[WhiskAuth](doc.rev)\n    val doc2 = put(authStore, auth2)\n\n    intercept[DocumentConflictException] {\n      delete(authStore, doc)\n    }\n  }\n\n  behavior of s\"${storeType}ArtifactStore get\"\n\n  it should \"get existing entity matching id and rev\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    val doc = put(authStore, auth)\n    val authFromGet = getWhiskAuth(doc)\n    authFromGet shouldBe auth\n    authFromGet.docinfo.rev shouldBe doc.rev\n  }\n\n  it should \"get existing entity matching id only\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    val doc = put(authStore, auth)\n    val authFromGet = getWhiskAuth(doc)\n    authFromGet shouldBe auth\n  }\n\n  it should \"get entity with timestamp\" in {\n    implicit val tid: TransactionId = transid()\n    val activation = WhiskActivation(\n      EntityPath(\"testnamespace\"),\n      EntityName(\"activation1\"),\n      Subject(),\n      ActivationId.generate(),\n      start = Instant.now.inMills,\n      end = Instant.now.inMills)\n    val activationDoc = put(activationStore, activation)\n    val activationFromDb = activationStore.get[WhiskActivation](activationDoc).futureValue\n    activationFromDb shouldBe activation\n  }\n\n  it should \"throws DocumentRevisionMismatchException when document revision does not match\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    val doc = put(authStore, auth)\n\n    val auth2 = getWhiskAuth(doc).copy(namespaces = Set(wskNS(\"foo1\"))).revision[WhiskAuth](doc.rev)\n    val doc2 = put(authStore, auth2)\n\n    authStore.get[WhiskAuth](doc).failed.futureValue shouldBe a[DocumentRevisionMismatchException]\n\n    val authFromGet = getWhiskAuth(doc2)\n    authFromGet shouldBe auth2\n  }\n\n  it should \"throws NoDocumentException when document does not exist\" in {\n    implicit val tid: TransactionId = transid()\n    authStore.get[WhiskAuth](DocInfo(\"non-existing-doc\")).failed.futureValue shouldBe a[NoDocumentException]\n  }\n\n  it should \"not get a deleted document\" in {\n    implicit val tid: TransactionId = transid()\n    val auth = newAuth()\n    //1. Create a document\n    val docInfo = put(authStore, auth)\n\n    //2. Now delete the document\n    delete(authStore, docInfo) shouldBe true\n\n    //3. Now getting a deleted document should fail\n    authStore.get[WhiskAuth](docInfo).failed.futureValue shouldBe a[NoDocumentException]\n\n    //Check get by id flow also which return none for such \"soft\" deleted document\n    authStore match {\n      case provider: DocumentProvider =>\n        provider.get(docInfo.id).futureValue shouldBe None\n      case _ =>\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ArtifactStoreQueryBehaviors.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport spray.json.{JsArray, JsNumber, JsObject, JsString}\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.WhiskQueries.TOP\nimport org.apache.openwhisk.core.entity.{EntityPath, WhiskAction, WhiskActivation, WhiskEntity}\n\ntrait ArtifactStoreQueryBehaviors extends ArtifactStoreBehaviorBase {\n\n  behavior of s\"${storeType}ArtifactStore query\"\n\n  it should \"find single entity\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val action = newAction(ns)\n    val docInfo = put(entityStore, action)\n\n    waitOnView(entityStore, ns.root, 1, WhiskAction.view)\n    val result = query[WhiskEntity](\n      entityStore,\n      WhiskAction.view.name,\n      List(ns.asString, 0),\n      List(ns.asString, TOP, TOP),\n      includeDocs = true)\n\n    result should have length 1\n\n    def js = result.head\n    js.fields(\"id\") shouldBe JsString(docInfo.id.id)\n    js.fields(\"key\") shouldBe JsArray(JsString(ns.asString), JsNumber(action.updated.toEpochMilli))\n    js.fields.get(\"value\") shouldBe defined\n    js.fields.get(\"doc\") shouldBe defined\n    js.fields(\"value\") shouldBe action.summaryAsJson\n    dropRev(js.fields(\"doc\").asJsObject) shouldBe action.toDocumentRecord\n  }\n\n  it should \"not have doc with includeDocs = false\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val action = newAction(ns)\n    val docInfo = put(entityStore, action)\n\n    waitOnView(entityStore, ns.root, 1, WhiskAction.view)\n    val result =\n      query[WhiskEntity](entityStore, WhiskAction.view.name, List(ns.asString, 0), List(ns.asString, TOP, TOP))\n\n    result should have length 1\n\n    def js = result.head\n    js.fields(\"id\") shouldBe JsString(docInfo.id.id)\n    js.fields(\"key\") shouldBe JsArray(JsString(ns.asString), JsNumber(action.updated.toEpochMilli))\n    js.fields.get(\"value\") shouldBe defined\n    js.fields.get(\"doc\") shouldBe None\n    js.fields(\"value\") shouldBe action.summaryAsJson\n  }\n\n  it should \"find all entities\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val entities = Seq(newAction(ns), newAction(ns))\n\n    entities foreach {\n      put(entityStore, _)\n    }\n\n    waitOnView(entityStore, ns.root, 2, WhiskAction.view)\n    val result =\n      query[WhiskEntity](entityStore, WhiskAction.view.name, List(ns.asString, 0), List(ns.asString, TOP, TOP))\n\n    result should have length entities.length\n    result.map(_.fields(\"value\")) should contain theSameElementsAs entities.map(_.summaryAsJson)\n  }\n\n  it should \"exclude deleted entities\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val entities = Seq(newAction(ns), newAction(ns), newAction(ns))\n    val validEntities = entities.tail\n    val infos = entities.map(put(entityStore, _))\n\n    delete(entityStore, infos.head)\n\n    waitOnView(entityStore, ns.root, 2, WhiskAction.view)\n    val result =\n      query[WhiskEntity](entityStore, WhiskAction.view.name, List(ns.asString, 0), List(ns.asString, TOP, TOP))\n\n    result should have length validEntities.length\n    result.map(_.fields(\"value\")) should contain theSameElementsAs validEntities.map(_.summaryAsJson)\n  }\n\n  it should \"return result in sorted order\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val activations = (1000 until 1100 by 10).map(newActivation(ns.asString, \"testact\", _))\n    activations foreach (put(activationStore, _))\n\n    val entityPath = s\"${ns.asString}/testact\"\n    waitOnView(activationStore, EntityPath(entityPath), activations.size, WhiskActivation.filtersView)\n\n    val resultDescending = query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 0),\n      List(entityPath, TOP, TOP))\n\n    resultDescending should have length activations.length\n    resultDescending.map(getJsField(_, \"value\", \"start\")) shouldBe activations\n      .map(_.summaryAsJson.fields(\"start\"))\n      .reverse\n\n    val resultAscending = query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 0),\n      List(entityPath, TOP, TOP),\n      descending = false)\n\n    resultAscending.map(getJsField(_, \"value\", \"start\")) shouldBe activations.map(_.summaryAsJson.fields(\"start\"))\n  }\n\n  it should \"support skipping results\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val activations = (1000 until 1100 by 10).map(newActivation(ns.asString, \"testact\", _))\n    activations foreach (put(activationStore, _))\n\n    val entityPath = s\"${ns.asString}/testact\"\n    waitOnView(activationStore, EntityPath(entityPath), activations.size, WhiskActivation.filtersView)\n    val result = query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 0),\n      List(entityPath, TOP, TOP),\n      skip = 5,\n      descending = false)\n\n    result.map(getJsField(_, \"value\", \"start\")) shouldBe activations.map(_.summaryAsJson.fields(\"start\")).drop(5)\n  }\n\n  it should \"support limiting results\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val activations = (1000 until 1100 by 10).map(newActivation(ns.asString, \"testact\", _))\n    activations foreach (put(activationStore, _))\n\n    val entityPath = s\"${ns.asString}/testact\"\n    waitOnView(activationStore, EntityPath(entityPath), activations.size, WhiskActivation.filtersView)\n    val result = query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 0),\n      List(entityPath, TOP, TOP),\n      limit = 5,\n      descending = false)\n\n    result.map(getJsField(_, \"value\", \"start\")) shouldBe activations.map(_.summaryAsJson.fields(\"start\")).take(5)\n  }\n\n  it should \"support including complete docs\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val activations = (1000 until 1100 by 10).map(newActivation(ns.asString, \"testact\", _))\n    activations foreach (put(activationStore, _))\n\n    val entityPath = s\"${ns.asString}/testact\"\n    waitOnView(activationStore, EntityPath(entityPath), activations.size, WhiskActivation.filtersView)\n    val result = query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 0),\n      List(entityPath, TOP, TOP),\n      includeDocs = true,\n      descending = false)\n\n    //Drop the _rev field as activations do not have that field\n    result.map(js => JsObject(getJsObject(js, \"doc\").fields - \"_rev\")) shouldBe activations.map(_.toDocumentRecord)\n  }\n\n  it should \"throw exception for negative limits and skip\" in {\n    implicit val tid: TransactionId = transid()\n    a[IllegalArgumentException] should be thrownBy query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(\"foo\", 0),\n      List(\"foo\", TOP, TOP),\n      limit = -1)\n\n    a[IllegalArgumentException] should be thrownBy query[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(\"foo\", 0),\n      List(\"foo\", TOP, TOP),\n      skip = -1)\n  }\n\n  behavior of s\"${storeType}ArtifactStore count\"\n\n  it should \"should match all created activations\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val activations = (1000 until 1100 by 10).map(newActivation(ns.asString, \"testact\", _))\n    activations foreach (put(activationStore, _))\n\n    val entityPath = s\"${ns.asString}/testact\"\n    waitOnView(activationStore, EntityPath(entityPath), activations.size, WhiskActivation.filtersView)\n    val result = count[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 0),\n      List(entityPath, TOP, TOP))\n\n    result shouldBe 10\n  }\n\n  it should \"count with skip\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val activations = (1000 until 1100 by 10).map(newActivation(ns.asString, \"testact\", _))\n    activations foreach (put(activationStore, _))\n\n    val entityPath = s\"${ns.asString}/testact\"\n    waitOnView(activationStore, EntityPath(entityPath), activations.size, WhiskActivation.filtersView)\n    val result = count[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 0),\n      List(entityPath, TOP, TOP),\n      skip = 4)\n\n    result shouldBe 10 - 4\n\n    val result2 = count[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(entityPath, 0),\n      List(entityPath, TOP, TOP),\n      skip = 1000)\n\n    result2 shouldBe 0\n  }\n\n  it should \"throw exception for negative skip\" in {\n    implicit val tid: TransactionId = transid()\n    a[IllegalArgumentException] should be thrownBy count[WhiskActivation](\n      activationStore,\n      WhiskActivation.filtersView.name,\n      List(\"foo\", 0),\n      List(\"foo\", TOP, TOP),\n      skip = -1)\n  }\n\n  private def dropRev(js: JsObject): JsObject = {\n    JsObject(js.fields - \"_rev\")\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ArtifactStoreSubjectQueryBehaviors.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport spray.json.{JsBoolean, JsObject, JsString}\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.NoDocumentException\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.invoker.NamespaceBlacklist\nimport org.apache.openwhisk.utils.JsHelpers\n\ntrait ArtifactStoreSubjectQueryBehaviors extends ArtifactStoreBehaviorBase {\n\n  behavior of s\"${storeType}ArtifactStore query subjects\"\n\n  it should \"find subject by namespace\" in {\n    implicit val tid: TransactionId = transid()\n    val uuid1 = UUID()\n    val uuid2 = UUID()\n    val ak1 = BasicAuthenticationAuthKey(uuid1, Secret())\n    val ak2 = BasicAuthenticationAuthKey(uuid2, Secret())\n    val ns1 = Namespace(aname(), uuid1)\n    val ns2 = Namespace(aname(), uuid2)\n    val subs =\n      Array(WhiskAuth(Subject(), Set(WhiskNamespace(ns1, ak1))), WhiskAuth(Subject(), Set(WhiskNamespace(ns2, ak2))))\n    subs foreach (put(authStore, _))\n\n    waitOnView(authStore, ak1, 1)\n    waitOnView(authStore, ak2, 1)\n\n    val s1 = Identity.get(authStore, ns1.name).futureValue\n    s1.subject shouldBe subs(0).subject\n\n    val s2 = Identity.get(authStore, ak2).futureValue\n    s2.subject shouldBe subs(1).subject\n  }\n\n  it should \"not get blocked subject\" in {\n    implicit val tid: TransactionId = transid()\n    val uuid1 = UUID()\n    val ns1 = Namespace(aname(), uuid1)\n    val ak1 = BasicAuthenticationAuthKey()\n    val auth = new ExtendedAuth(Subject(), Set(WhiskNamespace(ns1, ak1)), blocked = true)\n    put(authStore, auth)\n\n    Identity.get(authStore, ns1.name).failed.futureValue shouldBe a[NoDocumentException]\n  }\n\n  it should \"not find subject when authKey matches partially\" in {\n    implicit val tid: TransactionId = transid()\n    val uuid1 = UUID()\n    val uuid2 = UUID()\n    val ak1 = BasicAuthenticationAuthKey(uuid1, Secret())\n    val ak2 = BasicAuthenticationAuthKey(uuid2, Secret())\n    val ns1 = Namespace(aname(), uuid1)\n    val ns2 = Namespace(aname(), uuid2)\n\n    val auth = WhiskAuth(\n      Subject(),\n      Set(\n        WhiskNamespace(ns1, BasicAuthenticationAuthKey(ak1.uuid, ak2.key)),\n        WhiskNamespace(ns2, BasicAuthenticationAuthKey(ak2.uuid, ak1.key))))\n\n    put(authStore, auth)\n\n    waitOnView(authStore, BasicAuthenticationAuthKey(ak1.uuid, ak2.key), 1)\n    Identity.get(authStore, ak1).failed.futureValue shouldBe a[NoDocumentException]\n  }\n\n  it should \"should throw NoDocumentException for non existing namespaces\" in {\n    implicit val tid: TransactionId = transid()\n    val nonExistingNamesSpace = \"nonExistingNamesSpace\"\n    Identity.get(authStore, EntityName(nonExistingNamesSpace)).failed.futureValue shouldBe a[NoDocumentException]\n  }\n\n  it should \"should throw NoDocumentException for non existing authKeys\" in {\n    implicit val tid: TransactionId = transid()\n    val nonExistingUUID = \"nonExistingUUID\"\n    val nonExistingSecret = \"nonExistingSecret\"\n    Identity\n      .get(authStore, BasicAuthenticationAuthKey(UUID(nonExistingUUID), Secret()))\n      .failed\n      .futureValue shouldBe a[NoDocumentException]\n  }\n\n  it should \"find subject having multiple namespaces\" in {\n    implicit val tid: TransactionId = transid()\n    val uuid1 = UUID()\n    val uuid2 = UUID()\n    val ak1 = BasicAuthenticationAuthKey(uuid1, Secret())\n    val ak2 = BasicAuthenticationAuthKey(uuid2, Secret())\n    val ns1 = Namespace(aname(), uuid1)\n    val ns2 = Namespace(aname(), uuid2)\n\n    val auth = WhiskAuth(\n      Subject(ns1.name.name),\n      Set(\n        WhiskNamespace(ns1, BasicAuthenticationAuthKey(ak1.uuid, ak1.key)),\n        WhiskNamespace(ns2, BasicAuthenticationAuthKey(ak2.uuid, ak2.key))))\n\n    put(authStore, auth)\n\n    waitOnView(authStore, BasicAuthenticationAuthKey(ak1.uuid, ak1.key), 1)\n\n    val i1 = Identity.get(authStore, ns1.name).futureValue\n    i1.subject shouldBe auth.subject\n    i1.namespace shouldBe ns1\n\n    //Also check if all results returned match the provided namespace\n    val seq = Identity.list(authStore, List(ns1.name.asString), limit = 100).futureValue\n    seq.foreach { js =>\n      JsHelpers.getFieldPath(js, \"value\", \"namespace\").get shouldBe JsString(i1.namespace.name.asString)\n    }\n  }\n\n  it should \"find subject by namespace with limits\" in {\n    implicit val tid: TransactionId = transid()\n    val uuid1 = UUID()\n    val uuid2 = UUID()\n    val ak1 = BasicAuthenticationAuthKey(uuid1, Secret())\n    val ak2 = BasicAuthenticationAuthKey(uuid2, Secret())\n    val name1 = Namespace(aname(), uuid1)\n    val name2 = Namespace(aname(), uuid2)\n    val subs = Array(\n      WhiskAuth(Subject(), Set(WhiskNamespace(name1, ak1))),\n      WhiskAuth(Subject(), Set(WhiskNamespace(name2, ak2))))\n    subs foreach (put(authStore, _))\n\n    waitOnView(authStore, ak1, 1)\n    waitOnView(authStore, ak2, 1)\n\n    val limits = UserLimits(invocationsPerMinute = Some(7), firesPerMinute = Some(31))\n    put(authStore, new LimitEntity(name1.name, limits))\n\n    val i = Identity.get(authStore, name1.name).futureValue\n    i.subject shouldBe subs(0).subject\n    i.limits shouldBe limits\n  }\n\n  it should \"find blacklisted namespaces\" in {\n    implicit val tid: TransactionId = transid()\n\n    val n1 = aname()\n    val n2 = aname()\n    val n3 = aname()\n    val n4 = aname()\n    val n5 = aname()\n\n    val uuid1 = UUID()\n    val uuid2 = UUID()\n    val ak1 = BasicAuthenticationAuthKey(uuid1, Secret())\n    val ak2 = BasicAuthenticationAuthKey(uuid2, Secret())\n\n    //Create 3 limits entry where one has limits > 0 thus non blacklisted\n    //And one blocked subject with 2 namespaces\n    val limitsAndAuths = Seq(\n      new LimitEntity(n1, UserLimits(invocationsPerMinute = Some(0))),\n      new LimitEntity(n2, UserLimits(concurrentInvocations = Some(0))),\n      new LimitEntity(n3, UserLimits(invocationsPerMinute = Some(7), concurrentInvocations = Some(7))),\n      new ExtendedAuth(\n        Subject(),\n        Set(WhiskNamespace(Namespace(n4, uuid1), ak1), WhiskNamespace(Namespace(n5, uuid2), ak2)),\n        blocked = true))\n\n    limitsAndAuths foreach (put(authStore, _))\n\n    //2 for limits\n    //2 for 2 namespace in user blocked\n    waitOnView(authStore, 2 + 2, NamespaceBlacklist.view)\n\n    //Use contains assertion to ensure that even if same db is used by other setup\n    //we at least get our expected entries\n    val blacklist = new NamespaceBlacklist(authStore)\n    blacklist\n      .refreshBlacklist()\n      .futureValue should contain allElementsOf Seq(n1, n2, n4, n5).map(_.asString).toSet\n  }\n\n  private class LimitEntity(name: EntityName, limits: UserLimits) extends WhiskAuth(Subject(), Set.empty) {\n    override def docid = DocId(s\"${name.name}/limits\")\n\n    //There is no api to write limits. So piggy back on WhiskAuth but replace auth json\n    //with limits!\n    override def toJson = UserLimits.serdes.write(limits).asJsObject\n  }\n\n  private class ExtendedAuth(subject: Subject, namespaces: Set[WhiskNamespace], blocked: Boolean)\n      extends WhiskAuth(subject, namespaces) {\n    override def toJson = JsObject(super.toJson.fields + (\"blocked\" -> JsBoolean(blocked)))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ArtifactStoreTestUtil.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport common.TestUtils\n\nimport scala.util.{Failure, Success, Try}\n\nobject ArtifactStoreTestUtil {\n\n  def storeAvailable(storeAvailableCheck: Try[Any]): Boolean = {\n    storeAvailableCheck match {\n      case Success(_) => true\n      case Failure(x) =>\n        //If running on master on main repo build tests MUST be run\n        //For non main repo runs like in fork or for PR its fine for test\n        //to be cancelled\n        if (TestUtils.isBuildingOnMainRepo) throw x else false\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/database/test/behavior/ArtifactStoreWhisksQueryBehaviors.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database.test.behavior\n\nimport java.time.Instant\n\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.entity.WhiskQueries.TOP\nimport org.apache.openwhisk.core.entity._\n\ntrait ArtifactStoreWhisksQueryBehaviors extends ArtifactStoreBehaviorBase {\n\n  behavior of s\"${storeType}ArtifactStore query public packages\"\n\n  it should \"only list published entities\" in {\n    implicit val tid: TransactionId = transid()\n    val ns = newNS()\n    val n1 = aname()\n    val pkgs = Array(\n      WhiskPackage(ns, aname()),\n      WhiskPackage(ns, aname(), publish = true),\n      WhiskPackage(ns, aname(), publish = true, binding = Some(Binding(aname(), aname()))),\n      WhiskPackage(ns, aname(), binding = Some(Binding(aname(), aname()))))\n\n    pkgs foreach (put(entityStore, _))\n\n    waitOnView(entityStore, WhiskPackage, ns, pkgs.length)\n\n    val result =\n      query[WhiskEntity](entityStore, WhiskPackage.view.name, List(ns.asString, 0), List(ns.asString, TOP, TOP))\n    result.size shouldBe pkgs.length\n\n    val resultPublic =\n      query[WhiskEntity](\n        entityStore,\n        WhiskPackage.publicPackagesView.name,\n        List(ns.asString, 0),\n        List(ns.asString, TOP, TOP))\n\n    resultPublic.size shouldBe 1\n    resultPublic.head.fields(\"value\") shouldBe pkgs(1).summaryAsJson\n  }\n\n  it should \"list packages between given times\" in {\n    implicit val tid: TransactionId = transid()\n\n    val ns = newNS()\n    val pkgs = (1000 until 1100 by 10).map(new TestWhiskPackage(ns, aname(), _))\n\n    pkgs foreach (put(entityStore, _))\n\n    waitOnView(entityStore, WhiskPackage, ns, pkgs.length)\n\n    val resultSince =\n      query[WhiskEntity](entityStore, WhiskPackage.view.name, List(ns.asString, 1050), List(ns.asString, TOP, TOP))\n\n    resultSince.map(_.fields(\"value\")) shouldBe pkgs.reverse.filter(_.updated.toEpochMilli >= 1050).map(_.summaryAsJson)\n\n    val resultBetween =\n      query[WhiskEntity](entityStore, WhiskPackage.view.name, List(ns.asString, 1050), List(ns.asString, 1090, TOP))\n    resultBetween.map(_.fields(\"value\")) shouldBe pkgs.reverse\n      .filter(p => p.updated.toEpochMilli >= 1050 && p.updated.toEpochMilli <= 1090)\n      .map(_.summaryAsJson)\n  }\n\n  private class TestWhiskPackage(override val namespace: EntityPath, override val name: EntityName, updatedTest: Long)\n      extends WhiskPackage(namespace, name) {\n    //Not possible to easily control the updated so need to use this workaround\n    override val updated = Instant.ofEpochMilli(updatedTest)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/ActivationCompatTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport org.apache.openwhisk.common.WhiskInstants\nimport org.apache.openwhisk.core.connector.Activation\nimport org.apache.openwhisk.core.entity.{\n  ActionLimits,\n  ActivationId,\n  ActivationResponse,\n  EntityName,\n  EntityPath,\n  LogLimit,\n  MemoryLimit,\n  Parameters,\n  Subject,\n  TimeLimit,\n  WhiskActivation\n}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json._\n\nimport scala.concurrent.duration._\nimport org.apache.openwhisk.core.entity.size._\n\n@RunWith(classOf[JUnitRunner])\nclass ActivationCompatTests extends AnyFlatSpec with Matchers with WhiskInstants with DefaultJsonProtocol {\n  behavior of \"ActivationResponse\"\n\n  val activationResponseJs = \"\"\"\n     |{\n     |  \"result\": {\n     |    \"res\": \"hello\"\n     |  },\n     |  \"statusCode\": 0\n     |}\"\"\".stripMargin.parseJson\n\n  val whiskActivationJs = \"\"\"\n    |{\n    |  \"activationId\": \"be97c2fed5dc43d097c2fed5dc73d085\",\n    |  \"annotations\": [{\n    |    \"key\": \"causedBy\",\n    |    \"value\": \"sequence\"\n    |  }, {\n    |    \"key\": \"path\",\n    |    \"value\": \"ns2/a\"\n    |  }, {\n    |    \"key\": \"waitTime\",\n    |    \"value\": 5\n    |  }, {\n    |    \"key\": \"kind\",\n    |    \"value\": \"testkind\"\n    |  }, {\n    |    \"key\": \"limits\",\n    |    \"value\": {\n    |      \"concurrency\": 1,\n    |      \"logs\": 1,\n    |      \"memory\": 128,\n    |      \"timeout\": 1000\n    |    }\n    |  }, {\n    |    \"key\": \"initTime\",\n    |    \"value\": 10\n    |  }],\n    |  \"duration\": 123,\n    |  \"end\": 1570013740007,\n    |  \"logs\": [],\n    |  \"name\": \"a\",\n    |  \"namespace\": \"ns\",\n    |  \"publish\": false,\n    |  \"response\": {\n    |    \"result\": {\n    |      \"res\": 1\n    |    },\n    |    \"statusCode\": 0\n    |  },\n    |  \"start\": 1570013740005,\n    |  \"subject\": \"anon-HfJWZZSG9YE38Y8DJbgp9Xn0YyN\",\n    |  \"version\": \"0.0.1\"\n    |}\"\"\".stripMargin.parseJson\n\n  val whiskActivationErrorJs = \"\"\"\n    |{\n    |  \"activationId\": \"be97c2fed5dc43d097c2fed5dc73d085\",\n    |  \"annotations\": [{\n    |    \"key\": \"causedBy\",\n    |    \"value\": \"sequence\"\n    |  }, {\n    |    \"key\": \"path\",\n    |    \"value\": \"ns2/a\"\n    |  }, {\n    |    \"key\": \"waitTime\",\n    |    \"value\": 5\n    |  }, {\n    |    \"key\": \"kind\",\n    |    \"value\": \"testkind\"\n    |  }, {\n    |    \"key\": \"limits\",\n    |    \"value\": {\n    |      \"concurrency\": 1,\n    |      \"memory\": 128,\n    |      \"timeout\": 1000\n    |    }\n    |  }, {\n    |    \"key\": \"initTime\",\n    |    \"value\": 10\n    |  }],\n    |  \"duration\": 123,\n    |  \"end\": 1570013740007,\n    |  \"logs\": [],\n    |  \"name\": \"a\",\n    |  \"namespace\": \"ns\",\n    |  \"publish\": false,\n    |  \"response\": {\n    |    \"result\": {\n    |      \"error\": {\n    |         \"statusCode\": 404,\n    |         \"body\": \"Requested resource not found\"\n    |      }\n    |    },\n    |    \"statusCode\": 0\n    |  },\n    |  \"start\": 1570013740005,\n    |  \"subject\": \"anon-HfJWZZSG9YE38Y8DJbgp9Xn0YyN\",\n    |  \"version\": \"0.0.1\"\n    |}\"\"\".stripMargin.parseJson\n\n  val activationJs = \"\"\"\n     |{\n     |  \"causedBy\": \"sequence\",\n     |  \"activationId\": \"be97c2fed5dc43d097c2fed5dc73d085\",\n     |  \"conductor\": false,\n     |  \"duration\": 123,\n     |  \"initTime\": 10,\n     |  \"kind\": \"testkind\",\n     |  \"memory\": 128,\n     |  \"name\": \"ns2/a\",\n     |  \"statusCode\": 0,\n     |  \"waitTime\": 5\n     |}\"\"\".stripMargin.parseJson\n\n  val activationWithActionStatusCodeJs =\n    \"\"\"\n      |{\n      |  \"userDefinedStatusCode\": 404,\n      |  \"activationId\": \"be97c2fed5dc43d097c2fed5dc73d085\",\n      |  \"causedBy\": \"sequence\",\n      |  \"conductor\": false,\n      |  \"duration\": 123,\n      |  \"initTime\": 10,\n      |  \"kind\": \"testkind\",\n      |  \"memory\": 128,\n      |  \"name\": \"ns2/a\",\n      |  \"statusCode\": 0,\n      |  \"waitTime\": 5\n      |}\"\"\".stripMargin.parseJson\n\n  it should \"deserialize without error\" in {\n    val activationResponse = ActivationResponse.serdes.read(activationResponseJs)\n    val whiskActivation = WhiskActivation.serdes.read(whiskActivationJs)\n    val activation = Activation.activationFormat.read(activationJs)\n    val whiskActivationWithError = WhiskActivation.serdes.read(whiskActivationErrorJs)\n    val activationWithActionStatus = Activation.activationFormat.read(activationWithActionStatusCodeJs)\n  }\n\n  def generateJsons(): Unit = {\n    val resp = ActivationResponse.success(Some(JsObject(\"res\" -> JsString(\"hello\"))))\n    val whiskActivation = WhiskActivation(\n      namespace = EntityPath(\"ns\"),\n      name = EntityName(\"a\"),\n      Subject(),\n      activationId = ActivationId.generate(),\n      start = nowInMillis(),\n      end = nowInMillis(),\n      response = ActivationResponse.success(Some(JsObject(\"res\" -> JsNumber(1)))),\n      annotations = Parameters(\"limits\", ActionLimits(TimeLimit(1.second), MemoryLimit(128.MB), LogLimit(1.MB)).toJson) ++\n        Parameters(WhiskActivation.waitTimeAnnotation, 5.toJson) ++\n        Parameters(WhiskActivation.initTimeAnnotation, 10.toJson) ++\n        Parameters(WhiskActivation.kindAnnotation, \"testkind\") ++\n        Parameters(WhiskActivation.pathAnnotation, \"ns2/a\") ++\n        Parameters(WhiskActivation.causedByAnnotation, \"sequence\"),\n      duration = Some(123))\n\n    val activation = Activation.from(whiskActivation).get\n\n    println(resp.toJson.prettyPrint)\n    println(whiskActivation.toJson.prettyPrint)\n    println(activation.toJson.prettyPrint)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/ActivationResponseTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport scala.Vector\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json._\nimport org.apache.openwhisk.common.PrintStreamLogging\nimport org.apache.openwhisk.core.entity.ActivationResponse._\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.apache.openwhisk.http.Messages._\nimport scala.Left\nimport scala.Right\n\n@RunWith(classOf[JUnitRunner])\nclass ActivationResponseTests extends AnyFlatSpec with Matchers {\n\n  behavior of \"ActivationResponse\"\n\n  val logger = new PrintStreamLogging()\n\n  it should \"interpret truncated response\" in {\n    val max = 5.B\n    Seq(\"abcdef\", \"\"\"{\"msg\":\"abcedf\"}\"\"\", \"\"\"[\"a\",\"b\",\"c\",\"d\",\"e\"]\"\"\").foreach { m =>\n      {\n        val response = ContainerResponse(okStatus = false, m.take(max.toBytes.toInt - 1), Some(m.length.B, max))\n        val init = processInitResponseContent(Right(response), logger)\n        init.statusCode shouldBe DeveloperError\n        init.result.get.asJsObject\n          .fields(ERROR_FIELD) shouldBe truncatedResponse(response.entity, m.length.B, max).toJson\n        init.size.get should be > 0\n      }\n      {\n        val response = ContainerResponse(okStatus = true, m.take(max.toBytes.toInt - 1), Some(m.length.B, max))\n        val run = processRunResponseContent(Right(response), logger)\n        run.statusCode shouldBe ApplicationError\n        run.result.get.asJsObject\n          .fields(ERROR_FIELD) shouldBe truncatedResponse(response.entity, m.length.B, max).toJson\n      }\n    }\n  }\n\n  it should \"interpret failed init that does not response\" in {\n    Seq(ConnectionError(new Throwable()), NoResponseReceived(), Timeout(new Throwable()))\n      .map(Left(_))\n      .foreach { e =>\n        val ar = processInitResponseContent(e, logger)\n        ar.statusCode shouldBe DeveloperError\n        ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe abnormalInitialization.toJson\n      }\n  }\n\n  it should \"interpret failed init that responds with null string\" in {\n    val response = ContainerResponse(okStatus = false, null)\n    val ar = processInitResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidInitResponse(response.entity).toJson\n    ar.result.get.toString should not include regex(\"null\")\n  }\n\n  it should \"interpret failed init that responds with empty string\" in {\n    val response = ContainerResponse(okStatus = false, \"\")\n    val ar = processInitResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidInitResponse(response.entity).toJson\n    ar.result.get.asJsObject.fields(ERROR_FIELD).toString.endsWith(\".\\\"\") shouldBe true\n  }\n\n  it should \"interpret failed init that responds with non-empty string\" in {\n    val response = ContainerResponse(okStatus = false, \"string\")\n    val ar = processInitResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidInitResponse(response.entity).toJson\n    ar.result.get.toString should include(response.entity)\n    ar.size.get shouldBe \"string\".length\n  }\n\n  it should \"interpret failed init that responds with JSON string not object\" in {\n    val response = ContainerResponse(okStatus = false, Vector(1).toJson.compactPrint)\n    val ar = processInitResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidInitResponse(response.entity).toJson\n    ar.result.get.toString should include(response.entity)\n  }\n\n  it should \"interpret failed init that responds with JSON object containing error\" in {\n    val response = ContainerResponse(okStatus = false, Map(ERROR_FIELD -> \"foobar\").toJson.compactPrint)\n    val ar = processInitResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get shouldBe response.entity.parseJson\n  }\n\n  it should \"interpret failed init that responds with JSON object\" in {\n    val response = ContainerResponse(okStatus = false, Map(\"foobar\" -> \"baz\").toJson.compactPrint)\n    val ar = processInitResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidInitResponse(response.entity).toJson\n    ar.result.get.toString should include(\"baz\")\n  }\n\n  it should \"not interpret successful init\" in {\n    val response = ContainerResponse(okStatus = true, \"\")\n    an[IllegalArgumentException] should be thrownBy {\n      processInitResponseContent(Right(response), logger)\n    }\n  }\n\n  it should \"interpret failed run that does not response\" in {\n    Seq(ConnectionError(new Throwable()), NoResponseReceived(), Timeout(new Throwable()))\n      .map(Left(_))\n      .foreach { e =>\n        val ar = processRunResponseContent(e, logger)\n        ar.statusCode shouldBe DeveloperError\n        ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe abnormalRun.toJson\n      }\n  }\n\n  it should \"interpret failed run that responds with null string\" in {\n    val response = ContainerResponse(okStatus = false, null)\n    val ar = processRunResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidRunResponse(response.entity).toJson\n    ar.result.get.toString should not include regex(\"null\")\n  }\n\n  it should \"interpret failed run that responds with empty string\" in {\n    val response = ContainerResponse(okStatus = false, \"\")\n    val ar = processRunResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidRunResponse(response.entity).toJson\n    ar.result.get.asJsObject.fields(ERROR_FIELD).toString.endsWith(\".\\\"\") shouldBe true\n  }\n\n  it should \"interpret failed run that responds with non-empty string\" in {\n    val response = ContainerResponse(okStatus = false, \"string\")\n    val ar = processRunResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidRunResponse(response.entity).toJson\n    ar.result.get.toString should include(response.entity)\n  }\n\n  it should \"interpret failed run that responds with JSON string not object\" in {\n    val response = ContainerResponse(okStatus = false, Vector(1).toJson.compactPrint)\n    val ar = processRunResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidRunResponse(response.entity).toJson\n    ar.result.get.toString should include(response.entity)\n  }\n\n  it should \"interpret failed run that responds with JSON object containing error\" in {\n    val response = ContainerResponse(okStatus = false, Map(ERROR_FIELD -> \"foobar\").toJson.compactPrint)\n    val ar = processRunResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get shouldBe response.entity.parseJson\n  }\n\n  it should \"interpret failed run that responds with JSON object\" in {\n    val response = ContainerResponse(okStatus = false, Map(\"foobar\" -> \"baz\").toJson.compactPrint)\n    val ar = processRunResponseContent(Right(response), logger)\n    ar.statusCode shouldBe DeveloperError\n    ar.result.get.asJsObject.fields(ERROR_FIELD) shouldBe invalidRunResponse(response.entity).toJson\n    ar.result.get.toString should include(\"baz\")\n  }\n\n  it should \"interpret successful run that responds with JSON object containing error\" in {\n    val response = ContainerResponse(okStatus = true, Map(ERROR_FIELD -> \"foobar\").toJson.compactPrint)\n    val ar = processRunResponseContent(Right(response), logger)\n    ar.statusCode shouldBe ApplicationError\n    ar.result.get shouldBe response.entity.parseJson\n  }\n\n  it should \"interpret successful run that responds with JSON object\" in {\n    val response = ContainerResponse(okStatus = true, Map(\"foobar\" -> \"baz\").toJson.compactPrint)\n    val ar = processRunResponseContent(Right(response), logger)\n    ar.statusCode shouldBe Success\n    ar.result.get shouldBe response.entity.parseJson\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/ControllerInstanceIdTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.entity.{ControllerInstanceId, InstanceId}\nimport spray.json.{JsObject, JsString}\n\nimport scala.util.Success\n\n@RunWith(classOf[JUnitRunner])\nclass ControllerInstanceIdTests extends AnyFlatSpec with Matchers {\n\n  behavior of \"ControllerInstanceId\"\n\n  it should \"accept usable characters\" in {\n    Seq(\"a\", \"1\", \"a.1\", \"a_1\").foreach { s =>\n      ControllerInstanceId(s).asString shouldBe s\n\n    }\n  }\n\n  it should \"reject unusable characters\" in {\n    Seq(\" \", \"!\", \"$\", \"a\" * 129).foreach { s =>\n      an[IllegalArgumentException] shouldBe thrownBy {\n        ControllerInstanceId(s)\n      }\n    }\n  }\n\n  it should \"deserialize legacy ControllerInstanceId format\" in {\n    val i = ControllerInstanceId(\"controller0\")\n    ControllerInstanceId.parse(JsObject(\"asString\" -> JsString(\"controller0\")).compactPrint) shouldBe Success(i)\n  }\n\n  it should \"serialize and deserialize ControllerInstanceId\" in {\n    val i = ControllerInstanceId(\"controller0\")\n    i.serialize shouldBe JsObject(\"asString\" -> JsString(i.asString), \"instanceType\" -> JsString(i.instanceType)).compactPrint\n    i.serialize shouldBe i.toJson.compactPrint\n    InstanceId.parse(i.serialize) shouldBe Success(i)\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/DatastoreTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport scala.concurrent.Await\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatestplus.junit.JUnitRunner\nimport common.{StreamLogging, WskActorSystem}\nimport org.apache.openwhisk.common.WhiskInstants\nimport org.mockito.Mockito._\nimport org.apache.openwhisk.core.database.DocumentConflictException\nimport org.apache.openwhisk.core.database.CacheChangeNotification\nimport org.apache.openwhisk.core.database.NoDocumentException\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entity._\n\n@RunWith(classOf[JUnitRunner])\nclass DatastoreTests\n    extends AnyFlatSpec\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with WskActorSystem\n    with DbUtils\n    with ExecHelpers\n    with StreamLogging\n    with WhiskInstants {\n\n  val namespace = EntityPath(\"test namespace\")\n  val datastore = WhiskEntityStore.datastore()\n  val authstore = WhiskAuthStore.datastore()\n\n  implicit val cacheUpdateNotifier: Option[CacheChangeNotification] = None\n\n  override def afterEach() = {\n    cleanup()\n  }\n\n  override def afterAll() = {\n    println(\"Shutting down store connections\")\n    datastore.shutdown()\n    authstore.shutdown()\n    super.afterAll()\n  }\n\n  @volatile var counter = 0\n  def aname(implicit n: EntityName) = {\n    counter = counter + 1\n    EntityName(s\"$n$counter\")\n  }\n\n  def afullname(implicit namespace: EntityPath, name: String) = FullyQualifiedEntityName(namespace, EntityName(name))\n\n  behavior of \"Datastore\"\n\n  it should \"CRD action blackbox\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create action blackbox\")\n    val exec = bb(\"image\")\n    val actions = Seq(\n      WhiskAction(namespace, aname, exec),\n      WhiskAction(namespace, aname, exec, Parameters(\"x\", \"y\")),\n      WhiskAction(namespace, aname, exec, Parameters(\"x\", \"y\")),\n      WhiskAction(namespace, aname, exec, Parameters(\"x\", \"y\") ++ Parameters(\"x\", \"y\")),\n      WhiskAction(namespace, aname, exec, Parameters(\"x\", \"y\") ++ Parameters(\"y\", \"x\")))\n    val docs = actions.map { entity =>\n      putGetCheck(datastore, entity, WhiskAction)\n    }\n  }\n\n  it should \"CRD action js\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create action js\")\n    val exec = jsDefault(\"code\")\n    val actions = Seq(\n      WhiskAction(namespace, aname, exec, Parameters()),\n      WhiskAction(namespace, aname, exec, Parameters(\"x\", \"y\")),\n      WhiskAction(namespace, aname, exec, Parameters(\"x\", \"y\")),\n      WhiskAction(namespace, aname, exec, Parameters(\"x\", \"y\") ++ Parameters(\"x\", \"y\")),\n      WhiskAction(namespace, aname, exec, Parameters(\"x\", \"y\") ++ Parameters(\"y\", \"x\")))\n    val docs = actions.map { entity =>\n      putGetCheck(datastore, entity, WhiskAction)\n    }\n  }\n\n  it should \"CRD trigger\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create trigger\")\n    val triggers = Seq(\n      WhiskTrigger(namespace, aname),\n      WhiskTrigger(namespace, aname, Parameters(\"x\", \"y\")),\n      WhiskTrigger(namespace, aname, Parameters(\"x\", \"y\")))\n    val docs = triggers.map { entity =>\n      putGetCheck(datastore, entity, WhiskTrigger)\n    }\n  }\n\n  it should \"CRD rule\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create rule\")\n    val rules = Seq(\n      WhiskRule(namespace, aname, afullname(namespace, \"a trigger\"), afullname(namespace, \"an action\")),\n      WhiskRule(namespace, aname, afullname(namespace, \"a trigger\"), afullname(namespace, \"an action\")))\n    val docs = rules.map { entity =>\n      putGetCheck(datastore, entity, WhiskRule)\n    }\n  }\n\n  it should \"CRD activation\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create action blackbox\")\n    val activations = Seq(\n      WhiskActivation(namespace, aname, Subject(), ActivationId.generate(), start = nowInMillis(), end = nowInMillis()),\n      WhiskActivation(namespace, aname, Subject(), ActivationId.generate(), start = nowInMillis(), end = nowInMillis()))\n    val docs = activations.map { entity =>\n      putGetCheck(datastore, entity, WhiskActivation)\n    }\n  }\n\n  it should \"CRD activation with utf8 characters\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create action blackbox\")\n    val activations = Seq(\n      WhiskActivation(\n        namespace,\n        aname,\n        Subject(),\n        ActivationId.generate(),\n        start = nowInMillis(),\n        end = nowInMillis(),\n        logs = ActivationLogs(Vector(\"Prote\\u00EDna\"))))\n    val docs = activations.map { entity =>\n      putGetCheck(datastore, entity, WhiskActivation)\n    }\n  }\n\n  it should \"reject action with null arguments\" in {\n    val name = EntityName(\"bad action\")\n    intercept[IllegalArgumentException] {\n      WhiskAction(namespace, name, bb(\"i\"), Parameters(), null)\n    }\n  }\n\n  it should \"reject trigger with null arguments\" in {\n    val name = EntityName(\"bad trigger\")\n    intercept[IllegalArgumentException] {\n      WhiskTrigger(namespace, name, Parameters(), null)\n    }\n  }\n\n  it should \"reject rule with null arguments\" in {\n    val name = EntityName(\"bad rule\")\n    intercept[IllegalArgumentException] {\n      WhiskRule(\n        namespace,\n        name,\n        FullyQualifiedEntityName(namespace, EntityName(null)),\n        FullyQualifiedEntityName(namespace, EntityName(null)))\n    }\n    intercept[IllegalArgumentException] {\n      WhiskRule(\n        namespace,\n        name,\n        FullyQualifiedEntityName(namespace, EntityName(\"\")),\n        FullyQualifiedEntityName(namespace, EntityName(null)))\n    }\n    intercept[IllegalArgumentException] {\n      WhiskRule(\n        namespace,\n        name,\n        FullyQualifiedEntityName(namespace, EntityName(\" \")),\n        FullyQualifiedEntityName(namespace, EntityName(null)))\n    }\n  }\n\n  it should \"update action with a revision\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"update action\")\n    val exec = jsDefault(\"update\")\n    val action = WhiskAction(namespace, aname, exec, Parameters(), ActionLimits())\n    val docinfo = putGetCheck(datastore, action, WhiskAction, false)._2.docinfo\n    val revAction =\n      WhiskAction(namespace, action.name, exec, Parameters(), ActionLimits()).revision[WhiskAction](docinfo.rev)\n    putGetCheck(datastore, revAction, WhiskAction)\n  }\n\n  it should \"delete action attachments\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"attachment action\")\n    val javaAction =\n      WhiskAction(namespace, aname, javaDefault(\"ZHViZWU=\", Some(\"hello\")), annotations = Parameters(\"exec\", \"java\"))\n    val docinfo = putGetCheck(datastore, javaAction, WhiskAction, false)._2.docinfo\n\n    val proxy = spy(datastore)\n    Await.result(WhiskAction.del(proxy, docinfo), dbOpTimeout)\n\n    verify(proxy).deleteAttachments(docinfo)\n  }\n\n  it should \"update trigger with a revision\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"update trigger\")\n    val trigger = WhiskTrigger(namespace, aname)\n    val docinfo = putGetCheck(datastore, trigger, WhiskTrigger, false)._2.docinfo\n    val revTrigger = WhiskTrigger(namespace, trigger.name).revision[WhiskTrigger](docinfo.rev)\n    putGetCheck(datastore, revTrigger, WhiskTrigger)\n  }\n\n  it should \"update rule with a revision\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"update rule\")\n    val rule = WhiskRule(namespace, aname, afullname(namespace, \"a trigger\"), afullname(namespace, \"an action\"))\n    val docinfo = putGetCheck(datastore, rule, WhiskRule, false)._2.docinfo\n    val revRule = WhiskRule(namespace, rule.name, rule.trigger, rule.action).revision[WhiskRule](docinfo.rev)\n    putGetCheck(datastore, revRule, WhiskRule)\n  }\n\n  it should \"update activation with a revision\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"update activation\")\n    val activation =\n      WhiskActivation(namespace, aname, Subject(), ActivationId.generate(), start = nowInMillis(), end = nowInMillis())\n    val docinfo = putGetCheck(datastore, activation, WhiskActivation, false)._2.docinfo\n    val revActivation = WhiskActivation(\n      namespace,\n      aname,\n      activation.subject,\n      activation.activationId,\n      start = nowInMillis(),\n      end = nowInMillis()).revision[WhiskActivation](docinfo.rev)\n    putGetCheck(datastore, revActivation, WhiskActivation)\n  }\n\n  it should \"fail with document conflict when trying to write the same action twice without a revision\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create action twice\")\n    val exec = jsDefault(\"twice\")\n    val action = WhiskAction(namespace, aname, exec)\n    putGetCheck(datastore, action, WhiskAction)\n    intercept[DocumentConflictException] {\n      putGetCheck(datastore, action, WhiskAction)\n    }\n  }\n\n  it should \"fail with document conflict when trying to write the same trigger twice without a revision\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create trigger twice\")\n    val trigger = WhiskTrigger(namespace, aname, Parameters(\"x\", \"y\"))\n    putGetCheck(datastore, trigger, WhiskTrigger)\n    intercept[DocumentConflictException] {\n      putGetCheck(datastore, trigger, WhiskTrigger)\n    }\n  }\n\n  it should \"fail with document conflict when trying to write the same rule twice without a revision\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create rule twice\")\n    val rule = WhiskRule(namespace, aname, afullname(namespace, \"a trigger\"), afullname(namespace, \"an action\"))\n    putGetCheck(datastore, rule, WhiskRule)\n    intercept[DocumentConflictException] {\n      putGetCheck(datastore, rule, WhiskRule)\n    }\n  }\n\n  it should \"fail with document conflict when trying to write the same activation twice without a revision\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"create activation twice\")\n    val activation =\n      WhiskActivation(namespace, aname, Subject(), ActivationId.generate(), start = nowInMillis(), end = nowInMillis())\n    putGetCheck(datastore, activation, WhiskActivation)\n    intercept[DocumentConflictException] {\n      putGetCheck(datastore, activation, WhiskActivation)\n    }\n  }\n\n  it should \"fail with document does not exist when trying to delete the same action twice\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"delete action twice\")\n    val exec = jsDefault(\"twice\")\n    val action = WhiskAction(namespace, aname, exec)\n    val doc = putGetCheck(datastore, action, WhiskAction, false)._1\n    assert(Await.result(WhiskAction.del(datastore, doc), dbOpTimeout))\n    intercept[NoDocumentException] {\n      Await.result(WhiskAction.del(datastore, doc), dbOpTimeout)\n      assert(false)\n    }\n  }\n\n  it should \"fail with document does not exist when trying to delete the same trigger twice\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"delete trigger twice\")\n    val trigger = WhiskTrigger(namespace, aname, Parameters(\"x\", \"y\"))\n    val doc = putGetCheck(datastore, trigger, WhiskTrigger, false)._1\n    assert(Await.result(WhiskTrigger.del(datastore, doc), dbOpTimeout))\n    intercept[NoDocumentException] {\n      Await.result(WhiskTrigger.del(datastore, doc), dbOpTimeout)\n      assert(false)\n    }\n  }\n\n  it should \"fail with document does not exist when trying to delete the same rule twice\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"delete rule twice\")\n    val rule = WhiskRule(namespace, aname, afullname(namespace, \"a trigger\"), afullname(namespace, \"an action\"))\n    val doc = putGetCheck(datastore, rule, WhiskRule, false)._1\n    assert(Await.result(WhiskRule.del(datastore, doc), dbOpTimeout))\n    intercept[NoDocumentException] {\n      Await.result(WhiskRule.del(datastore, doc), dbOpTimeout)\n      assert(false)\n    }\n  }\n\n  it should \"fail with document does not exist when trying to delete the same activation twice\" in {\n    implicit val tid = transid()\n    implicit val basename = EntityName(\"delete activation twice\")\n    val activation =\n      WhiskActivation(namespace, aname, Subject(), ActivationId.generate(), start = nowInMillis(), end = nowInMillis())\n    val doc = putGetCheck(datastore, activation, WhiskActivation, false)._1\n    assert(Await.result(WhiskActivation.del(datastore, doc), dbOpTimeout))\n    intercept[NoDocumentException] {\n      Await.result(WhiskActivation.del(datastore, doc), dbOpTimeout)\n      assert(false)\n    }\n  }\n\n  it should \"fail to CRD with undefined argument\" in {\n    implicit val tid = transid()\n    intercept[IllegalArgumentException] {\n      Await.result(WhiskAction.del(null, null), dbOpTimeout)\n      assert(false)\n    }\n    intercept[IllegalArgumentException] {\n      Await.result(WhiskAction.put(null, null, None), dbOpTimeout)\n      assert(false)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/ExecHelpers.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.Suite\nimport common.StreamLogging\nimport common.WskActorSystem\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.ArgNormalizer.trim\nimport org.apache.openwhisk.core.entity.ExecManifest._\nimport org.apache.openwhisk.core.entity.size._\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\ntrait ExecHelpers extends Matchers with WskActorSystem with StreamLogging {\n  self: Suite =>\n\n  private val config = new WhiskConfig(ExecManifest.requiredProperties)\n  ExecManifest.initialize(config) should be a 'success\n\n  protected val NODEJS = \"nodejs:20\"\n  protected val SWIFT5 = \"swift:5.3\"\n  protected val BLACKBOX = \"blackbox\"\n  protected val JAVA_DEFAULT = \"java:8\"\n\n  private def attFmt[T: JsonFormat] = Attachments.serdes[T]\n\n  protected def imageName(name: String) =\n    ExecManifest.runtimesManifest.runtimes.flatMap(_.versions).find(_.kind == name).get.image\n\n  protected def jsOld(code: String, main: Option[String] = None) = {\n    CodeExecAsString(\n      RuntimeManifest(\n        NODEJS,\n        imageName(NODEJS),\n        default = Some(true),\n        deprecated = Some(false),\n        stemCells = Some(List(StemCell(2, 256.MB)))),\n      trim(code),\n      main.map(_.trim))\n  }\n\n  protected def js(code: String, main: Option[String] = None) = {\n    val attachment = attFmt[String].read(code.trim.toJson)\n    val manifest = ExecManifest.runtimesManifest.resolveDefaultRuntime(NODEJS).get\n\n    CodeExecAsAttachment(manifest, attachment, main.map(_.trim), Exec.isBinaryCode(code))\n  }\n\n  protected def jsDefault(code: String, main: Option[String] = None) = {\n    js(code, main)\n  }\n\n  protected def jsMetaDataOld(main: Option[String] = None, binary: Boolean) = {\n    CodeExecMetaDataAsString(\n      RuntimeManifest(\n        NODEJS,\n        imageName(NODEJS),\n        default = Some(true),\n        deprecated = Some(false),\n        stemCells = Some(List(StemCell(2, 256.MB)))),\n      binary,\n      main.map(_.trim))\n  }\n\n  protected def jsMetaData(main: Option[String] = None, binary: Boolean) = {\n    val manifest = ExecManifest.runtimesManifest.resolveDefaultRuntime(NODEJS).get\n\n    CodeExecMetaDataAsAttachment(manifest, binary, main.map(_.trim))\n  }\n\n  protected def javaDefault(code: String, main: Option[String] = None) = {\n    val attachment = attFmt[String].read(code.trim.toJson)\n    val manifest = ExecManifest.runtimesManifest.resolveDefaultRuntime(JAVA_DEFAULT).get\n\n    CodeExecAsAttachment(manifest, attachment, main.map(_.trim), Exec.isBinaryCode(code))\n  }\n\n  protected def javaMetaData(main: Option[String] = None, binary: Boolean) = {\n    val manifest = ExecManifest.runtimesManifest.resolveDefaultRuntime(JAVA_DEFAULT).get\n\n    CodeExecMetaDataAsAttachment(manifest, binary, main.map(_.trim))\n  }\n\n  protected def swift(code: String, main: Option[String] = None) = {\n    val attachment = attFmt[String].read(code.trim.toJson)\n    val manifest = ExecManifest.runtimesManifest.resolveDefaultRuntime(SWIFT5).get\n\n    CodeExecAsAttachment(manifest, attachment, main.map(_.trim), Exec.isBinaryCode(code))\n  }\n\n  protected def sequence(components: Vector[FullyQualifiedEntityName]) = SequenceExec(components)\n\n  protected def sequenceMetaData(components: Vector[FullyQualifiedEntityName]) = SequenceExecMetaData(components)\n\n  protected def bb(image: String) = BlackBoxExec(ExecManifest.ImageName(trim(image)), None, None, false, false)\n\n  protected def bb(image: String, code: String, main: Option[String] = None) = {\n    val (codeOpt, binary) =\n      if (code.trim.nonEmpty) (Some(attFmt[String].read(code.toJson)), Exec.isBinaryCode(code))\n      else (None, false)\n    BlackBoxExec(ExecManifest.ImageName(trim(image)), codeOpt, main, false, binary)\n  }\n\n  protected def blackBoxMetaData(image: String, main: Option[String] = None, binary: Boolean) = {\n    BlackBoxExecMetaData(ExecManifest.ImageName(trim(image)), main, false, binary)\n  }\n\n  protected def actionLimits(memory: ByteSize, concurrency: Int): ActionLimits =\n    ActionLimits(memory = MemoryLimit(memory), concurrency = IntraConcurrencyLimit(concurrency))\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/ExecManifestTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport java.util.concurrent.TimeUnit\n\nimport common.{StreamLogging, WskActorSystem}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.entity.ExecManifest\nimport org.apache.openwhisk.core.entity.ExecManifest._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.ByteSize\n\nimport scala.concurrent.duration.FiniteDuration\nimport scala.util.Success\n\n@RunWith(classOf[JUnitRunner])\nclass ExecManifestTests extends AnyFlatSpec with WskActorSystem with StreamLogging with Matchers {\n\n  behavior of \"ExecManifest\"\n\n  private def manifestFactory(runtimes: JsObject) = {\n    JsObject(\"runtimes\" -> runtimes)\n  }\n\n  it should \"parse an image name\" in {\n    Map(\n      \"i\" -> ImageName(\"i\"),\n      \"i:t\" -> ImageName(\"i\", tag = Some(\"t\")),\n      \"i:tt\" -> ImageName(\"i\", tag = Some(\"tt\")),\n      \"ii\" -> ImageName(\"ii\"),\n      \"ii:t\" -> ImageName(\"ii\", tag = Some(\"t\")),\n      \"ii:tt\" -> ImageName(\"ii\", tag = Some(\"tt\")),\n      \"p/i\" -> ImageName(\"i\", None, Some(\"p\")),\n      \"pre/img\" -> ImageName(\"img\", None, Some(\"pre\")),\n      \"pre/img:t\" -> ImageName(\"img\", None, Some(\"pre\"), Some(\"t\")),\n      \"hostname:1234/img\" -> ImageName(\"img\", Some(\"hostname:1234\"), None),\n      \"hostname:1234/img:t\" -> ImageName(\"img\", Some(\"hostname:1234\"), None, Some(\"t\")),\n      \"pre1/pre2/img\" -> ImageName(\"img\", None, Some(\"pre1/pre2\")),\n      \"pre1/pre2/img:t\" -> ImageName(\"img\", None, Some(\"pre1/pre2\"), Some(\"t\")),\n      \"hostname:1234/pre1/pre2/img\" -> ImageName(\"img\", Some(\"hostname:1234\"), Some(\"pre1/pre2\")),\n      \"hostname.com:3121/pre1/pre2/img:t\" -> ImageName(\"img\", Some(\"hostname.com:3121\"), Some(\"pre1/pre2\"), Some(\"t\")),\n      \"hostname.com:3121/pre1/pre2/img:t@sha256:77af4d6b9913e693e8d0b4b294fa62ade6054e6b2f1ffb617ac955dd63fb0182\" ->\n        ImageName(\"img\", Some(\"hostname.com:3121\"), Some(\"pre1/pre2\"), Some(\"t\")))\n      .foreach {\n        case (s, v) => ImageName.fromString(s) shouldBe Success(v)\n      }\n\n    Seq(\"ABC\", \"x:8080:10/abc\", \"p/a:x:y\", \"p/a:t@sha256:77af4d6b9\").foreach { s =>\n      a[DeserializationException] should be thrownBy ImageName.fromString(s).get\n    }\n  }\n\n  it should \"read a valid configuration without default prefix, default tag or blackbox images\" in {\n    val k1 = RuntimeManifest(\"k1\", ImageName(\"???\"))\n    val k2 = RuntimeManifest(\"k2\", ImageName(\"???\"), default = Some(true))\n    val p1 = RuntimeManifest(\"p1\", ImageName(\"???\"))\n    val s1 = RuntimeManifest(\"s1\", ImageName(\"???\"), stemCells = Some(List(StemCell(2, 256.MB))))\n    val mf = manifestFactory(JsObject(\"ks\" -> Set(k1, k2).toJson, \"p1\" -> Set(p1).toJson, \"s1\" -> Set(s1).toJson))\n    val runtimes = ExecManifest.runtimes(mf, RuntimeManifestConfig()).get\n\n    Seq(\"k1\", \"k2\", \"p1\", \"s1\").foreach {\n      runtimes.knownContainerRuntimes.contains(_) shouldBe true\n    }\n\n    runtimes.knownContainerRuntimes.contains(\"k3\") shouldBe false\n\n    runtimes.resolveDefaultRuntime(\"k1\") shouldBe Some(k1)\n    runtimes.resolveDefaultRuntime(\"k2\") shouldBe Some(k2)\n    runtimes.resolveDefaultRuntime(\"p1\") shouldBe Some(p1)\n    runtimes.resolveDefaultRuntime(\"s1\") shouldBe Some(s1)\n\n    runtimes.resolveDefaultRuntime(\"ks:default\") shouldBe Some(k2)\n    runtimes.resolveDefaultRuntime(\"p1:default\") shouldBe Some(p1)\n    runtimes.resolveDefaultRuntime(\"s1:default\") shouldBe Some(s1)\n  }\n\n  it should \"read a valid configuration where an image may omit registry, prefix or tag\" in {\n    val i1 = RuntimeManifest(\"i1\", ImageName(\"???\"))\n    val i2 = RuntimeManifest(\"i2\", ImageName(\"???\", Some(\"rrr\")))\n    val i3 = RuntimeManifest(\"i3\", ImageName(\"???\", Some(\"rrr\"), Some(\"ppp\")), default = Some(true))\n    val i4 = RuntimeManifest(\"i4\", ImageName(\"???\", Some(\"rrr\"), Some(\"ppp\"), Some(\"ttt\")))\n    val j1 = RuntimeManifest(\"j1\", ImageName(\"???\", None, None, Some(\"ttt\")))\n    val k1 = RuntimeManifest(\"k1\", ImageName(\"???\", None, Some(\"ppp\")))\n    val p1 = RuntimeManifest(\"p1\", ImageName(\"???\", None, Some(\"ppp\"), Some(\"ttt\")))\n    val q1 = RuntimeManifest(\"q1\", ImageName(\"???\", Some(\"rrr\"), None, Some(\"ttt\")))\n    val s1 = RuntimeManifest(\"s1\", ImageName(\"???\"), stemCells = Some(List(StemCell(2, 256.MB))))\n\n    val mf =\n      JsObject(\n        \"runtimes\" -> JsObject(\n          \"is\" -> Set(i1, i2, i3, i4).toJson,\n          \"js\" -> Set(j1).toJson,\n          \"ks\" -> Set(k1).toJson,\n          \"ps\" -> Set(p1).toJson,\n          \"qs\" -> Set(q1).toJson,\n          \"ss\" -> Set(s1).toJson))\n    val rmc = RuntimeManifestConfig()\n    val runtimes = ExecManifest.runtimes(mf, rmc).get\n\n    runtimes.resolveDefaultRuntime(\"i1\").get.image.resolveImageName() shouldBe \"???\"\n    runtimes.resolveDefaultRuntime(\"i2\").get.image.resolveImageName() shouldBe \"rrr/???\"\n    runtimes.resolveDefaultRuntime(\"i3\").get.image.resolveImageName() shouldBe \"rrr/ppp/???\"\n    runtimes.resolveDefaultRuntime(\"i4\").get.image.resolveImageName() shouldBe \"rrr/ppp/???:ttt\"\n    runtimes.resolveDefaultRuntime(\"j1\").get.image.resolveImageName() shouldBe \"???:ttt\"\n    runtimes.resolveDefaultRuntime(\"k1\").get.image.resolveImageName() shouldBe \"ppp/???\"\n    runtimes.resolveDefaultRuntime(\"p1\").get.image.resolveImageName() shouldBe \"ppp/???:ttt\"\n    runtimes.resolveDefaultRuntime(\"q1\").get.image.resolveImageName() shouldBe \"rrr/???:ttt\"\n    runtimes.resolveDefaultRuntime(\"s1\").get.image.resolveImageName() shouldBe \"???\"\n    runtimes.resolveDefaultRuntime(\"s1\").get.stemCells.get(0).initialCount shouldBe 2\n    runtimes.resolveDefaultRuntime(\"s1\").get.stemCells.get(0).memory shouldBe 256.MB\n  }\n\n  it should \"read a valid configuration with blackbox images but without default registry, prefix or tag\" in {\n    val imgs = Set(\n      ImageName(\"???\"),\n      ImageName(\"???\", Some(\"rrr\")),\n      ImageName(\"???\", Some(\"rrr\"), Some(\"ppp\")),\n      ImageName(\"???\", Some(\"rrr\"), Some(\"ppp\"), Some(\"ttt\")),\n      ImageName(\"???\", None, None, Some(\"ttt\")),\n      ImageName(\"???\", None, Some(\"ppp\")),\n      ImageName(\"???\", None, Some(\"ppp\"), Some(\"ttt\")),\n      ImageName(\"???\", Some(\"rrr\"), None, Some(\"ttt\")))\n\n    val mf = JsObject(\"runtimes\" -> JsObject.empty, \"blackboxes\" -> imgs.toJson)\n    val runtimes = ExecManifest.runtimes(mf, RuntimeManifestConfig()).get\n\n    runtimes.blackboxImages shouldBe imgs\n    imgs.foreach(img => runtimes.skipDockerPull(img) shouldBe true)\n    runtimes.skipDockerPull(ImageName(\"???\", Some(\"aaa\"))) shouldBe false\n    runtimes.skipDockerPull(ImageName(\"???\", None, Some(\"bbb\"))) shouldBe false\n  }\n\n  it should \"read a valid configuration with blackbox images, which may omit registry, prefix or tag\" in {\n    val imgs = List(\n      ImageName(\"???\"),\n      ImageName(\"???\", Some(\"rrr\")),\n      ImageName(\"???\", Some(\"rrr\"), Some(\"ppp\")),\n      ImageName(\"???\", Some(\"rrr\"), Some(\"ppp\"), Some(\"ttt\")),\n      ImageName(\"???\", None, None, Some(\"ttt\")),\n      ImageName(\"???\", None, Some(\"ppp\")),\n      ImageName(\"???\", None, Some(\"ppp\"), Some(\"ttt\")),\n      ImageName(\"???\", Some(\"rrr\"), None, Some(\"ttt\")))\n\n    val mf = JsObject(\"runtimes\" -> JsObject.empty, \"blackboxes\" -> imgs.toJson)\n    val rmc = RuntimeManifestConfig()\n    val runtimes = ExecManifest.runtimes(mf, rmc).get\n\n    runtimes.blackboxImages shouldBe imgs.toSet\n\n    imgs.forall(runtimes.skipDockerPull(_)) shouldBe true\n\n    runtimes.skipDockerPull(ImageName(\"xxx\")) shouldBe false\n    runtimes.skipDockerPull(ImageName(\"???\", Some(\"rrr\"), Some(\"bbb\"))) shouldBe false\n    runtimes.skipDockerPull(ImageName(\"???\", Some(\"rrr\"), Some(\"ppp\"), Some(\"test\"))) shouldBe false\n    runtimes.skipDockerPull(ImageName(\"???\", None, None, Some(\"test\"))) shouldBe false\n  }\n\n  it should \"reject runtimes with multiple defaults\" in {\n    val k1 = RuntimeManifest(\"k1\", ImageName(\"???\"), default = Some(true))\n    val k2 = RuntimeManifest(\"k2\", ImageName(\"???\"), default = Some(true))\n    val mf = manifestFactory(JsObject(\"ks\" -> Set(k1, k2).toJson))\n\n    an[IllegalArgumentException] should be thrownBy ExecManifest.runtimes(mf, RuntimeManifestConfig()).get\n  }\n\n  it should \"reject finding a default when none specified for multiple versions in the same family\" in {\n    val k1 = RuntimeManifest(\"k1\", ImageName(\"???\"))\n    val k2 = RuntimeManifest(\"k2\", ImageName(\"???\"))\n    val mf = manifestFactory(JsObject(\"ks\" -> Set(k1, k2).toJson))\n\n    an[IllegalArgumentException] should be thrownBy ExecManifest.runtimes(mf, RuntimeManifestConfig()).get\n  }\n\n  it should \"prefix image name with overrides without registry\" in {\n    val name = \"xyz\"\n    ExecManifest.ImageName(name, Some(\"\"), Some(\"\"), Some(\"\")).resolveImageName() shouldBe name\n\n    Seq(\n      (ExecManifest.ImageName(name), name),\n      (ExecManifest.ImageName(name, None, None, Some(\"t\")), s\"$name:t\"),\n      (ExecManifest.ImageName(name, None, Some(\"pre\")), s\"pre/$name\"),\n      (ExecManifest.ImageName(name, None, Some(\"pre\"), Some(\"t\")), s\"pre/$name:t\"),\n    ).foreach {\n      case (image, exp) =>\n        image.resolveImageName() shouldBe exp\n        image.resolveImageName(Some(\"\")) shouldBe exp\n        image.resolveImageName(Some(\"r\")) shouldBe s\"r/$exp\"\n        image.resolveImageName(Some(\"r/\")) shouldBe s\"r/$exp\"\n\n    }\n  }\n\n  it should \"prefix image name with overrides with registry\" in {\n    val name = \"xyz\"\n    ExecManifest.ImageName(name, Some(\"\"), Some(\"\"), Some(\"\")).resolveImageName() shouldBe name\n\n    Seq(\n      (ExecManifest.ImageName(name, Some(\"hostname.com\")), s\"hostname.com/$name\"),\n      (ExecManifest.ImageName(name, Some(\"hostname.com\"), None, Some(\"t\")), s\"hostname.com/$name:t\"),\n      (ExecManifest.ImageName(name, Some(\"hostname.com\"), Some(\"pre\")), s\"hostname.com/pre/$name\"),\n      (ExecManifest.ImageName(name, Some(\"hostname.com\"), Some(\"pre\"), Some(\"t\")), s\"hostname.com/pre/$name:t\"),\n    ).foreach {\n      case (image, exp) =>\n        image.resolveImageName() shouldBe exp\n        image.resolveImageName(Some(\"\")) shouldBe exp\n        image.resolveImageName(Some(\"r\")) shouldBe exp\n        image.resolveImageName(Some(\"r/\")) shouldBe exp\n\n    }\n  }\n\n  it should \"indicate image is local if it matches deployment docker prefix\" in {\n    val mf = JsObject.empty\n    val rmc = RuntimeManifestConfig(bypassPullForLocalImages = Some(true), localImagePrefix = Some(\"localpre\"))\n    val manifest = ExecManifest.runtimes(mf, rmc)\n\n    manifest.get.skipDockerPull(ImageName(prefix = Some(\"x\"), name = \"y\")) shouldBe false\n    manifest.get.skipDockerPull(ImageName(prefix = Some(\"localpre\"), name = \"y\")) shouldBe true\n  }\n\n  it should \"de/serialize stem cell configuration\" in {\n    val cell = StemCell(3, 128.MB)\n    val cellAsJson = JsObject(\"initialCount\" -> JsNumber(3), \"memory\" -> JsString(\"128 MB\"))\n    stemCellSerdes.write(cell) shouldBe cellAsJson\n    stemCellSerdes.read(cellAsJson) shouldBe cell\n\n    an[IllegalArgumentException] shouldBe thrownBy {\n      StemCell(-1, 128.MB)\n    }\n\n    an[IllegalArgumentException] shouldBe thrownBy {\n      StemCell(0, 128.MB)\n    }\n\n    an[IllegalArgumentException] shouldBe thrownBy {\n      val cellAsJson = JsObject(\"initialCount\" -> JsNumber(0), \"memory\" -> JsString(\"128 MB\"))\n      stemCellSerdes.read(cellAsJson)\n    }\n\n    the[IllegalArgumentException] thrownBy {\n      val cellAsJson = JsObject(\"initialCount\" -> JsNumber(1), \"memory\" -> JsString(\"128\"))\n      stemCellSerdes.read(cellAsJson)\n    } should have message {\n      ByteSize.formatError\n    }\n  }\n\n  it should \"parse manifest without reactive from JSON string\" in {\n    val json = \"\"\"\n                 |{ \"runtimes\": {\n                 |    \"nodef\": [\n                 |      {\n                 |        \"kind\": \"nodejs:20\",\n                 |        \"default\": true,\n                 |        \"image\": {\n                 |          \"name\": \"nodejsaction\"\n                 |        },\n                 |        \"stemCells\": [{\n                 |          \"initialCount\": 1,\n                 |          \"memory\": \"128 MB\"\n                 |        }, {\n                 |          \"initialCount\": 1,\n                 |          \"memory\": \"256 MB\"\n                 |        }]\n                 |      }, {\n                 |        \"kind\": \"nodejs:12\",\n                 |        \"deprecated\": true,\n                 |        \"image\": {\n                 |          \"name\": \"nodejsaction\"\n                 |        },\n                 |        \"stemCells\": [{\n                 |          \"initialCount\": 1,\n                 |          \"memory\": \"128 MB\"\n                 |        }]\n                 |      }\n                 |    ],\n                 |    \"pythonf\": [{\n                 |      \"kind\": \"python\",\n                 |      \"image\": {\n                 |        \"name\": \"pythonaction\"\n                 |      },\n                 |      \"stemCells\": [{\n                 |        \"initialCount\": 2,\n                 |        \"memory\": \"256 MB\"\n                 |      }]\n                 |    }],\n                 |    \"swiftf\": [{\n                 |      \"kind\": \"swift\",\n                 |      \"image\": {\n                 |        \"name\": \"swiftaction\"\n                 |      },\n                 |      \"stemCells\": []\n                 |    }],\n                 |    \"phpf\": [{\n                 |      \"kind\": \"php\",\n                 |      \"image\": {\n                 |        \"name\": \"phpaction\"\n                 |      }\n                 |    }]\n                 |  }\n                 |}\n                 |\"\"\".stripMargin.parseJson.asJsObject\n\n    val js14 = RuntimeManifest(\n      \"nodejs:20\",\n      ImageName(\"nodejsaction\"),\n      default = Some(true),\n      stemCells = Some(List(StemCell(1, 128.MB), StemCell(1, 256.MB))))\n    val js12 = RuntimeManifest(\n      \"nodejs:12\",\n      ImageName(\"nodejsaction\"),\n      deprecated = Some(true),\n      stemCells = Some(List(StemCell(1, 128.MB))))\n    val py = RuntimeManifest(\"python\", ImageName(\"pythonaction\"), stemCells = Some(List(StemCell(2, 256.MB))))\n    val sw = RuntimeManifest(\"swift\", ImageName(\"swiftaction\"), stemCells = Some(List.empty))\n    val ph = RuntimeManifest(\"php\", ImageName(\"phpaction\"))\n    val mf = ExecManifest.runtimes(json, RuntimeManifestConfig()).get\n\n    mf shouldBe {\n      Runtimes(\n        Set(\n          RuntimeFamily(\"nodef\", Set(js14, js12)),\n          RuntimeFamily(\"pythonf\", Set(py)),\n          RuntimeFamily(\"swiftf\", Set(sw)),\n          RuntimeFamily(\"phpf\", Set(ph))),\n        Set.empty,\n        None)\n    }\n\n    mf.stemcells.flatMap {\n      case (m, cells) =>\n        cells.map { c =>\n          (m.kind, m.image, c.initialCount, c.memory)\n        }\n    }.toList should contain theSameElementsAs List(\n      (js14.kind, js14.image, 1, 128.MB),\n      (js14.kind, js14.image, 1, 256.MB),\n      (js12.kind, js12.image, 1, 128.MB),\n      (py.kind, py.image, 2, 256.MB))\n  }\n\n  it should \"parse manifest with reactive from JSON string\" in {\n    val json = \"\"\"\n                 |{ \"runtimes\": {\n                 |    \"nodef\": [\n                 |      {\n                 |        \"kind\": \"nodejs:20\",\n                 |        \"default\": true,\n                 |        \"image\": {\n                 |          \"name\": \"nodejsaction\"\n                 |        },\n                 |        \"stemCells\": [{\n                 |          \"initialCount\": 1,\n                 |          \"memory\": \"128 MB\",\n                 |          \"reactive\": {\n                 |            \"minCount\": 1,\n                 |            \"maxCount\": 4,\n                 |            \"ttl\": \"2 minutes\",\n                 |            \"threshold\": 1,\n                 |            \"increment\": 1\n                 |          }\n                 |        }, {\n                 |          \"initialCount\": 1,\n                 |          \"memory\": \"256 MB\",\n                 |          \"reactive\": {\n                 |            \"minCount\": 1,\n                 |            \"maxCount\": 4,\n                 |            \"ttl\": \"2 minutes\",\n                 |            \"threshold\": 1,\n                 |            \"increment\": 1\n                 |           }\n                 |        }]\n                 |      }, {\n                 |        \"kind\": \"nodejs:12\",\n                 |        \"deprecated\": true,\n                 |        \"image\": {\n                 |          \"name\": \"nodejsaction\"\n                 |        },\n                 |        \"stemCells\": [{\n                 |          \"initialCount\": 1,\n                 |          \"memory\": \"128 MB\",\n                 |          \"reactive\": {\n                 |            \"minCount\": 1,\n                 |            \"maxCount\": 4,\n                 |            \"ttl\": \"2 minutes\",\n                 |            \"threshold\": 1,\n                 |            \"increment\": 1\n                 |           }\n                 |        }]\n                 |      }\n                 |    ],\n                 |    \"pythonf\": [{\n                 |      \"kind\": \"python\",\n                 |      \"image\": {\n                 |        \"name\": \"pythonaction\"\n                 |      },\n                 |      \"stemCells\": [{\n                 |        \"initialCount\": 2,\n                 |        \"memory\": \"256 MB\",\n                 |        \"reactive\": {\n                 |           \"minCount\": 1,\n                 |           \"maxCount\": 4,\n                 |           \"ttl\": \"2 minutes\",\n                 |           \"threshold\": 1,\n                 |           \"increment\": 1\n                 |          }\n                 |      }]\n                 |    }],\n                 |    \"swiftf\": [{\n                 |      \"kind\": \"swift\",\n                 |      \"image\": {\n                 |        \"name\": \"swiftaction\"\n                 |      },\n                 |      \"stemCells\": []\n                 |    }],\n                 |    \"phpf\": [{\n                 |      \"kind\": \"php\",\n                 |      \"image\": {\n                 |        \"name\": \"phpaction\"\n                 |      }\n                 |    }]\n                 |  }\n                 |}\n                 |\"\"\".stripMargin.parseJson.asJsObject\n\n    val reactive = Some(ReactivePrewarmingConfig(1, 4, FiniteDuration(2, TimeUnit.MINUTES), 1, 1))\n    val js14 = RuntimeManifest(\n      \"nodejs:20\",\n      ImageName(\"nodejsaction\"),\n      default = Some(true),\n      stemCells = Some(List(StemCell(1, 128.MB, reactive), StemCell(1, 256.MB, reactive))))\n    val js12 = RuntimeManifest(\n      \"nodejs:12\",\n      ImageName(\"nodejsaction\"),\n      deprecated = Some(true),\n      stemCells = Some(List(StemCell(1, 128.MB, reactive))))\n    val py = RuntimeManifest(\"python\", ImageName(\"pythonaction\"), stemCells = Some(List(StemCell(2, 256.MB, reactive))))\n    val sw = RuntimeManifest(\"swift\", ImageName(\"swiftaction\"), stemCells = Some(List.empty))\n    val ph = RuntimeManifest(\"php\", ImageName(\"phpaction\"))\n    val mf = ExecManifest.runtimes(json, RuntimeManifestConfig()).get\n\n    mf shouldBe {\n      Runtimes(\n        Set(\n          RuntimeFamily(\"nodef\", Set(js14, js12)),\n          RuntimeFamily(\"pythonf\", Set(py)),\n          RuntimeFamily(\"swiftf\", Set(sw)),\n          RuntimeFamily(\"phpf\", Set(ph))),\n        Set.empty,\n        None)\n    }\n\n    mf.stemcells.flatMap {\n      case (m, cells) =>\n        cells.map { c =>\n          (m.kind, m.image, c.initialCount, c.memory)\n        }\n    }.toList should contain theSameElementsAs List(\n      (js14.kind, js14.image, 1, 128.MB),\n      (js14.kind, js14.image, 1, 256.MB),\n      (js12.kind, js12.image, 1, 128.MB),\n      (py.kind, py.image, 2, 256.MB))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/ExecTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport org.apache.pekko.http.scaladsl.model.ContentTypes\nimport common.StreamLogging\nimport spray.json._\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.entity.Attachments.{Attached, Inline}\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.core.entity.{\n  BlackBoxExec,\n  CodeExecAsAttachment,\n  CodeExecAsString,\n  Exec,\n  ExecManifest,\n  WhiskAction\n}\n\nimport scala.collection.mutable\n\n@RunWith(classOf[JUnitRunner])\nclass ExecTests extends AnyFlatSpec with Matchers with StreamLogging with BeforeAndAfterAll {\n  behavior of \"exec deserialization\"\n\n  val config = new WhiskConfig(ExecManifest.requiredProperties)\n  ExecManifest.initialize(config)\n\n  override protected def afterAll(): Unit = {\n    ExecManifest.initialize(config)\n    super.afterAll()\n  }\n\n  it should \"read existing code string as attachment\" in {\n    val json = \"\"\"{\n                 |  \"name\": \"action_tests_name2\",\n                 |  \"_id\": \"anon-Yzycx8QnIYDp3Tby0Fnj23KcMtH/action_tests_name2\",\n                 |  \"publish\": false,\n                 |  \"annotations\": [],\n                 |  \"version\": \"0.0.1\",\n                 |  \"updated\": 1533623651650,\n                 |  \"entityType\": \"action\",\n                 |  \"exec\": {\n                 |    \"kind\": \"nodejs:20\",\n                 |    \"code\": \"foo\",\n                 |    \"binary\": false\n                 |  },\n                 |  \"parameters\": [\n                 |    {\n                 |      \"key\": \"x\",\n                 |      \"value\": \"b\"\n                 |    }\n                 |  ],\n                 |  \"limits\": {\n                 |    \"timeout\": 60000,\n                 |    \"memory\": 256,\n                 |    \"logs\": 10\n                 |  },\n                 |  \"namespace\": \"anon-Yzycx8QnIYDp3Tby0Fnj23KcMtH\"\n                 |}\"\"\".stripMargin.parseJson.asJsObject\n    val action = WhiskAction.serdes.read(json)\n    action.exec should matchPattern { case CodeExecAsAttachment(_, Inline(\"foo\"), None, false) => }\n  }\n\n  it should \"properly determine binary property\" in {\n    val j1 = \"\"\"{\n               |  \"kind\": \"nodejs:20\",\n               |  \"code\": \"SGVsbG8gT3BlbldoaXNr\",\n               |  \"binary\": false\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    Exec.serdes.read(j1) should matchPattern {\n      case CodeExecAsAttachment(_, Inline(\"SGVsbG8gT3BlbldoaXNr\"), None, true) =>\n    }\n\n    val j2 = \"\"\"{\n               |  \"kind\": \"nodejs:20\",\n               |  \"code\": \"while (true)\",\n               |  \"binary\": false\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    Exec.serdes.read(j2) should matchPattern {\n      case CodeExecAsAttachment(_, Inline(\"while (true)\"), None, false) =>\n    }\n\n    //Defaults to binary\n    val j3 = \"\"\"{\n               |  \"kind\": \"nodejs:20\",\n               |  \"code\": \"while (true)\"\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    Exec.serdes.read(j3) should matchPattern {\n      case CodeExecAsAttachment(_, Inline(\"while (true)\"), None, false) =>\n    }\n  }\n\n  it should \"read code stored as attachment\" in {\n    val json = \"\"\"{\n                 |  \"kind\": \"java:8\",\n                 |  \"code\": {\n                 |    \"attachmentName\": \"foo:bar\",\n                 |    \"attachmentType\": \"application/java-archive\",\n                 |    \"length\": 32768,\n                 |    \"digest\": \"sha256-foo\"\n                 |  },\n                 |  \"binary\": true,\n                 |  \"main\": \"hello\"\n                 |}\"\"\".stripMargin.parseJson.asJsObject\n    Exec.serdes.read(json) should matchPattern {\n      case CodeExecAsAttachment(_, Attached(\"foo:bar\", _, Some(32768), Some(\"sha256-foo\")), Some(\"hello\"), true) =>\n    }\n  }\n\n  it should \"read code stored as jar property\" in {\n    val j1 = \"\"\"{\n               |  \"kind\": \"nodejs:20\",\n               |  \"jar\": \"SGVsbG8gT3BlbldoaXNr\",\n               |  \"binary\": false\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    Exec.serdes.read(j1) should matchPattern {\n      case CodeExecAsAttachment(_, Inline(\"SGVsbG8gT3BlbldoaXNr\"), None, true) =>\n    }\n  }\n\n  it should \"read existing code string as string with old manifest\" in {\n    val oldManifestJson =\n      \"\"\"{\n        |  \"runtimes\": {\n        |    \"nodejs\": [\n        |      {\n        |        \"kind\": \"nodejs:20\",\n        |        \"default\": true,\n        |        \"image\": {\n        |          \"prefix\": \"openwhisk\",\n        |          \"name\": \"nodejs6action\",\n        |          \"tag\": \"latest\"\n        |        },\n        |        \"deprecated\": false,\n        |        \"stemCells\": [{\n        |          \"initialCount\": 2,\n        |          \"memory\": \"256 MB\"\n        |        }]\n        |      }\n        |    ]\n        |  }\n        |}\"\"\".stripMargin.parseJson.compactPrint\n\n    val oldConfig =\n      new TestConfig(Map(WhiskConfig.runtimesManifest -> oldManifestJson), ExecManifest.requiredProperties)\n    ExecManifest.initialize(oldConfig)\n    val j1 = \"\"\"{\n               |  \"kind\": \"nodejs:20\",\n               |  \"code\": \"SGVsbG8gT3BlbldoaXNr\",\n               |  \"binary\": false\n             |}\"\"\".stripMargin.parseJson.asJsObject\n\n    Exec.serdes.read(j1) should matchPattern {\n      case CodeExecAsString(_, \"SGVsbG8gT3BlbldoaXNr\", None) =>\n    }\n\n    //Reset config back\n    ExecManifest.initialize(config)\n  }\n\n  behavior of \"blackbox exec deserialization\"\n\n  it should \"read existing code string as attachment\" in {\n    val json = \"\"\"{\n                 |  \"name\": \"action_tests_name2\",\n                 |  \"_id\": \"anon-Yzycx8QnIYDp3Tby0Fnj23KcMtH/action_tests_name2\",\n                 |  \"publish\": false,\n                 |  \"annotations\": [],\n                 |  \"version\": \"0.0.1\",\n                 |  \"updated\": 1533623651650,\n                 |  \"entityType\": \"action\",\n                 |  \"exec\": {\n                 |    \"kind\": \"blackbox\",\n                 |    \"image\": \"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\",\n                 |    \"code\": \"foo\",\n                 |    \"binary\": false\n                 |  },\n                 |  \"parameters\": [\n                 |    {\n                 |      \"key\": \"x\",\n                 |      \"value\": \"b\"\n                 |    }\n                 |  ],\n                 |  \"limits\": {\n                 |    \"timeout\": 60000,\n                 |    \"memory\": 256,\n                 |    \"logs\": 10\n                 |  },\n                 |  \"namespace\": \"anon-Yzycx8QnIYDp3Tby0Fnj23KcMtH\"\n                 |}\"\"\".stripMargin.parseJson.asJsObject\n    val action = WhiskAction.serdes.read(json)\n    action.exec should matchPattern { case BlackBoxExec(_, Some(Inline(\"foo\")), None, false, false) => }\n  }\n\n  it should \"properly determine binary property\" in {\n    val j1 = \"\"\"{\n               |    \"kind\": \"blackbox\",\n               |    \"image\": \"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\",\n               |    \"code\": \"SGVsbG8gT3BlbldoaXNr\",\n               |    \"binary\": false\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    Exec.serdes.read(j1) should matchPattern {\n      case BlackBoxExec(_, Some(Inline(\"SGVsbG8gT3BlbldoaXNr\")), None, false, true) =>\n    }\n\n    val j2 = \"\"\"{\n               |  \"kind\": \"blackbox\",\n               |  \"image\": \"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\",\n               |  \"code\":  \"while (true)\",\n               |  \"binary\": false\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    Exec.serdes.read(j2) should matchPattern {\n      case BlackBoxExec(_, Some(Inline(\"while (true)\")), None, false, false) =>\n    }\n\n    //Empty code should resolve as None\n    val j3 = \"\"\"{\n               |  \"kind\": \"blackbox\",\n               |  \"image\": \"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\",\n               |  \"code\": \" \"\n               |}\"\"\".stripMargin.parseJson.asJsObject\n    Exec.serdes.read(j3) should matchPattern {\n      case BlackBoxExec(_, None, None, false, false) =>\n    }\n\n    val j4 = \"\"\"{\n                 |  \"kind\": \"blackbox\",\n                 |  \"image\": \"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\",\n                 |  \"code\": {\n                 |    \"attachmentName\": \"foo:bar\",\n                 |    \"attachmentType\": \"application/octet-stream\",\n                 |    \"length\": 32768,\n                 |    \"digest\": \"sha256-foo\"\n                 |  },\n                 |  \"binary\": true,\n                 |  \"main\": \"hello\"\n                 |}\"\"\".stripMargin.parseJson.asJsObject\n    Exec.serdes.read(j4) should matchPattern {\n      case BlackBoxExec(_, Some(Attached(\"foo:bar\", _, Some(32768), Some(\"sha256-foo\"))), Some(\"hello\"), false, true) =>\n    }\n  }\n\n  behavior of \"blackbox exec serialization\"\n\n  it should \"serialize with inline attachment\" in {\n    val bb = BlackBoxExec(\n      ImageName.fromString(\"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\").get,\n      Some(Inline(\"foo\")),\n      None,\n      false,\n      false)\n    val js = Exec.serdes.write(bb)\n\n    val js2 = \"\"\"{\n                |  \"kind\": \"blackbox\",\n                |  \"image\": \"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\",\n                |  \"binary\": false,\n                |  \"code\": \"foo\"\n                |}\"\"\".stripMargin.parseJson.asJsObject\n    js shouldBe js2\n  }\n\n  it should \"serialize with attached attachment\" in {\n    val bb = BlackBoxExec(\n      ImageName.fromString(\"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\").get,\n      Some(Attached(\"foo\", ContentTypes.`application/octet-stream`, Some(42), Some(\"sha1-42\"))),\n      None,\n      false,\n      true)\n    val js = Exec.serdes.write(bb)\n\n    val js2 = \"\"\"{\n                |  \"kind\": \"blackbox\",\n                |  \"image\": \"docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1\",\n                |  \"binary\": true,\n                |  \"code\": {\n                |    \"attachmentName\": \"foo\",\n                |    \"attachmentType\": \"application/octet-stream\",\n                |    \"length\": 42,\n                |    \"digest\": \"sha1-42\"\n                |  }\n                |}\"\"\".stripMargin.parseJson.asJsObject\n    js shouldBe js2\n  }\n\n  private class TestConfig(val props: Map[String, String], requiredProperties: Map[String, String])\n      extends WhiskConfig(requiredProperties) {\n    override protected def getProperties() = mutable.Map(props.toSeq: _*)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/InvokerInstanceIdTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.apache.openwhisk.core.entity.{ByteSize, InstanceId, InvokerInstanceId}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.{JsArray, JsNumber, JsObject, JsString}\n\nimport scala.util.Success\n\n@RunWith(classOf[JUnitRunner])\nclass InvokerInstanceIdTests extends AnyFlatSpec with Matchers {\n\n  behavior of \"InvokerInstanceIdTests\"\n\n  val defaultUserMemory: ByteSize = 1024.MB\n  it should \"serialize and deserialize InvokerInstanceId\" in {\n    val i = InvokerInstanceId(0, userMemory = defaultUserMemory)\n    i.serialize shouldBe JsObject(\n      \"instance\" -> JsNumber(i.instance),\n      \"userMemory\" -> JsString(i.userMemory.toString),\n      \"instanceType\" -> JsString(i.instanceType),\n      \"tags\" -> JsArray.empty,\n      \"dedicatedNamespaces\" -> JsArray.empty).compactPrint\n    i.serialize shouldBe i.toJson.compactPrint\n    InstanceId.parse(i.serialize) shouldBe Success(i)\n  }\n\n  it should \"serialize and deserialize InvokerInstanceId with optional field\" in {\n    val i1 = InvokerInstanceId(0, uniqueName = Some(\"uniqueInvoker\"), userMemory = defaultUserMemory)\n    i1.serialize shouldBe JsObject(\n      \"instance\" -> JsNumber(i1.instance),\n      \"userMemory\" -> JsString(i1.userMemory.toString),\n      \"instanceType\" -> JsString(i1.instanceType),\n      \"uniqueName\" -> JsString(i1.uniqueName.getOrElse(\"\")),\n      \"tags\" -> JsArray.empty,\n      \"dedicatedNamespaces\" -> JsArray.empty).compactPrint\n    i1.serialize shouldBe i1.toJson.compactPrint\n    InstanceId.parse(i1.serialize) shouldBe Success(i1)\n\n    val i2 = InvokerInstanceId(\n      0,\n      uniqueName = Some(\"uniqueInvoker\"),\n      displayedName = Some(\"displayedInvoker\"),\n      userMemory = defaultUserMemory)\n    i2.serialize shouldBe JsObject(\n      \"instance\" -> JsNumber(i2.instance),\n      \"userMemory\" -> JsString(i2.userMemory.toString),\n      \"instanceType\" -> JsString(i2.instanceType),\n      \"uniqueName\" -> JsString(i2.uniqueName.getOrElse(\"\")),\n      \"displayedName\" -> JsString(i2.displayedName.getOrElse(\"\")),\n      \"tags\" -> JsArray.empty,\n      \"dedicatedNamespaces\" -> JsArray.empty).compactPrint\n    i2.serialize shouldBe i2.toJson.compactPrint\n    InstanceId.parse(i2.serialize) shouldBe Success(i2)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/MigrationEntities.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport org.apache.openwhisk.core.database.DocumentFactory\nimport spray.json._\nimport org.apache.openwhisk.core.entity._\n\n/**\n * Contains types which represent former versions of database schemas\n * to be able to test migration path\n */\n/**\n * Old schema of rules, containing the rules' status in the rule record\n * itself\n */\ncase class OldWhiskRule(namespace: EntityPath,\n                        override val name: EntityName,\n                        trigger: EntityName,\n                        action: EntityName,\n                        status: Status,\n                        version: SemVer = SemVer(),\n                        publish: Boolean = false,\n                        annotations: Parameters = Parameters())\n    extends WhiskEntity(name, \"rule\") {\n\n  def toJson = OldWhiskRule.serdes.write(this).asJsObject\n\n  def toWhiskRule = {\n    WhiskRule(\n      namespace,\n      name,\n      FullyQualifiedEntityName(namespace, trigger),\n      FullyQualifiedEntityName(namespace, action),\n      version,\n      publish,\n      annotations)\n  }\n}\n\nobject OldWhiskRule\n    extends DocumentFactory[OldWhiskRule]\n    with WhiskEntityQueries[OldWhiskRule]\n    with DefaultJsonProtocol {\n\n  override val collectionName = \"rules\"\n  override implicit val serdes = jsonFormat8(OldWhiskRule.apply)\n}\n\n/**\n * Old schema of triggers, not containing a map of ReducedRules\n */\ncase class OldWhiskTrigger(namespace: EntityPath,\n                           override val name: EntityName,\n                           parameters: Parameters = Parameters(),\n                           limits: TriggerLimits = TriggerLimits(),\n                           version: SemVer = SemVer(),\n                           publish: Boolean = false,\n                           annotations: Parameters = Parameters())\n    extends WhiskEntity(name, \"trigger\") {\n\n  def toJson = OldWhiskTrigger.serdes.write(this).asJsObject\n\n  def toWhiskTrigger = WhiskTrigger(namespace, name, parameters, limits, version, publish, annotations)\n}\n\nobject OldWhiskTrigger\n    extends DocumentFactory[OldWhiskTrigger]\n    with WhiskEntityQueries[OldWhiskTrigger]\n    with DefaultJsonProtocol {\n\n  override val collectionName = \"triggers\"\n  override implicit val serdes = jsonFormat7(OldWhiskTrigger.apply)\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/ParameterEncryptionTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.apache.openwhisk.core.entity.test\n\nimport java.security.InvalidAlgorithmParameterException\n\nimport org.apache.openwhisk.core.entity._\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\n@RunWith(classOf[JUnitRunner])\nclass ParameterEncryptionTests extends AnyFlatSpec with Matchers with BeforeAndAfter {\n\n  val k128 = \"ra1V6AfOYAv0jCzEdufIFA==\"\n  val k256 = \"j5rLzhtxwzPyUVUy8/p8XJmBoKeDoSzNJP1SITJEY9E=\"\n\n  // default is no-op but keys are available to decode encoded params\n  val noop = ParameterEncryption(ParameterStorageConfig(aes128 = Some(k128), aes256 = Some(k256)))\n\n  val aes128decoder = ParameterEncryption(ParameterStorageConfig(\"aes-128\", aes128 = Some(k128)))\n  val aes128encoder = aes128decoder.default\n\n  val aes256decoder = ParameterEncryption(ParameterStorageConfig(\"aes-256\", aes256 = Some(k256)))\n  val aes256encoder = aes256decoder.default\n\n  val parameters = new Parameters(\n    Map(\n      new ParameterName(\"one\") -> new ParameterValue(\"secret\".toJson, false),\n      new ParameterName(\"two\") -> new ParameterValue(\"secret\".toJson, true)))\n\n  behavior of \"ParameterEncryption\"\n\n  it should \"not have a default coder when turned off\" in {\n    ParameterEncryption(ParameterStorageConfig(\"\")).default shouldBe empty\n    ParameterEncryption(ParameterStorageConfig(\"off\")).default shouldBe empty\n    ParameterEncryption(ParameterStorageConfig(\"noop\")).default shouldBe empty\n    ParameterEncryption(ParameterStorageConfig(\"OFF\")).default shouldBe empty\n    ParameterEncryption(ParameterStorageConfig(\"NOOP\")).default shouldBe empty\n  }\n\n  behavior of \"Parameters\"\n\n  it should \"handle decryption of json objects\" in {\n    val originalValue =\n      \"\"\"\n        |[\n        |  { \"key\": \"paramName1\", \"init\": false, \"value\": \"from-action\" },\n        |  { \"key\": \"paramName2\", \"init\": false, \"value\": \"from-pack\" }\n        |]\n        |\"\"\".stripMargin\n\n    val p = Parameters.serdes.read(originalValue.parseJson)\n    p.get(\"paramName1\").get.convertTo[String] shouldBe \"from-action\"\n    p.get(\"paramName2\").get.convertTo[String] shouldBe \"from-pack\"\n    p.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe empty\n    }\n  }\n\n  it should \"handle decryption of json objects with null field\" in {\n    val originalValue =\n      \"\"\"\n        |[\n        |  { \"key\": \"paramName1\", \"encryption\":null, \"init\": false, \"value\": \"from-action\" },\n        |  { \"key\": \"paramName2\", \"encryption\":null, \"init\": false, \"value\": \"from-pack\" }\n        |]\n        |\"\"\".stripMargin\n\n    val p = Parameters.serdes.read(originalValue.parseJson)\n    p.get(\"paramName1\").get.convertTo[String] shouldBe \"from-action\"\n    p.get(\"paramName2\").get.convertTo[String] shouldBe \"from-pack\"\n    p.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe empty\n    }\n  }\n\n  it should \"drop encryption propery when no longer encrypted\" in {\n    val originalValue =\n      \"\"\"\n        |[\n        |  { \"key\": \"paramName1\", \"encryption\":null, \"init\": false, \"value\": \"from-action\" },\n        |  { \"key\": \"paramName2\", \"encryption\":null, \"init\": false, \"value\": \"from-pack\" }\n        |]\n        |\"\"\".stripMargin\n\n    val p = Parameters.serdes.read(originalValue.parseJson)\n    Parameters.serdes.write(p).compactPrint should not include \"encryption\"\n    p.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe empty\n    }\n  }\n\n  it should \"read the merged message payload from kafka into parameters\" in {\n    val locked = parameters.lock(aes128encoder)\n    val mixedParams = locked.merge(Some(Parameters(\"plain\", \"test-plain\").toJsObject))\n    mixedParams shouldBe defined\n    mixedParams.get.fields(\"one\") shouldBe locked.get(\"one\").get\n    mixedParams.get.fields(\"two\") shouldBe locked.get(\"two\").get\n    mixedParams.get.fields(\"two\") should not be locked.get(\"one\").get\n    mixedParams.get.fields(\"plain\") shouldBe JsString(\"test-plain\")\n  }\n\n  behavior of \"AesParameterEncryption\"\n\n  it should \"correctly mark the encrypted parameters after lock\" in {\n    val locked = parameters.lock(aes128encoder)\n\n    locked.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe Some(\"aes-128\")\n        paramValue.value.convertTo[String] should not be \"secret\"\n    }\n  }\n\n  it should \"serialize to json correctly\" in {\n    val locked = parameters.lock(aes128encoder)\n    locked.toJsObject.toString should fullyMatch regex \"\"\"\\Q{\"one\":\"\\E.*\\Q\",\"two\":\"\\E.*\\Q\"}\"\"\".stripMargin.r\n    locked.lockedParameters() shouldBe Map(\"one\" -> \"aes-128\", \"two\" -> \"aes-128\")\n  }\n\n  it should \"serialize to json correctly when a locked parameter is overridden\" in {\n    val locked = parameters.lock(aes128encoder)\n    locked\n      .merge(Some(JsObject(\"one\" -> JsString(\"override\"))))\n      .get\n      .compactPrint should fullyMatch regex \"\"\"\\Q{\"one\":\"override\",\"two\":\"\\E.*\\Q\"}\"\"\".stripMargin.r\n    locked.lockedParameters(Set(\"one\")) shouldBe Map(\"two\" -> \"aes-128\")\n  }\n\n  it should \"correctly decrypt encrypted values\" in {\n    val locked = parameters.lock(aes128encoder)\n\n    locked.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe Some(\"aes-128\")\n        paramValue.value.convertTo[String] should not be \"secret\"\n    }\n\n    val unlocked = locked.unlock(aes128decoder)\n    unlocked.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe empty\n        paramValue.value.convertTo[String] shouldBe \"secret\"\n    }\n  }\n\n  it should \"correctly decrypt encrypted JsObject values\" in {\n    val obj = Map(\"key\" -> \"xyz\".toJson, \"value\" -> \"v1\".toJson).toJson\n    val complexParam = new Parameters(Map(new ParameterName(\"one\") -> new ParameterValue(obj, false)))\n\n    val locked = complexParam.lock(aes128encoder)\n    locked.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe Some(\"aes-128\")\n        paramValue.value.convertTo[String] should not be \"secret\"\n    }\n\n    val unlocked = locked.unlock(aes128decoder)\n    unlocked.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe empty\n        paramValue.value shouldBe obj\n    }\n  }\n  it should \"correctly decrypt encrypted multiline values\" in {\n    val lines = \"line1\\nline2\\nline3\\nline4\"\n    val multiline = new Parameters(Map(new ParameterName(\"one\") -> new ParameterValue(JsString(lines), false)))\n\n    val locked = multiline.lock(aes128encoder)\n    locked.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe Some(\"aes-128\")\n        paramValue.value.convertTo[String] should not be \"secret\"\n    }\n\n    val unlocked = locked.unlock(aes128decoder)\n    unlocked.params.foreach {\n      case (_, paramValue) =>\n        paramValue.encryption shouldBe empty\n        paramValue.value.convertTo[String] shouldBe lines\n    }\n  }\n\n  // Not sure having cancelled tests is a good idea either, need to work on aes256 packaging.\n  it should \"work if with aes256 if policy allows it\" in {\n    try {\n      val locked = parameters.lock(aes256encoder)\n      locked.params.foreach {\n        case (_, paramValue) =>\n          paramValue.encryption shouldBe Some(\"aes-256\")\n          paramValue.value.convertTo[String] should not be \"secret\"\n      }\n\n      val unlocked = locked.unlock(noop)\n      unlocked.params.foreach {\n        case (_, paramValue) =>\n          paramValue.encryption shouldBe empty\n          paramValue.value.convertTo[String] shouldBe \"secret\"\n      }\n    } catch {\n      case e: InvalidAlgorithmParameterException =>\n        cancel(e.toString)\n    }\n  }\n\n  it should \"support reverting back to Noop encryption\" in {\n    try {\n      val locked = parameters.lock(aes128encoder)\n      locked.params.foreach {\n        case (_, paramValue) =>\n          paramValue.encryption shouldBe Some(\"aes-128\")\n          paramValue.value.convertTo[String] should not be \"secret\"\n      }\n\n      val lockedJson = Parameters.serdes.write(locked).compactPrint\n      val toDecrypt = Parameters.serdes.read(lockedJson.parseJson)\n\n      // defaults to no-op\n      val unlocked = toDecrypt.unlock(noop)\n      unlocked.params.foreach {\n        case (_, paramValue) =>\n          paramValue.encryption shouldBe empty\n          paramValue.value.convertTo[String] shouldBe \"secret\"\n      }\n\n      unlocked.toJsObject shouldBe JsObject(\"one\" -> \"secret\".toJson, \"two\" -> \"secret\".toJson)\n    } catch {\n      case e: InvalidAlgorithmParameterException =>\n        cancel(e.toString)\n    }\n  }\n\n  behavior of \"No-op Encryption\"\n\n  it should \"not mark parameters as encrypted\" in {\n    val locked = parameters.lock()\n    locked.params.foreach {\n      case (_, paramValue) =>\n        paramValue.value.convertTo[String] shouldBe \"secret\"\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/SchemaTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport java.util.Base64\n\nimport scala.language.postfixOps\nimport scala.util.Failure\nimport scala.util.Try\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.controller.test.WhiskAuthHelpers\nimport org.apache.openwhisk.core.entitlement.Privilege\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.utils.JsHelpers\n\n@RunWith(classOf[JUnitRunner])\nclass SchemaTests extends AnyFlatSpec with BeforeAndAfter with ExecHelpers with Matchers {\n\n  behavior of \"Privilege\"\n\n  private implicit class ExecJson(e: Exec) {\n    def asJson: JsObject = Exec.serdes.write(e).asJsObject\n  }\n\n  it should \"serdes a right\" in {\n    Privilege.serdes.read(\"READ\".toJson) shouldBe Privilege.READ\n    Privilege.serdes.read(\"read\".toJson) shouldBe Privilege.READ\n    a[DeserializationException] should be thrownBy Privilege.serdes.read(\"???\".toJson)\n  }\n\n  behavior of \"TransactionId\"\n\n  it should \"serdes a transaction id without extraLogging parameter\" in {\n    val txIdWithoutParameter = TransactionId(\"4711\")\n\n    // test serialization\n    val serializedTxIdWithoutParameter = TransactionId.serdes.write(txIdWithoutParameter)\n    serializedTxIdWithoutParameter match {\n      case JsArray(Vector(JsString(id), JsNumber(_))) =>\n        assert(id == txIdWithoutParameter.meta.id)\n      case _ => withClue(serializedTxIdWithoutParameter) { assert(false) }\n    }\n\n    // test deserialization\n    val deserializedTxIdWithoutParameter = TransactionId.serdes.read(serializedTxIdWithoutParameter)\n    deserializedTxIdWithoutParameter.meta.id should equal(txIdWithoutParameter.meta.id)\n    deserializedTxIdWithoutParameter.meta.extraLogging should equal(false)\n  }\n\n  it should \"serdes a transaction id with extraLogging parameter\" in {\n    val txIdWithParameter = TransactionId(\"4711\", true)\n\n    // test serialization\n    val serializedTxIdWithParameter = TransactionId.serdes.write(txIdWithParameter)\n    serializedTxIdWithParameter match {\n      case JsArray(Vector(JsString(id), JsNumber(_), JsBoolean(extraLogging))) =>\n        assert(id == txIdWithParameter.meta.id)\n        assert(extraLogging)\n      case _ => withClue(serializedTxIdWithParameter) { assert(false) }\n    }\n\n    // test deserialization\n    val deserializedTxIdWithParameter = TransactionId.serdes.read(serializedTxIdWithParameter)\n    deserializedTxIdWithParameter.meta.id should equal(txIdWithParameter.meta.id)\n    assert(deserializedTxIdWithParameter.meta.extraLogging)\n  }\n\n  behavior of \"Identity\"\n\n  it should \"serdes write an identity\" in {\n    val i = WhiskAuthHelpers.newIdentity()\n    val expected = JsObject(\n      \"subject\" -> i.subject.asString.toJson,\n      \"namespace\" -> i.namespace.toJson,\n      \"authkey\" -> i.authkey.toEnvironment,\n      \"rights\" -> Array(\"READ\", \"PUT\", \"DELETE\", \"ACTIVATE\").toJson,\n      \"limits\" -> JsObject.empty)\n    Identity.serdes.write(i) shouldBe expected\n  }\n\n  it should \"serdes read a generic identity\" in {\n    val uuid = UUID()\n    val subject = Subject(\"test_subject\")\n    val entity = EntityName(\"test_subject\")\n    val genericAuthKey = new GenericAuthKey(JsObject(\"test_key\" -> \"test_value\".toJson))\n    val i = WhiskAuthHelpers.newIdentityGenricAuth(subject, uuid, genericAuthKey)\n\n    val json = JsObject(\n      \"subject\" -> Subject(\"test_subject\").toJson,\n      \"namespace\" -> Namespace(entity, uuid).toJson,\n      \"authkey\" -> JsObject(\"test_key\" -> \"test_value\".toJson),\n      \"rights\" -> Array(\"READ\", \"PUT\", \"DELETE\", \"ACTIVATE\").toJson,\n      \"limits\" -> JsObject.empty)\n    Identity.serdes.read(json) shouldBe i\n  }\n\n  it should \"deserialize view result\" in {\n    implicit val tid = TransactionId(\"test\")\n    val subject = Subject(\"test_subject\")\n    val id = WhiskAuthHelpers.newIdentity(subject)\n\n    val json = JsObject(\n      \"id\" -> subject.asString.toJson,\n      \"value\" -> JsObject(\n        \"uuid\" -> id.authkey.asInstanceOf[BasicAuthenticationAuthKey].uuid.toJson,\n        \"key\" -> id.authkey.asInstanceOf[BasicAuthenticationAuthKey].key.toJson,\n        \"namespace\" -> \"test_subject\".toJson),\n      \"doc\" -> JsNull)\n\n    Identity.rowToIdentity(json, \"test\") shouldBe id\n  }\n\n  behavior of \"DocInfo\"\n\n  it should \"accept well formed doc info\" in {\n    Seq(\"a\", \" a\", \"a \").foreach { i =>\n      val d = DocInfo(i)\n      assert(d.id.asString == i.trim)\n    }\n  }\n\n  it should \"accept any string as doc revision\" in {\n    Seq(\"a\", \" a\", \"a \", \"\", null).foreach { i =>\n      val d = DocRevision(i)\n      assert(d.rev == (if (i != null) i.trim else null))\n    }\n\n    DocRevision.serdes.read(JsNull) shouldBe DocRevision.empty\n    DocRevision.serdes.read(JsString.empty) shouldBe DocRevision(\"\")\n    DocRevision.serdes.read(JsString(\"a\")) shouldBe DocRevision(\"a\")\n    DocRevision.serdes.read(JsString(\" a\")) shouldBe DocRevision(\"a\")\n    DocRevision.serdes.read(JsString(\"a \")) shouldBe DocRevision(\"a\")\n    a[DeserializationException] should be thrownBy DocRevision.serdes.read(JsNumber(1))\n  }\n\n  it should \"reject malformed doc info\" in {\n    Seq(null, \"\", \" \").foreach { i =>\n      an[IllegalArgumentException] should be thrownBy DocInfo(i)\n    }\n  }\n\n  it should \"reject malformed doc ids\" in {\n    Seq(null, \"\", \" \").foreach { i =>\n      an[IllegalArgumentException] should be thrownBy DocId(i)\n    }\n  }\n\n  behavior of \"EntityPath\"\n\n  it should \"accept well formed paths\" in {\n    val paths = Seq(\n      \"/a\",\n      \"//a\",\n      \"//a//\",\n      \"//a//b//c\",\n      \"//a//b/c//\",\n      \"a\",\n      \"a/b\",\n      \"a/b/\",\n      \"a@b.c\",\n      \"a@b.c/\",\n      \"a@b.c/d\",\n      \"_a/\",\n      \"_ _\",\n      \"a/b/c\")\n    val expected =\n      Seq(\"a\", \"a\", \"a\", \"a/b/c\", \"a/b/c\", \"a\", \"a/b\", \"a/b\", \"a@b.c\", \"a@b.c\", \"a@b.c/d\", \"_a\", \"_ _\", \"a/b/c\")\n    val spaces = paths.zip(expected).foreach { p =>\n      EntityPath(p._1).namespace shouldBe p._2\n    }\n\n    EntityPath.DEFAULT.addPath(EntityName(\"a\")).toString shouldBe \"_/a\"\n\n    EntityPath.DEFAULT.addPath(EntityPath(\"a\")).toString shouldBe \"_/a\"\n    EntityPath.DEFAULT.addPath(EntityPath(\"a/b\")).toString shouldBe \"_/a/b\"\n\n    EntityPath.DEFAULT.resolveNamespace(EntityName(\"a\")) shouldBe EntityPath(\"a\")\n    EntityPath(\"a\").resolveNamespace(EntityName(\"b\")) shouldBe EntityPath(\"a\")\n\n    EntityPath.DEFAULT.resolveNamespace(Namespace(EntityName(\"a\"), UUID())) shouldBe EntityPath(\"a\")\n    EntityPath(\"a\").resolveNamespace(Namespace(EntityName(\"b\"), UUID())) shouldBe EntityPath(\"a\")\n\n    EntityPath(\"a\").defaultPackage shouldBe true\n    EntityPath(\"a/b\").defaultPackage shouldBe false\n\n    EntityPath(\"a\").root shouldBe EntityName(\"a\")\n    EntityPath(\"a\").last shouldBe EntityName(\"a\")\n    EntityPath(\"a/b\").root shouldBe EntityName(\"a\")\n    EntityPath(\"a/b\").last shouldBe EntityName(\"b\")\n\n    EntityPath(\"a\").relativePath shouldBe empty\n    EntityPath(\"a/b\").relativePath shouldBe Some(EntityPath(\"b\"))\n    EntityPath(\"a/b/c\").relativePath shouldBe Some(EntityPath(\"b/c\"))\n\n    EntityPath(\"a/b\").toFullyQualifiedEntityName shouldBe FullyQualifiedEntityName(EntityPath(\"a\"), EntityName(\"b\"))\n  }\n\n  it should \"reject malformed paths\" in {\n    val paths = Seq(\n      null,\n      \"\",\n      \" \",\n      \"a/ \",\n      \"a/b/c \",\n      \" xxx\",\n      \"xxx \",\n      \" xxx\",\n      \"xxx/ \",\n      \"/\",\n      \" /\",\n      \"/ \",\n      \"//\",\n      \"///\",\n      \" / / / \",\n      \"a/b/ c\",\n      \"a/ /b\",\n      \" a/ b\")\n    paths.foreach { p =>\n      an[IllegalArgumentException] should be thrownBy EntityPath(p)\n    }\n\n    an[IllegalArgumentException] should be thrownBy EntityPath(\"a\").toFullyQualifiedEntityName\n  }\n\n  behavior of \"EntityName\"\n\n  it should \"accept well formed names\" in {\n    val paths = Seq(\n      \"a\",\n      \"a b\",\n      \"a@b.c&d\",\n      \"a@&b\",\n      \"_a\",\n      \"_\",\n      \"_ _\",\n      \"a0\",\n      \"a 0\",\n      \"a.0\",\n      \"a@@&\",\n      \"0\",\n      \"0.0\",\n      \"0.0.0\",\n      \"0a\",\n      \"0.a\",\n      \"a\" * EntityName.ENTITY_NAME_MAX_LENGTH)\n    paths.foreach { n =>\n      assert(EntityName(n).toString == n)\n    }\n  }\n\n  it should \"reject malformed names\" in {\n    val paths = Seq(\n      null,\n      \"\",\n      \" \",\n      \" xxx\",\n      \"xxx \",\n      \"/\",\n      \" /\",\n      \"/ \",\n      \"0 \",\n      \"a=2b\",\n      \"_ \",\n      \"a?b\",\n      \"x#x\",\n      \"a§b\",\n      \"a  \",\n      \"a()b\",\n      \"a{}b\",\n      \"a \\t\",\n      \"-abc\",\n      \"&abc\",\n      \"a\\n\",\n      \"a\" * (EntityName.ENTITY_NAME_MAX_LENGTH + 1))\n    paths.foreach { p =>\n      an[IllegalArgumentException] should be thrownBy EntityName(p)\n    }\n  }\n\n  behavior of \"FullyQualifiedEntityName\"\n\n  it should \"work with paths\" in {\n    FullyQualifiedEntityName(EntityPath(\"a\"), EntityName(\"b\")).add(EntityName(\"c\")) shouldBe\n      FullyQualifiedEntityName(EntityPath(\"a/b\"), EntityName(\"c\"))\n\n    FullyQualifiedEntityName(EntityPath(\"a\"), EntityName(\"b\")).fullPath shouldBe EntityPath(\"a/b\")\n  }\n\n  it should \"deserialize a fully qualified name without a version\" in {\n    val names = Seq(\n      JsObject(\"path\" -> \"a\".toJson, \"name\" -> \"b\".toJson),\n      JsObject(\"path\" -> \"a\".toJson, \"name\" -> \"b\".toJson, \"version\" -> \"0.0.1\".toJson),\n      JsString(\"a/b\"),\n      JsString(\"n/a/b\"),\n      JsString(\"/a/b\"),\n      JsString(\"/n/a/b\"),\n      JsString(\"b\")) //JsObject(\"namespace\" -> \"a\".toJson, \"name\" -> \"b\".toJson))\n\n    FullyQualifiedEntityName.serdes.read(names(0)) shouldBe FullyQualifiedEntityName(EntityPath(\"a\"), EntityName(\"b\"))\n    FullyQualifiedEntityName.serdes.read(names(1)) shouldBe FullyQualifiedEntityName(\n      EntityPath(\"a\"),\n      EntityName(\"b\"),\n      Some(SemVer()))\n    FullyQualifiedEntityName.serdes.read(names(2)) shouldBe FullyQualifiedEntityName(EntityPath(\"a\"), EntityName(\"b\"))\n    FullyQualifiedEntityName.serdes.read(names(3)) shouldBe FullyQualifiedEntityName(EntityPath(\"n/a\"), EntityName(\"b\"))\n    FullyQualifiedEntityName.serdes.read(names(4)) shouldBe FullyQualifiedEntityName(EntityPath(\"a\"), EntityName(\"b\"))\n    FullyQualifiedEntityName.serdes.read(names(5)) shouldBe FullyQualifiedEntityName(EntityPath(\"n/a\"), EntityName(\"b\"))\n    a[DeserializationException] should be thrownBy FullyQualifiedEntityName.serdes.read(names(6))\n\n    a[DeserializationException] should be thrownBy FullyQualifiedEntityName.serdesAsDocId.read(names(0))\n    a[DeserializationException] should be thrownBy FullyQualifiedEntityName.serdesAsDocId.read(names(1))\n    FullyQualifiedEntityName.serdesAsDocId.read(names(2)) shouldBe FullyQualifiedEntityName(\n      EntityPath(\"a\"),\n      EntityName(\"b\"))\n    FullyQualifiedEntityName.serdesAsDocId.read(names(3)) shouldBe FullyQualifiedEntityName(\n      EntityPath(\"n/a\"),\n      EntityName(\"b\"))\n    FullyQualifiedEntityName.serdesAsDocId.read(names(4)) shouldBe FullyQualifiedEntityName(\n      EntityPath(\"a\"),\n      EntityName(\"b\"))\n    FullyQualifiedEntityName.serdesAsDocId.read(names(5)) shouldBe FullyQualifiedEntityName(\n      EntityPath(\"n/a\"),\n      EntityName(\"b\"))\n    a[DeserializationException] should be thrownBy FullyQualifiedEntityName.serdesAsDocId.read(names(6))\n  }\n\n  it should \"resolve names that may or may not be fully qualified\" in {\n    FullyQualifiedEntityName.resolveName(JsString(\"a\"), EntityName(\"ns\")) shouldBe Some(\n      EntityPath(\"ns/a\").toFullyQualifiedEntityName)\n\n    FullyQualifiedEntityName.resolveName(JsString(\"/_/a\"), EntityName(\"ns\")) shouldBe Some(\n      EntityPath(\"ns/a\").toFullyQualifiedEntityName)\n\n    FullyQualifiedEntityName.resolveName(JsString(\"_/a\"), EntityName(\"ns\")) shouldBe Some(\n      EntityPath(\"ns/_/a\").toFullyQualifiedEntityName)\n\n    FullyQualifiedEntityName.resolveName(JsString(\"/_/a/b\"), EntityName(\"ns\")) shouldBe Some(\n      EntityPath(\"ns/a/b\").toFullyQualifiedEntityName)\n\n    FullyQualifiedEntityName.resolveName(JsString(\"a/b\"), EntityName(\"ns\")) shouldBe Some(\n      EntityPath(\"ns/a/b\").toFullyQualifiedEntityName)\n\n    FullyQualifiedEntityName.resolveName(JsString(\"a/b/c\"), EntityName(\"ns\")) shouldBe Some(\n      EntityPath(\"/a/b/c\").toFullyQualifiedEntityName)\n\n    FullyQualifiedEntityName.resolveName(JsString(\"a/b/c/d\"), EntityName(\"ns\")) shouldBe None\n\n    FullyQualifiedEntityName.resolveName(JsString(\"/a\"), EntityName(\"ns\")) shouldBe None\n\n    FullyQualifiedEntityName.resolveName(JsString(\"/a/b\"), EntityName(\"ns\")) shouldBe Some(\n      EntityPath(\"/a/b\").toFullyQualifiedEntityName)\n\n    FullyQualifiedEntityName.resolveName(JsString(\"/a/b/c\"), EntityName(\"ns\")) shouldBe Some(\n      EntityPath(\"/a/b/c\").toFullyQualifiedEntityName)\n\n    FullyQualifiedEntityName.resolveName(JsString(\"/a/b/c/d\"), EntityName(\"ns\")) shouldBe None\n\n    FullyQualifiedEntityName.resolveName(JsString.empty, EntityName(\"ns\")) shouldBe None\n  }\n\n  behavior of \"Binding\"\n\n  it should \"desiarilize legacy format\" in {\n    val names =\n      Seq(\n        JsObject(\"namespace\" -> \"a\".toJson, \"name\" -> \"b\".toJson),\n        JsObject.empty,\n        JsObject(\"name\" -> \"b\".toJson),\n        JsNull)\n\n    Binding.optionalBindingDeserializer.read(names(0)) shouldBe Some(Binding(EntityName(\"a\"), EntityName(\"b\")))\n    Binding.optionalBindingDeserializer.read(names(1)) shouldBe None\n    a[DeserializationException] should be thrownBy Binding.optionalBindingDeserializer.read(names(2))\n    a[DeserializationException] should be thrownBy Binding.optionalBindingDeserializer.read(names(3))\n  }\n\n  it should \"serialize optional binding to empty object\" in {\n    Binding.optionalBindingSerializer.write(None) shouldBe JsObject.empty\n  }\n\n  behavior of \"WhiskPackagePut\"\n\n  it should \"deserialize empty request\" in {\n    WhiskPackagePut.serdes.read(JsObject.empty) shouldBe WhiskPackagePut()\n    //WhiskPackagePut.serdes.read(JsObject(\"binding\" -> JsNull)) shouldBe WhiskPackagePut()\n    WhiskPackagePut.serdes.read(JsObject(\"binding\" -> JsObject.empty)) shouldBe WhiskPackagePut()\n    //WhiskPackagePut.serdes.read(JsObject(\"binding\" -> \"a/b\".toJson)) shouldBe WhiskPackagePut(binding = Some(Binding(EntityPath(\"a\"), EntityName(\"b\"))))\n    a[DeserializationException] should be thrownBy WhiskPackagePut.serdes.read(JsObject(\"binding\" -> JsNull))\n  }\n\n  behavior of \"WhiskPackage\"\n\n  it should \"not deserialize package without binding property\" in {\n    val pkg = WhiskPackage(EntityPath(\"a\"), EntityName(\"b\"))\n    WhiskPackage.serdes.read(JsObject(pkg.toJson.fields + (\"binding\" -> JsObject.empty))) shouldBe pkg\n    a[DeserializationException] should be thrownBy WhiskPackage.serdes.read(JsObject(pkg.toJson.fields - \"binding\"))\n  }\n\n  it should \"serialize package with empty binding property\" in {\n    val pkg = WhiskPackage(EntityPath(\"a\"), EntityName(\"b\"))\n    WhiskPackage.serdes.write(pkg) shouldBe JsObject(\n      \"namespace\" -> \"a\".toJson,\n      \"name\" -> \"b\".toJson,\n      \"binding\" -> JsObject.empty,\n      \"parameters\" -> Parameters().toJson,\n      \"version\" -> SemVer().toJson,\n      \"publish\" -> JsFalse,\n      \"annotations\" -> Parameters().toJson,\n      \"updated\" -> pkg.updated.toEpochMilli.toJson)\n  }\n\n  it should \"serialize and deserialize package binding\" in {\n    val pkg = WhiskPackage(EntityPath(\"a\"), EntityName(\"b\"), Some(Binding(EntityName(\"x\"), EntityName(\"y\"))))\n    val pkgAsJson = JsObject(\n      \"namespace\" -> \"a\".toJson,\n      \"name\" -> \"b\".toJson,\n      \"binding\" -> JsObject(\"namespace\" -> \"x\".toJson, \"name\" -> \"y\".toJson),\n      \"parameters\" -> Parameters().toJson,\n      \"version\" -> SemVer().toJson,\n      \"publish\" -> JsFalse,\n      \"annotations\" -> Parameters().toJson,\n      \"updated\" -> pkg.updated.toEpochMilli.toJson)\n    //val legacyPkgAsJson = JsObject(pkgAsJson.fields + (\"binding\" -> JsObject(\"namespace\" -> \"x\".toJson, \"name\" -> \"y\".toJson)))\n    WhiskPackage.serdes.write(pkg) shouldBe pkgAsJson\n    WhiskPackage.serdes.read(pkgAsJson) shouldBe pkg\n    //WhiskPackage.serdes.read(legacyPkgAsJson) shouldBe pkg\n  }\n\n  behavior of \"SemVer\"\n\n  it should \"parse semantic versions\" in {\n    val semvers = Seq(\"0.0.1\", \"1\", \"1.2\", \"1.2.3.\").map { SemVer(_) }\n    assert(semvers(0) == SemVer(0, 0, 1) && semvers(0).toString == \"0.0.1\")\n    assert(semvers(1) == SemVer(1, 0, 0) && semvers(1).toString == \"1.0.0\")\n    assert(semvers(2) == SemVer(1, 2, 0) && semvers(2).toString == \"1.2.0\")\n    assert(semvers(3) == SemVer(1, 2, 3) && semvers(3).toString == \"1.2.3\")\n\n  }\n\n  it should \"permit leading zeros but strip them away\" in {\n    val semvers = Seq(\"0.0.01\", \"01\", \"01.02\", \"01.02.003.\").map { SemVer(_) }\n    assert(semvers(0) == SemVer(0, 0, 1))\n    assert(semvers(1) == SemVer(1, 0, 0))\n    assert(semvers(2) == SemVer(1, 2, 0))\n    assert(semvers(3) == SemVer(1, 2, 3))\n  }\n\n  it should \"reject malformed semantic version\" in {\n    val semvers = Seq(\"0\", \"0.0.0\", \"00.00.00\", \".1\", \"-1\", \"0.-1.0\", \"0.0.-1\", \"xyz\", \"\", null)\n    semvers.foreach { v =>\n      val thrown = intercept[IllegalArgumentException] {\n        SemVer(v)\n      }\n      assert(thrown.getMessage.contains(\"bad semantic version\"))\n    }\n  }\n\n  it should \"reject negative values\" in {\n    an[IllegalArgumentException] should be thrownBy SemVer(-1, 0, 0)\n    an[IllegalArgumentException] should be thrownBy SemVer(0, -1, 0)\n    an[IllegalArgumentException] should be thrownBy SemVer(0, 0, -1)\n    an[IllegalArgumentException] should be thrownBy SemVer(0, 0, 0)\n  }\n\n  behavior of \"Exec\"\n\n  it should \"initialize exec manifest\" in {\n    val runtimes = ExecManifest.runtimesManifest\n    val kind = runtimes.resolveDefaultRuntime(\"nodejs:default\").get.kind\n    Some(kind) should contain oneOf (\"nodejs:12\", \"nodejs:20\")\n  }\n\n  it should \"properly deserialize and reserialize JSON\" in {\n    val b64Body = \"\"\"ZnVuY3Rpb24gbWFpbihhcmdzKSB7IHJldHVybiBhcmdzOyB9Cg==\"\"\"\n\n    val json = Seq[JsObject](\n      JsObject(\"kind\" -> \"nodejs:20\".toJson, \"code\" -> \"js1\".toJson, \"binary\" -> false.toJson),\n      JsObject(\"kind\" -> \"nodejs:20\".toJson, \"code\" -> \"js2\".toJson, \"binary\" -> false.toJson, \"foo\" -> \"bar\".toJson),\n      JsObject(\"kind\" -> \"swift:5.3\".toJson, \"code\" -> \"swift1\".toJson, \"binary\" -> false.toJson),\n      JsObject(\"kind\" -> \"swift:5.3\".toJson, \"code\" -> b64Body.toJson, \"binary\" -> true.toJson),\n      JsObject(\"kind\" -> \"nodejs:20\".toJson, \"code\" -> b64Body.toJson, \"binary\" -> true.toJson))\n\n    val execs = json.map { e =>\n      Exec.serdes.read(e)\n    }\n\n    assert(execs(0) == jsDefault(\"js1\") && json(0) == jsDefault(\"js1\").asJson)\n    assert(execs(1) == jsDefault(\"js2\") && json(1) != jsDefault(\"js2\").asJson) // ignores unknown properties\n    assert(execs(2) == swift(\"swift1\") && json(2) == swift(\"swift1\").asJson)\n    assert(execs(3) == swift(b64Body) && json(3) == swift(b64Body).asJson)\n    assert(execs(4) == jsDefault(b64Body) && json(4) == jsDefault(b64Body).asJson)\n  }\n\n  it should \"properly deserialize and reserialize JSON blackbox\" in {\n    val b64 = Base64.getEncoder()\n    val contents = b64.encodeToString(\"tarball\".getBytes)\n    val json = Seq[JsObject](\n      JsObject(\"kind\" -> \"blackbox\".toJson, \"image\" -> \"container1\".toJson, \"binary\" -> false.toJson),\n      JsObject(\n        \"kind\" -> \"blackbox\".toJson,\n        \"image\" -> \"container1\".toJson,\n        \"binary\" -> true.toJson,\n        \"code\" -> contents.toJson),\n      JsObject(\n        \"kind\" -> \"blackbox\".toJson,\n        \"image\" -> \"container1\".toJson,\n        \"binary\" -> true.toJson,\n        \"code\" -> contents.toJson,\n        \"main\" -> \"naim\".toJson))\n\n    val execs = json.map { e =>\n      Exec.serdes.read(e)\n    }\n\n    execs(0) shouldBe bb(\"container1\")\n    execs(1) shouldBe bb(\"container1\", contents)\n    execs(2) shouldBe bb(\"container1\", contents, Some(\"naim\"))\n\n    json(0) shouldBe bb(\"container1\").asJson\n    json(1) shouldBe bb(\"container1\", contents).asJson\n    json(2) shouldBe bb(\"container1\", contents, Some(\"naim\")).asJson\n\n    execs(0) shouldBe Exec.serdes.read(\n      JsObject(\n        \"kind\" -> \"blackbox\".toJson,\n        \"image\" -> \"container1\".toJson,\n        \"binary\" -> false.toJson,\n        \"code\" -> \" \".toJson))\n    execs(0) shouldBe Exec.serdes.read(\n      JsObject(\n        \"kind\" -> \"blackbox\".toJson,\n        \"image\" -> \"container1\".toJson,\n        \"binary\" -> false.toJson,\n        \"code\" -> \"\".toJson))\n  }\n\n  it should \"exclude undefined code in whisk action initializer\" in {\n    ExecutableWhiskAction(EntityPath(\"a\"), EntityName(\"b\"), bb(\"container1\")).containerInitializer() shouldBe {\n      JsObject(\"name\" -> \"b\".toJson, \"binary\" -> false.toJson, \"main\" -> \"main\".toJson)\n    }\n    ExecutableWhiskAction(EntityPath(\"a\"), EntityName(\"b\"), bb(\"container1\", \"xyz\")).containerInitializer() shouldBe {\n      JsObject(\"name\" -> \"b\".toJson, \"binary\" -> false.toJson, \"main\" -> \"main\".toJson, \"code\" -> \"xyz\".toJson)\n    }\n    ExecutableWhiskAction(EntityPath(\"a\"), EntityName(\"b\"), bb(\"container1\", \"\", Some(\"naim\")))\n      .containerInitializer() shouldBe {\n      JsObject(\"name\" -> \"b\".toJson, \"binary\" -> false.toJson, \"main\" -> \"naim\".toJson)\n    }\n  }\n\n  it should \"allow of main override in action initializer\" in {\n    ExecutableWhiskAction(EntityPath(\"a\"), EntityName(\"b\"), jsDefault(\"\")).containerInitializer() shouldBe {\n      JsObject(\"name\" -> \"b\".toJson, \"binary\" -> false.toJson, \"code\" -> JsString.empty, \"main\" -> \"main\".toJson)\n    }\n\n    ExecutableWhiskAction(EntityPath(\"a\"), EntityName(\"b\"), jsDefault(\"\", Some(\"bar\")))\n      .containerInitializer() shouldBe {\n      JsObject(\"name\" -> \"b\".toJson, \"binary\" -> false.toJson, \"code\" -> JsString.empty, \"main\" -> \"bar\".toJson)\n    }\n  }\n\n  it should \"include optional environment variables\" in {\n    val env = Map(\n      \"A\" -> \"c\".toJson,\n      \"B\" -> JsNull,\n      \"C\" -> JsTrue,\n      \"D\" -> JsNumber(3),\n      \"E\" -> JsArray(JsString(\"a\")),\n      \"F\" -> JsObject(\"a\" -> JsFalse))\n\n    ExecutableWhiskAction(EntityPath(\"a\"), EntityName(\"b\"), bb(\"container1\")).containerInitializer(env) shouldBe {\n      JsObject(\n        \"name\" -> \"b\".toJson,\n        \"binary\" -> false.toJson,\n        \"main\" -> \"main\".toJson,\n        \"env\" -> JsObject(\n          \"A\" -> JsString(\"c\"),\n          \"B\" -> JsString(\"\"),\n          \"C\" -> JsString(\"true\"),\n          \"D\" -> JsString(\"3\"),\n          \"E\" -> JsString(\"[\\\"a\\\"]\"),\n          \"F\" -> JsString(\"{\\\"a\\\":false}\")))\n    }\n  }\n\n  it should \"compare as equal two actions even if their revision does not match\" in {\n    val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n    val actionA = WhiskAction(EntityPath(\"actionSpace\"), EntityName(\"actionName\"), exec)\n    val actionB = actionA.copy()\n    val actionC = actionA.copy()\n    actionC.revision(DocRevision(\"2\"))\n    actionA shouldBe actionB\n    actionA shouldBe actionC\n  }\n\n  it should \"compare as equal two executable actions even if their revision does not match\" in {\n    val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n    val actionA = ExecutableWhiskAction(EntityPath(\"actionSpace\"), EntityName(\"actionName\"), exec)\n    val actionB = actionA.copy()\n    val actionC = actionA.copy()\n    actionC.revision(DocRevision(\"2\"))\n    actionA shouldBe actionB\n    actionA shouldBe actionC\n  }\n\n  it should \"reject malformed JSON\" in {\n    val b64 = Base64.getEncoder()\n    val contents = b64.encodeToString(\"tarball\".getBytes)\n\n    val execs = Seq[JsValue](\n      null,\n      JsObject.empty,\n      JsNull,\n      JsObject(\"init\" -> \"zipfile\".toJson),\n      JsObject(\"kind\" -> \"nodejs:20\".toJson, \"code\" -> JsNumber(42)),\n      JsObject(\"kind\" -> \"nodejs:20\".toJson, \"init\" -> \"zipfile\".toJson),\n      JsObject(\"kind\" -> \"turbopascal\".toJson, \"code\" -> \"BEGIN1\".toJson),\n      JsObject(\"kind\" -> \"blackbox\".toJson, \"code\" -> \"js\".toJson),\n      JsObject(\"kind\" -> \"swift\".toJson, \"swiftcode\" -> \"swift\".toJson))\n\n    execs.foreach { e =>\n      withClue(if (e != null) e else \"null\") {\n        val thrown = intercept[Throwable] {\n          Exec.serdes.read(e)\n        }\n        thrown match {\n          case _: DeserializationException =>\n          case _: IllegalArgumentException =>\n          case t                           => assert(false, \"Unexpected exception:\" + t)\n        }\n      }\n    }\n  }\n\n  it should \"reject null code/image arguments\" in {\n    an[IllegalArgumentException] should be thrownBy Exec.serdes.read(null)\n    a[DeserializationException] should be thrownBy Exec.serdes.read(\"{}\" parseJson)\n    a[DeserializationException] should be thrownBy Exec.serdes.read(JsString.empty)\n  }\n\n  it should \"serialize to json\" in {\n    val execs = Seq(bb(\"container\"), jsDefault(\"js\"), jsDefault(\"js\"), swift(\"swift\")).map { _.asJson }\n    assert(execs(0) == JsObject(\"kind\" -> \"blackbox\".toJson, \"image\" -> \"container\".toJson, \"binary\" -> false.toJson))\n    assert(execs(1) == JsObject(\"kind\" -> \"nodejs:20\".toJson, \"code\" -> \"js\".toJson, \"binary\" -> false.toJson))\n    assert(execs(2) == JsObject(\"kind\" -> \"nodejs:20\".toJson, \"code\" -> \"js\".toJson, \"binary\" -> false.toJson))\n    assert(execs(3) == JsObject(\"kind\" -> \"swift:5.3\".toJson, \"code\" -> \"swift\".toJson, \"binary\" -> false.toJson))\n  }\n\n  behavior of \"Parameter\"\n\n  it should \"properly deserialize and reserialize JSON without optional field\" in {\n    val json = Seq[JsValue](\n      JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> \"v\".toJson)),\n      JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> \"v\".toJson, \"foo\" -> \"bar\".toJson)),\n      JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> 3.toJson)),\n      JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> Vector(false, true).toJson)))\n    val params = json.map { p =>\n      Parameters.serdes.read(p)\n    }\n    assert(params(0) == Parameters(\"k\", \"v\"))\n    assert(params(1) == Parameters(\"k\", \"v\"))\n    assert(params(0).toString == json(0).compactPrint)\n    assert(params(1).toString == json(0).compactPrint) // drops unknown prop \"foo\"\n    assert(params(1).toString != json(1).compactPrint) // drops unknown prop \"foo\"\n    assert(params(2).toString == json(2).compactPrint) // drops unknown prop \"foo\"\n    assert(params(3).toString == json(3).compactPrint) // drops unknown prop \"foo\"\n  }\n\n  it should \"properly deserialize and reserialize parameters with optional field\" in {\n    val json = Seq[JsValue](\n      JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> \"v\".toJson)),\n      JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> \"v\".toJson, \"init\" -> JsFalse)),\n      JsArray(JsObject(Map(\"key\" -> \"k\".toJson, \"value\" -> \"v\".toJson, \"init\" -> JsTrue))))\n\n    val params = json.map { p =>\n      Parameters.serdes.read(p)\n    }\n    assert(params(0) == Parameters(\"k\", \"v\"))\n    assert(params(1) == Parameters(\"k\", \"v\", false))\n    assert(params(2) == Parameters(\"k\", \"v\", true))\n    assert(params(0).toString == json(0).compactPrint)\n    assert(params(1).toString == json(0).compactPrint) // init == false drops the property from the JSON\n    assert(params(2).toString == json(2).compactPrint)\n  }\n\n  it should \"reject parameters with invalid optional field\" in {\n    val json = Seq[JsValue](JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> \"v\".toJson, \"init\" -> JsString(\"true\"))))\n\n    json.foreach { p =>\n      an[DeserializationException] should be thrownBy Parameters.serdes.read(p)\n    }\n  }\n\n  it should \"filter immutable parameters\" in {\n    val params = Parameters(\"k\", \"v\") ++ Parameters(\"ns\", null: String) ++ Parameters(\"njs\", JsNull)\n    params.definedParameters shouldBe Set(\"k\")\n  }\n\n  it should \"reject malformed JSON\" in {\n    val params = Seq[JsValue](\n      null,\n      JsObject.empty,\n      JsObject(\"key\" -> \"k\".toJson),\n      JsObject(\"value\" -> \"v\".toJson),\n      JsObject(\"key\" -> JsNull, \"value\" -> \"v\".toJson),\n      JsObject(\"key\" -> \"k\".toJson, (\"value\" -> JsNull)),\n      JsObject(\"key\" -> JsNull, \"value\" -> JsNull),\n      JsObject(\"KEY\" -> \"k\".toJson, \"VALUE\" -> \"v\".toJson),\n      JsObject(\"key\" -> \"k\".toJson, \"value\" -> 0.toJson))\n\n    params.foreach { p =>\n      a[DeserializationException] should be thrownBy Parameters.serdes.read(p)\n    }\n  }\n\n  it should \"reject undefined key\" in {\n    a[DeserializationException] should be thrownBy Parameters.serdes.read(null: JsValue)\n    an[IllegalArgumentException] should be thrownBy Parameters(null, null: String)\n    an[IllegalArgumentException] should be thrownBy Parameters(\"\", null: JsValue)\n    an[IllegalArgumentException] should be thrownBy Parameters(\" \", null: String)\n    an[IllegalArgumentException] should be thrownBy Parameters(null, \"\")\n    an[IllegalArgumentException] should be thrownBy Parameters(null, \" \")\n    an[IllegalArgumentException] should be thrownBy Parameters(null)\n  }\n\n  it should \"recognize truthy values\" in {\n    Seq(JsTrue, JsNumber(1), JsString(\"x\")).foreach { v =>\n      Parameters(\"x\", v).isTruthy(\"x\") shouldBe true\n    }\n\n    Seq(JsFalse, JsNumber(0), JsString.empty, JsNull).foreach { v =>\n      Parameters(\"x\", v).isTruthy(\"x\") shouldBe false\n    }\n\n    Parameters(\"x\", JsTrue).isTruthy(\"y\") shouldBe false\n    Parameters(\"x\", JsTrue).isTruthy(\"y\", valueForNonExistent = true) shouldBe true\n  }\n\n  it should \"serialize to json\" in {\n    assert(\n      Parameters(\"k\", null: String).toString == JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> JsNull)).compactPrint)\n    assert(Parameters(\"k\", \"\").toString == JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> \"\".toJson)).compactPrint)\n    assert(Parameters(\"k\", \" \").toString == JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> \"\".toJson)).compactPrint)\n    assert(Parameters(\"k\", \"v\").toString == JsArray(JsObject(\"key\" -> \"k\".toJson, \"value\" -> \"v\".toJson)).compactPrint)\n  }\n\n  behavior of \"ActionLimits\"\n\n  it should \"properly deserialize JSON\" in {\n    val json = Seq[JsValue](\n      JsObject(\n        \"timeout\" -> TimeLimit.STD_DURATION.toMillis.toInt.toJson,\n        \"memory\" -> MemoryLimit.STD_MEMORY.toMB.toInt.toJson,\n        \"logs\" -> LogLimit.STD_LOGSIZE.toMB.toInt.toJson,\n        \"concurrency\" -> IntraConcurrencyLimit.STD_CONCURRENT.toInt.toJson),\n      JsObject(\n        \"timeout\" -> TimeLimit.STD_DURATION.toMillis.toInt.toJson,\n        \"memory\" -> MemoryLimit.STD_MEMORY.toMB.toInt.toJson,\n        \"logs\" -> LogLimit.STD_LOGSIZE.toMB.toInt.toJson,\n        \"concurrency\" -> IntraConcurrencyLimit.STD_CONCURRENT.toInt.toJson,\n        \"foo\" -> \"bar\".toJson),\n      JsObject(\n        \"timeout\" -> TimeLimit.STD_DURATION.toMillis.toInt.toJson,\n        \"memory\" -> MemoryLimit.STD_MEMORY.toMB.toInt.toJson))\n    val limits = json.map(ActionLimits.serdes.read)\n    assert(limits(0) == ActionLimits())\n    assert(limits(1) == ActionLimits())\n    assert(limits(2) == ActionLimits())\n    assert(limits(0).toJson == json(0))\n    assert(limits(1).toJson == json(0)) // drops unknown prop \"foo\"\n    assert(limits(1).toJson != json(1)) // drops unknown prop \"foo\"\n  }\n\n  it should \"reject malformed JSON\" in {\n    val limits = Seq[JsValue](\n      null,\n      JsObject.empty,\n      JsNull,\n      JsObject(\"timeout\" -> TimeLimit.STD_DURATION.toMillis.toInt.toJson),\n      JsObject(\"memory\" -> MemoryLimit.STD_MEMORY.toMB.toInt.toJson),\n      JsObject(\"logs\" -> (LogLimit.STD_LOGSIZE.toMB.toInt + 1).toJson),\n      JsObject(\n        \"TIMEOUT\" -> TimeLimit.STD_DURATION.toMillis.toInt.toJson,\n        \"MEMORY\" -> MemoryLimit.STD_MEMORY.toMB.toInt.toJson),\n      JsObject(\n        \"timeout\" -> (TimeLimit.STD_DURATION.toMillis.toDouble + .01).toJson,\n        \"memory\" -> (MemoryLimit.STD_MEMORY.toMB.toDouble + .01).toJson),\n      JsObject(\"timeout\" -> null, \"memory\" -> null),\n      JsObject(\"timeout\" -> JsNull, \"memory\" -> JsNull),\n      JsObject(\n        \"timeout\" -> TimeLimit.STD_DURATION.toMillis.toString.toJson,\n        \"memory\" -> MemoryLimit.STD_MEMORY.toMB.toInt.toString.toJson))\n\n    limits.foreach { p =>\n      a[DeserializationException] should be thrownBy ActionLimits.serdes.read(p)\n    }\n  }\n\n  it should \"pass the correct error message through\" in {\n    val serdes = Seq(TimeLimit.serdes, MemoryLimit.serdes, LogLimit.serdes)\n\n    serdes foreach { s =>\n      withClue(s\"serializer $s\") {\n        if (s == LogLimit.serdes) {\n          val lb = the[DeserializationException] thrownBy s.read(JsNumber(-1))\n          lb.getMessage should include(\"a negative size of an object is not allowed\")\n        }\n        val int = the[DeserializationException] thrownBy s.read(JsNumber(2.5))\n        int.getMessage should include(\"limit must be whole number\")\n      }\n    }\n  }\n\n  it should \"parse activation id as uuid\" in {\n    val id = \"213174381920559471141441e1111111\"\n    val aid = ActivationId.parse(id)\n    assert(aid.isSuccess)\n    assert(aid.get.toString == id)\n  }\n\n  it should \"parse activation id as uuid when made up of no numbers\" in {\n    val id = \"a\" * 32\n    val aid = ActivationId.parse(id)\n    assert(aid.isSuccess)\n    assert(aid.get.toString == id)\n  }\n\n  it should \"parse activation id as uuid when made up of no letters\" in {\n    val id = \"1\" * 32\n    val aid = ActivationId.parse(id)\n    assert(aid.isSuccess)\n    assert(aid.get.toString == id)\n  }\n\n  it should \"parse an activation id as uuid when it is a number\" in {\n    val id = \"1\" * 32\n    val aid = Try { ActivationId.serdes.read(BigInt(id).toJson) }\n    assert(aid.isSuccess)\n    assert(aid.get.toString == id)\n  }\n\n  it should \"not parse invalid activation id\" in {\n    val id = \"213174381920559471141441e111111z\"\n    assert(ActivationId.parse(id).isFailure)\n    Try(ActivationId.serdes.read(JsString(id))) shouldBe Failure {\n      DeserializationException(Messages.activationIdIllegal)\n    }\n  }\n\n  it should \"not parse activation id if longer than uuid\" in {\n    val id = \"213174381920559471141441e1111111abc\"\n    assert(ActivationId.parse(id).isFailure)\n    Try(ActivationId.serdes.read(JsString(id))) shouldBe Failure {\n      DeserializationException(Messages.activationIdLengthError(SizeError(\"Activation id\", id.length.B, 32.B)))\n    }\n  }\n\n  it should \"not parse activation id if shorter than uuid\" in {\n    val id = \"213174381920559471141441e1\"\n    ActivationId.parse(id) shouldBe 'failure\n    Try(ActivationId.serdes.read(JsString(id))) shouldBe Failure {\n      DeserializationException(Messages.activationIdLengthError(SizeError(\"Activation id\", id.length.B, 32.B)))\n    }\n  }\n\n  behavior of \"Js Helpers\"\n\n  it should \"project paths from json object\" in {\n    val js = JsObject(\"a\" -> JsObject(\"b\" -> JsObject(\"c\" -> JsString(\"v\"))), \"b\" -> JsString(\"v\"))\n    JsHelpers.fieldPathExists(js) shouldBe true\n    JsHelpers.fieldPathExists(js, \"a\") shouldBe true\n    JsHelpers.fieldPathExists(js, \"a\", \"b\") shouldBe true\n    JsHelpers.fieldPathExists(js, \"a\", \"b\", \"c\") shouldBe true\n    JsHelpers.fieldPathExists(js, \"a\", \"b\", \"c\", \"d\") shouldBe false\n    JsHelpers.fieldPathExists(js, \"b\") shouldBe true\n    JsHelpers.fieldPathExists(js, \"c\") shouldBe false\n\n    JsHelpers.getFieldPath(js) shouldBe Some(js)\n    JsHelpers.getFieldPath(js, \"x\") shouldBe None\n    JsHelpers.getFieldPath(js, \"b\") shouldBe Some(JsString(\"v\"))\n    JsHelpers.getFieldPath(js, \"a\") shouldBe Some(JsObject(\"b\" -> JsObject(\"c\" -> JsString(\"v\"))))\n    JsHelpers.getFieldPath(js, \"a\", \"b\") shouldBe Some(JsObject(\"c\" -> JsString(\"v\")))\n    JsHelpers.getFieldPath(js, \"a\", \"b\", \"c\") shouldBe Some(JsString(\"v\"))\n    JsHelpers.getFieldPath(js, \"a\", \"b\", \"c\", \"d\") shouldBe None\n    JsHelpers.getFieldPath(JsObject.empty) shouldBe Some(JsObject.empty)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/SizeTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport scala.language.postfixOps\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json._\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.apache.openwhisk.core.entity.ByteSize\n\n@RunWith(classOf[JUnitRunner])\nclass SizeTests extends AnyFlatSpec with Matchers {\n\n  behavior of \"Size Entity\"\n\n  // Comparing\n  it should \"1 Byte smaller than 1 KB smaller than 1 MB\" in {\n    val oneByte = 1 B\n    val oneKB = 1 KB\n    val oneMB = 1 MB\n\n    oneByte should be < oneKB\n    oneKB should be < oneMB\n  }\n\n  it should \"3 Bytes smaller than 2 KB smaller than 1 MB\" in {\n    val myBytes = 3 B\n    val myKBs = 2 KB\n    val myMBs = 1 MB\n\n    myBytes should be < myKBs\n    myKBs should be < myMBs\n  }\n\n  it should \"1 MB greater than 1 KB greater than 1 Byte\" in {\n    val oneByte = 1 B\n    val oneKB = 1 KB\n    val oneMB = 1 MB\n\n    oneMB should be > oneKB\n    oneKB should be > oneByte\n  }\n\n  it should \"1 MB == 1024 KB == 1048576 B\" in {\n    val myBytes = (1 << 20) B\n    val myKBs = 1024 KB\n    val myMBs = 1 MB\n\n    myBytes should equal(myKBs)\n    myKBs should equal(myMBs)\n  }\n\n  // Addition\n  it should \"1 Byte + 1 KB = 1025 Bytes\" in {\n    1.B + 1.KB should be(1025 B)\n  }\n\n  it should \"1 MB + 1 MB = 2 MB\" in {\n    (1.MB + 1.MB).toBytes should be(2.MB.toBytes)\n  }\n\n  // Subtraction\n  it should \"1 KB - 1B = 1023 Bytes\" in {\n    1.KB - 1.B should be(1023 B)\n  }\n\n  it should \"1 MB - 1 MB = 0 MB\" in {\n    1.MB - 1.MB should be(0 B)\n  }\n\n  it should \"throw an exception if subtraction leads to a negative size\" in {\n    an[IllegalArgumentException] should be thrownBy {\n      0.B - 1.B\n    }\n  }\n\n  // Multiplication\n  it should \"2 B * 10 = 20 B\" in {\n    2.B * 10 should be(20.B)\n  }\n\n  it should \"40 MB * 2 = 80 MB\" in {\n    40.MB * 2 should be(80.MB)\n  }\n\n  // Division\n  it should \"5 Byte / 2 Byte = 2.5\" in {\n    5.B / 2.B should be(2.5)\n  }\n\n  it should \"1 KB / 512 Byte = 2\" in {\n    1.KB / 512.B should be(2)\n  }\n\n  it should \"throw an exception if division is through 0 byte\" in {\n    an[ArithmeticException] should be thrownBy {\n      1.MB / 0.B\n    }\n  }\n\n  it should \"5 Byte / 2 = 2 Byte\" in {\n    5.B / 2 should be(2.B)\n  }\n\n  it should \"1 MB / 512 = 2 Byte\" in {\n    1.MB / 512 should be(2.KB)\n  }\n\n  it should \"not go into integer overflow for a few GB\" in {\n    4096.MB / 2 should be(2048.MB)\n  }\n\n  it should \"throw an exception if division is through 0\" in {\n    an[ArithmeticException] should be thrownBy {\n      1.MB / 0\n    }\n  }\n\n  // Conversions\n  it should \"1024 B to KB = 1\" in {\n    (1024 B).toKB should be(1)\n  }\n\n  it should \"1048576 B to MB = 1\" in {\n    ((1 << 20) B).toMB should be(1)\n  }\n\n  it should \"1 KB to B = 1024\" in {\n    (1 KB).toBytes should be(1024)\n  }\n\n  it should \"1024 KB to MB = 1\" in {\n    (1024 KB).toMB should be(1)\n  }\n\n  it should \"1 MB to B = 1048576\" in {\n    (1 MB).toBytes should be(1 << 20)\n  }\n\n  it should \"1 MB to KB = 1024\" in {\n    (1 MB).toKB should be(1024)\n  }\n\n  // Create ObjectSize from String\n  it should \"create ObjectSize from String 3B\" in {\n    ByteSize.fromString(\"3b\") should be(3 B)\n    ByteSize.fromString(\"3B\") should be(3 B)\n    ByteSize.fromString(\"3 b\") should be(3 B)\n    ByteSize.fromString(\"3 B\") should be(3 B)\n  }\n\n  it should \"create ObjectSize from String 7K\" in {\n    ByteSize.fromString(\"7k\") should be(7 KB)\n    ByteSize.fromString(\"7K\") should be(7 KB)\n    ByteSize.fromString(\"7KB\") should be(7 KB)\n    ByteSize.fromString(\"7kB\") should be(7 KB)\n    ByteSize.fromString(\"7kb\") should be(7 KB)\n    ByteSize.fromString(\"7 k\") should be(7 KB)\n    ByteSize.fromString(\"7 K\") should be(7 KB)\n    ByteSize.fromString(\"7 KB\") should be(7 KB)\n    ByteSize.fromString(\"7 kB\") should be(7 KB)\n    ByteSize.fromString(\"7 kb\") should be(7 KB)\n  }\n\n  it should \"create ObjectSize from String 120M\" in {\n    ByteSize.fromString(\"120m\") should be(120 MB)\n    ByteSize.fromString(\"120M\") should be(120 MB)\n    ByteSize.fromString(\"120MB\") should be(120 MB)\n    ByteSize.fromString(\"120mB\") should be(120 MB)\n    ByteSize.fromString(\"120mb\") should be(120 MB)\n    ByteSize.fromString(\"120 m\") should be(120 MB)\n    ByteSize.fromString(\"120 M\") should be(120 MB)\n    ByteSize.fromString(\"120 MB\") should be(120 MB)\n    ByteSize.fromString(\"120 mB\") should be(120 MB)\n    ByteSize.fromString(\"120 mb\") should be(120 MB)\n  }\n\n  it should \"read and write size as JSON\" in {\n    import org.apache.openwhisk.core.entity.size.serdes\n    serdes.read(JsString(\"3b\")) should be(3 B)\n    serdes.write(3 B) should be(JsString(\"3 B\"))\n    a[DeserializationException] should be thrownBy (serdes.read(JsNumber(3)))\n  }\n\n  it should \"throw error on creating ObjectSize from String 120A\" in {\n    the[IllegalArgumentException] thrownBy {\n      ByteSize.fromString(\"120A\")\n    } should have message ByteSize.formatError\n  }\n\n  it should \"throw error on creating ByteSize object with negative size\" in {\n    the[IllegalArgumentException] thrownBy {\n      -130 MB\n    } should have message \"requirement failed: a negative size of an object is not allowed.\"\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/ViewTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport java.time.Clock\nimport java.time.Instant\n\nimport scala.concurrent.Await\nimport scala.language.postfixOps\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport common.StreamLogging\nimport common.WskActorSystem\nimport org.apache.openwhisk.core.controller.test.WhiskAuthHelpers\nimport org.apache.openwhisk.core.database.ArtifactStore\nimport org.apache.openwhisk.core.database.StaleParameter\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entity._\n\n@RunWith(classOf[JUnitRunner])\nclass ViewTests\n    extends AnyFlatSpec\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with Matchers\n    with DbUtils\n    with ExecHelpers\n    with WskActorSystem\n    with StreamLogging {\n\n  def aname() = MakeName.next(\"viewtests\")\n  def afullname(namespace: EntityPath) = FullyQualifiedEntityName(namespace, aname())\n\n  object MakeName {\n    @volatile var counter = 1\n    def next(prefix: String = \"test\")(): EntityName = {\n      counter = counter + 1\n      EntityName(s\"${prefix}_name$counter\")\n    }\n  }\n\n  val creds1 = WhiskAuthHelpers.newAuth(Subject(\"s12345\"))\n  val namespace1 = EntityPath(creds1.subject.asString)\n\n  val creds2 = WhiskAuthHelpers.newAuth(Subject(\"t12345\"))\n  val namespace2 = EntityPath(creds2.subject.asString)\n\n  val entityStore = WhiskEntityStore.datastore()\n  val activationStore = WhiskActivationStore.datastore()\n\n  override def afterEach = {\n    cleanup()\n  }\n\n  override def afterAll() = {\n    println(\"Shutting down store connections\")\n    entityStore.shutdown()\n    activationStore.shutdown()\n    super.afterAll()\n  }\n\n  behavior of \"Datastore View\"\n\n  def getAllActivationsInNamespace[Au <: WhiskEntity](store: ArtifactStore[Au], ns: EntityPath)(\n    implicit entities: Seq[WhiskEntity]) = {\n    implicit val tid = transid()\n    val result =\n      Await\n        .result(WhiskActivation.listCollectionInNamespace(store, ns, 0, 0, stale = StaleParameter.No), dbOpTimeout)\n        .left\n        .get\n        .map(e => e)\n    val expected = entities filter { _.namespace.root.toPath == ns }\n    result should have length expected.length\n    result should contain theSameElementsAs expected.map(_.summaryAsJson)\n  }\n\n  def resolveListMethodForKind(kind: String) = kind match {\n    case \"actions\"     => WhiskAction\n    case \"packages\"    => WhiskPackage\n    case \"rules\"       => WhiskRule\n    case \"triggers\"    => WhiskTrigger\n    case \"activations\" => WhiskActivation\n  }\n\n  def getKindInNamespace[Au <: WhiskEntity](store: ArtifactStore[Au],\n                                            ns: EntityPath,\n                                            kind: String,\n                                            f: (WhiskEntity) => Boolean)(implicit entities: Seq[WhiskEntity]) = {\n    implicit val tid = transid()\n    val q = resolveListMethodForKind(kind)\n    val result =\n      Await.result(q.listCollectionInNamespace(store, ns, 0, 0, stale = StaleParameter.No).map(_.left.get), dbOpTimeout)\n    val expected = entities filter { e =>\n      f(e) && e.namespace.root.toPath == ns\n    }\n    result should have length expected.length\n    result should contain theSameElementsAs expected.map(_.summaryAsJson)\n  }\n\n  def getKindInNamespaceWithDoc[T](ns: EntityPath, kind: String, f: (WhiskEntity) => Boolean)(\n    implicit entities: Seq[WhiskEntity]) = {\n    implicit val tid = transid()\n    val q = resolveListMethodForKind(kind)\n    val result =\n      Await.result(\n        q.listCollectionInNamespace(entityStore, ns, 0, 0, includeDocs = true, stale = StaleParameter.No)\n          .map(_.right.get),\n        dbOpTimeout)\n    val expected = entities filter { e =>\n      f(e) && e.namespace.root.toPath == ns\n    }\n    result should have length expected.length\n    result should contain theSameElementsAs expected\n  }\n\n  def getKindInNamespaceByName[Au <: WhiskEntity](store: ArtifactStore[Au],\n                                                  ns: EntityPath,\n                                                  kind: String,\n                                                  name: EntityName,\n                                                  f: (WhiskEntity) => Boolean)(implicit entities: Seq[WhiskEntity]) = {\n    require(kind == \"actions\") // currently only actions are permitted in packages\n    implicit val tid = transid()\n    val q = resolveListMethodForKind(kind)\n    val result =\n      Await.result(\n        q.listCollectionInNamespace(store, ns.addPath(name), 0, 0, stale = StaleParameter.No).map(_.left.get),\n        dbOpTimeout)\n    val expected = entities filter { e =>\n      f(e) && e.namespace.root.toPath == ns\n    }\n    result should have length expected.length\n    result should contain theSameElementsAs expected.map(_.summaryAsJson)\n  }\n\n  def getActivationsInNamespaceByName(store: ArtifactStore[WhiskActivation],\n                                      ns: EntityPath,\n                                      name: EntityName,\n                                      f: (WhiskEntity) => Boolean)(implicit entities: Seq[WhiskEntity]) = {\n    implicit val tid = transid()\n    val result =\n      Await.result(\n        WhiskActivation\n          .listActivationsMatchingName(store, ns, name.toPath, 0, 0, stale = StaleParameter.No)\n          .map(_.left.get),\n        dbOpTimeout)\n    val expected = entities filter { e =>\n      f(e) && e.namespace.root.toPath == ns\n    }\n    result should have length expected.length\n    result should contain theSameElementsAs expected.map(_.summaryAsJson)\n  }\n\n  def getKindInPackage(ns: EntityPath, kind: String, f: (WhiskEntity) => Boolean)(\n    implicit entities: Seq[WhiskEntity]) = {\n    implicit val tid = transid()\n    val q = resolveListMethodForKind(kind)\n    val result = Await.result(\n      q.listCollectionInNamespace(entityStore, ns, 0, 0, stale = StaleParameter.No).map(_.left.get),\n      dbOpTimeout)\n    val expected = entities filter { e =>\n      f(e) && e.namespace == ns\n    }\n    result should have length expected.length\n    result should contain theSameElementsAs expected.map(_.summaryAsJson)\n  }\n\n  def getActivationsInNamespaceByNameSortedByDate(store: ArtifactStore[WhiskActivation],\n                                                  ns: EntityPath,\n                                                  kind: String,\n                                                  name: EntityName,\n                                                  skip: Int,\n                                                  count: Int,\n                                                  start: Option[Instant],\n                                                  end: Option[Instant],\n                                                  f: (WhiskEntity) => Boolean)(implicit entities: Seq[WhiskEntity]) = {\n    implicit val tid = transid()\n    val result = Await.result(\n      WhiskActivation\n        .listActivationsMatchingName(store, ns, name.toPath, skip, count, false, start, end, StaleParameter.No)\n        .map(_.left.get),\n      dbOpTimeout)\n    val expected = entities filter { e =>\n      f(e) && e.namespace.root.toPath == ns\n    } sortBy { case (e: WhiskActivation) => e.start.toEpochMilli; case _ => 0 } map { _.summaryAsJson }\n    result should have length expected.length\n    result should be(expected reverse)\n  }\n\n  it should \"query whisk view by namespace, collection and entity name in whisk actions db\" in {\n    implicit val tid = transid()\n    val exec = bb(\"image\")\n    val pkgname1 = namespace1.addPath(aname())\n    val pkgname2 = namespace2.addPath(aname())\n    val actionName = aname()\n    def now = Instant.now(Clock.systemUTC())\n\n    implicit val entities = Seq(\n      WhiskAction(namespace1, aname(), exec),\n      WhiskAction(namespace1, aname(), exec),\n      WhiskAction(namespace1.addPath(aname()), aname(), exec),\n      WhiskAction(namespace1.addPath(aname()), aname(), exec),\n      WhiskAction(pkgname1, aname(), exec),\n      WhiskAction(pkgname1, aname(), exec),\n      WhiskAction(pkgname1, actionName, exec),\n      WhiskTrigger(namespace1, aname()),\n      WhiskTrigger(namespace1, aname()),\n      WhiskRule(namespace1, aname(), trigger = afullname(namespace1), action = afullname(namespace1)),\n      WhiskRule(namespace1, aname(), trigger = afullname(namespace1), action = afullname(namespace1)),\n      WhiskPackage(namespace1, aname()),\n      WhiskPackage(namespace1, aname()),\n      WhiskPackage(namespace1, aname(), Some(Binding(namespace2.root, aname()))),\n      WhiskPackage(namespace1, aname(), Some(Binding(namespace2.root, aname()))),\n      WhiskAction(namespace2, aname(), exec),\n      WhiskAction(namespace2, aname(), exec),\n      WhiskAction(namespace2.addPath(aname()), aname(), exec),\n      WhiskAction(namespace2.addPath(aname()), aname(), exec),\n      WhiskAction(pkgname2, aname(), exec),\n      WhiskAction(pkgname2, aname(), exec),\n      WhiskTrigger(namespace2, aname()),\n      WhiskTrigger(namespace2, aname()),\n      WhiskRule(namespace2, aname(), trigger = afullname(namespace2), action = afullname(namespace2)),\n      WhiskRule(namespace2, aname(), trigger = afullname(namespace2), action = afullname(namespace2)),\n      WhiskPackage(namespace2, aname()),\n      WhiskPackage(namespace2, aname()),\n      WhiskPackage(namespace2, aname(), Some(Binding(namespace1.root, aname()))),\n      WhiskPackage(namespace2, aname(), Some(Binding(namespace1.root, aname()))))\n\n    entities foreach { put(entityStore, _) }\n    waitOnView(entityStore, namespace1.root, 7, WhiskAction.view)\n    waitOnView(entityStore, namespace1.root, 2, WhiskTrigger.view)\n    waitOnView(entityStore, namespace1.root, 2, WhiskRule.view)\n    waitOnView(entityStore, namespace1.root, 4, WhiskPackage.view)\n    waitOnView(entityStore, namespace2.root, 6, WhiskAction.view)\n    waitOnView(entityStore, namespace2.root, 2, WhiskTrigger.view)\n    waitOnView(entityStore, namespace2.root, 2, WhiskRule.view)\n    waitOnView(entityStore, namespace2.root, 4, WhiskPackage.view)\n\n    getKindInNamespace(entityStore, namespace1, \"actions\", {\n      case (e: WhiskAction) => true\n      case (_)              => false\n    })\n    getKindInNamespace(entityStore, namespace1, \"triggers\", {\n      case (e: WhiskTrigger) => true\n      case (_)               => false\n    })\n    getKindInNamespaceWithDoc[WhiskRule](namespace1, \"rules\", {\n      case (e: WhiskRule) => true\n      case (_)            => false\n    })\n    getKindInNamespace(entityStore, namespace1, \"packages\", {\n      case (e: WhiskPackage) => true\n      case (_)               => false\n    })\n    getKindInPackage(pkgname1, \"actions\", {\n      case (e: WhiskAction) => true\n      case (_)              => false\n    })\n    getKindInNamespaceByName(entityStore, pkgname1, \"actions\", actionName, {\n      case (e: WhiskAction) => (e.name == actionName)\n      case (_)              => false\n    })\n\n    getKindInNamespace(entityStore, namespace2, \"actions\", {\n      case (e: WhiskAction) => true\n      case (_)              => false\n    })\n    getKindInNamespace(entityStore, namespace2, \"triggers\", {\n      case (e: WhiskTrigger) => true\n      case (_)               => false\n    })\n    getKindInNamespaceWithDoc[WhiskRule](namespace2, \"rules\", {\n      case (e: WhiskRule) => true\n      case (_)            => false\n    })\n    getKindInNamespace(entityStore, namespace2, \"packages\", {\n      case (e: WhiskPackage) => true\n      case (_)               => false\n    })\n    getKindInPackage(pkgname2, \"actions\", {\n      case (e: WhiskAction) => true\n      case (_)              => false\n    })\n  }\n\n  it should \"query whisk view by namespace, collection and entity name in whisk activations db\" in {\n    implicit val tid = transid()\n    val actionName = aname()\n    def now = Instant.now(Clock.systemUTC())\n\n    // creates 5 entities in each namespace as follows:\n    // - some activations in each namespace (some may have prescribed action name to query by name)\n    implicit val entities = Seq(\n      WhiskActivation(namespace1, aname(), Subject(), ActivationId.generate(), start = now, end = now),\n      WhiskActivation(namespace1, aname(), Subject(), ActivationId.generate(), start = now, end = now),\n      WhiskActivation(namespace2, aname(), Subject(), ActivationId.generate(), start = now, end = now),\n      WhiskActivation(namespace2, actionName, Subject(), ActivationId.generate(), start = now, end = now),\n      WhiskActivation(namespace2, actionName, Subject(), ActivationId.generate(), start = now, end = now))\n\n    entities foreach { put(activationStore, _) }\n    waitOnView(activationStore, namespace1.root, 2, WhiskActivation.view)\n    waitOnView(activationStore, namespace2.root, 3, WhiskActivation.view)\n\n    getAllActivationsInNamespace(activationStore, namespace1)\n    getKindInNamespace(activationStore, namespace1, \"activations\", {\n      case (e: WhiskActivation) => true\n      case (_)                  => false\n    })\n\n    getAllActivationsInNamespace(activationStore, namespace2)\n    getKindInNamespace(activationStore, namespace2, \"activations\", {\n      case (e: WhiskActivation) => true\n      case (_)                  => false\n    })\n    getActivationsInNamespaceByName(activationStore, namespace2, actionName, {\n      case (e: WhiskActivation) => (e.name == actionName)\n      case (_)                  => false\n    })\n  }\n\n  it should \"query whisk view for activations sorted by date\" in {\n    implicit val tid = transid()\n    val actionName = aname()\n    val now = Instant.now(Clock.systemUTC())\n    implicit val entities = Seq(\n      WhiskActivation(namespace1, actionName, Subject(), ActivationId.generate(), start = now, end = now),\n      WhiskActivation(\n        namespace1,\n        actionName,\n        Subject(),\n        ActivationId.generate(),\n        start = now.plusSeconds(20),\n        end = now.plusSeconds(20)),\n      WhiskActivation(\n        namespace1,\n        actionName,\n        Subject(),\n        ActivationId.generate(),\n        start = now.plusSeconds(10),\n        end = now.plusSeconds(20)),\n      WhiskActivation(\n        namespace1,\n        actionName,\n        Subject(),\n        ActivationId.generate(),\n        start = now.plusSeconds(40),\n        end = now.plusSeconds(20)),\n      WhiskActivation(\n        namespace1,\n        actionName,\n        Subject(),\n        ActivationId.generate(),\n        start = now.plusSeconds(30),\n        end = now.plusSeconds(20)))\n\n    entities foreach { put(activationStore, _) }\n    waitOnView(activationStore, namespace1.addPath(actionName), entities.length, WhiskActivation.filtersView)\n\n    getActivationsInNamespaceByNameSortedByDate(\n      activationStore,\n      namespace1,\n      \"activations\",\n      actionName,\n      0,\n      5,\n      None,\n      None, {\n        case (e: WhiskActivation) => e.name == actionName\n        case (_)                  => false\n      })\n\n    val since = entities(1).start\n    val upto = entities(4).start\n    getActivationsInNamespaceByNameSortedByDate(\n      activationStore,\n      namespace1,\n      \"activations\",\n      actionName,\n      0,\n      5,\n      Some(since),\n      Some(upto), {\n        case (e: WhiskActivation) =>\n          e.name == actionName && (e.start.equals(since) || e.start\n            .equals(upto) || (e.start.isAfter(since) && e.start.isBefore(upto)))\n        case (_) => false\n      })\n  }\n\n  it should \"list actions and retrieve full documents\" in {\n    implicit val tid = transid()\n    val actionName = aname()\n    val now = Instant.now(Clock.systemUTC())\n    implicit val entities =\n      Seq(WhiskAction(namespace1, aname(), jsDefault(\"??\")), WhiskAction(namespace1, aname(), jsDefault(\"??\")))\n\n    entities foreach { put(entityStore, _) }\n    waitOnView(entityStore, namespace1.root, 2, WhiskAction.view)\n\n    getKindInNamespaceWithDoc[WhiskAction](namespace1, \"actions\", {\n      case (e: WhiskAction) => true\n      case (_)              => false\n    })\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/entity/test/WhiskEntityTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.entity.test\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.openwhisk.core.entity.EntityPath\nimport org.apache.openwhisk.core.entity.EntityName\nimport org.apache.openwhisk.core.entity.WhiskAction\nimport org.apache.openwhisk.core.entity.DocRevision\nimport org.apache.openwhisk.core.entity.Parameters\nimport org.apache.openwhisk.core.entity.FullyQualifiedEntityName\nimport org.apache.openwhisk.core.entity.SequenceExec\nimport org.apache.openwhisk.core.entity.WhiskPackage\nimport org.apache.openwhisk.core.entity.WhiskActivation\nimport org.apache.openwhisk.core.entity.Subject\nimport org.apache.openwhisk.core.entity.ActivationId\nimport org.apache.openwhisk.core.entity.WhiskDocumentReader\nimport java.time.Instant\n\nimport spray.json._\nimport org.apache.openwhisk.core.entity.ActivationLogs\nimport org.apache.openwhisk.core.entity.WhiskTrigger\nimport org.apache.openwhisk.core.entity.ReducedRule\nimport org.apache.openwhisk.core.entity.Status\nimport org.apache.openwhisk.core.entity.WhiskEntity\nimport org.apache.openwhisk.core.entity.WhiskRule\nimport org.apache.openwhisk.core.database.DocumentTypeMismatchException\n\n@RunWith(classOf[JUnitRunner])\nclass WhiskEntityTests extends AnyFlatSpec with ExecHelpers with Matchers {\n\n  val namespace = EntityPath(\"testspace\")\n  val name = EntityName(\"testname\")\n  val revision = DocRevision(\"test\")\n\n  behavior of \"WhiskAction\"\n\n  it should \"correctly inherit parameters and preserve revision through the process\" in {\n    def withParameters(p: Parameters) =\n      WhiskAction(namespace, name, jsDefault(\"js1\"), parameters = p).revision[WhiskAction](revision)\n\n    val toInherit = Parameters(\"testParam\", \"testValue\")\n    Seq(Parameters(), Parameters(\"testParam2\", \"testValue\"), Parameters(\"testParam\", \"testValue2\")).foreach { params =>\n      val action = withParameters(params)\n      val inherited = action.inherit(toInherit)\n      inherited shouldBe action.copy(parameters = toInherit ++ action.parameters)\n      inherited.rev shouldBe action.rev\n    }\n  }\n\n  it should \"correctly resolve default namespace and preserve its revision through the process\" in {\n    val user = \"testuser\"\n    val sequenceAction = FullyQualifiedEntityName(EntityPath(\"_\"), EntityName(\"testaction\"))\n    val action = WhiskAction(namespace, name, sequence(Vector(sequenceAction))).revision[WhiskAction](revision)\n\n    val resolved = action.resolve(EntityName(user))\n    resolved.exec.asInstanceOf[SequenceExec].components.head shouldBe sequenceAction.copy(path = EntityPath(user))\n    action.rev shouldBe resolved.rev\n  }\n\n  behavior of \"WhiskPackage\"\n\n  it should \"correctly inherit parameters and preserve revision through the process\" in {\n    def withParameters(p: Parameters) = WhiskPackage(namespace, name, parameters = p).revision[WhiskPackage](revision)\n\n    val toInherit = Parameters(\"testParam\", \"testValue\")\n    Seq(Parameters(), Parameters(\"testParam2\", \"testValue\"), Parameters(\"testParam\", \"testValue2\")).foreach { params =>\n      val pkg = withParameters(params)\n      val inherited = pkg.inherit(toInherit)\n      inherited shouldBe pkg.copy(parameters = toInherit ++ pkg.parameters)\n      inherited.rev shouldBe pkg.rev\n    }\n  }\n\n  it should \"correctly merge parameters and preserve revision through the process\" in {\n    def withParameters(p: Parameters) = WhiskPackage(namespace, name, parameters = p).revision[WhiskPackage](revision)\n\n    val toOverride = Parameters(\"testParam\", \"testValue\")\n    Seq(Parameters(), Parameters(\"testParam2\", \"testValue\"), Parameters(\"testParam\", \"testValue2\")).foreach { params =>\n      val pkg = withParameters(params)\n      val inherited = pkg.mergeParameters(toOverride)\n      inherited shouldBe pkg.copy(parameters = pkg.parameters ++ toOverride)\n      inherited.rev shouldBe pkg.rev\n    }\n  }\n\n  behavior of \"WhiskActivation\"\n\n  it should \"add and remove logs and preserve revision in the process\" in {\n    val activation = WhiskActivation(namespace, name, Subject(), ActivationId.generate(), Instant.now(), Instant.now())\n      .revision[WhiskActivation](revision)\n    val logs = ActivationLogs(Vector(\"testlog\"))\n\n    val withLogs = activation.withLogs(logs)\n    withLogs shouldBe activation.copy(logs = logs)\n    withLogs.rev shouldBe activation.rev\n\n    val withoutLogs = withLogs.withoutLogs\n    withoutLogs shouldBe activation\n    withoutLogs.rev shouldBe activation.rev\n  }\n\n  behavior of \"WhiskTrigger\"\n\n  it should \"add and remove rules and preserve revision in the process\" in {\n    val fqn = FullyQualifiedEntityName(namespace, name)\n    val trigger = WhiskTrigger(namespace, name).revision[WhiskTrigger](revision)\n    val rule = ReducedRule(fqn, Status.ACTIVE)\n\n    // Add a rule\n    val ruleAdded = trigger.addRule(fqn, rule)\n    ruleAdded.rules shouldBe Some(Map(fqn -> rule))\n    ruleAdded.rev shouldBe trigger.rev\n\n    // Remove the rule\n    val ruleRemoved = ruleAdded.removeRule(fqn)\n    ruleRemoved.rules shouldBe Some(Map.empty[FullyQualifiedEntityName, ReducedRule])\n    ruleRemoved.rev shouldBe trigger.rev\n\n    // Remove all rules\n    val rulesRemoved = ruleAdded.withoutRules\n    rulesRemoved.rules shouldBe None\n    rulesRemoved.rev shouldBe trigger.rev\n  }\n\n  behavior of \"WhiskEntity\"\n\n  it should \"define the entityType property in its json representation\" in {\n\n    val action = WhiskAction(namespace, name, jsDefault(\"code\"), Parameters())\n    assertType(action, \"action\")\n\n    val activation = WhiskActivation(namespace, name, Subject(), ActivationId.generate(), Instant.now(), Instant.now())\n    assertType(activation, \"activation\")\n\n    val whiskPackage = WhiskPackage(namespace, name)\n    assertType(whiskPackage, \"package\")\n\n    val rule =\n      WhiskRule(namespace, name, FullyQualifiedEntityName(namespace, name), FullyQualifiedEntityName(namespace, name))\n    assertType(rule, \"rule\")\n\n    val trigger = WhiskTrigger(namespace, name)\n    assertType(trigger, \"trigger\")\n  }\n\n  behavior of \"WhiskDocumentReader\"\n\n  it should \"check entityType when deserialize\" in {\n    def assertType(d: WhiskEntity, entityType: String) = {\n      d.toDocumentRecord.fields(\"entityType\") shouldBe JsString(entityType)\n    }\n\n    val json =\n      \"\"\"{\n        |\t\"name\": \"action_test\",\n        |\t\"publish\": false,\n        |\t\"annotations\": [],\n        |\t\"version\": \"0.0.2\",\n        |\t\"entityType\": \"action\",\n        |\t\"exec\": {\n        |\t\t\"kind\": \"nodejs:20\",\n        |\t\t\"code\": \"foo\",\n        |\t\t\"binary\": false\n        |\t},\n        |\t\"parameters\": [],\n        |\t\"limits\": {\n        |\t\t\"timeout\": 60000,\n        |\t\t\"memory\": 256\n        |\t},\n        |\t\"namespace\": \"namespace\",\n        |\t\"updated\": 1546268400000\n        |}\"\"\".stripMargin.parseJson\n\n    val action = WhiskDocumentReader.read(manifest[WhiskAction], json)\n    assertType(action.asInstanceOf[WhiskEntity], \"action\")\n    assertThrows[DocumentTypeMismatchException] {\n      WhiskDocumentReader.read(manifest[WhiskTrigger], json)\n    }\n  }\n\n  it should \"deserialize without entityType\" in {\n    val json =\n      \"\"\"{\n        |  \"name\": \"action_test\",\n        |  \"publish\": false,\n        |  \"annotations\": [],\n        |  \"version\": \"0.0.1\",\n        |  \"exec\": {\n        |\t   \"kind\": \"nodejs:20\",\n        |    \"code\": \"foo\",\n        |    \"binary\": false\n        |  },\n        |  \"parameters\": [],\n        |  \"limits\": {\n        |    \"timeout\": 60000,\n        |    \"memory\": 256\n        |  },\n        |  \"namespace\": \"namespace\",\n        |  \"updated\": 1546268400000\n        |}\"\"\".stripMargin.parseJson\n    val action = WhiskDocumentReader.read(manifest[WhiskAction], json)\n    assertType(action.asInstanceOf[WhiskEntity], \"action\")\n  }\n\n  protected def assertType(d: WhiskEntity, entityType: String) = {\n    d.toDocumentRecord.fields(\"entityType\") shouldBe JsString(entityType)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/invoker/test/ContainerMessageConsumerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker.test\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.{TestKit, TestProbe}\nimport common.StreamLogging\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.connector.ContainerCreationError._\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.connector.test.TestConnector\nimport org.apache.openwhisk.core.containerpool.v2.CreationContainer\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.apache.openwhisk.core.invoker.ContainerMessageConsumer\nimport org.apache.openwhisk.core.{WarmUp, WhiskConfig}\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.utils.{retry => utilRetry}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\nimport java.nio.charset.StandardCharsets\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.util.Try\n\n@RunWith(classOf[JUnitRunner])\nclass ContainerMessageConsumerTests\n    extends TestKit(ActorSystem(\"ContainerMessageConsumer\"))\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with StreamLogging\n    with MockFactory\n    with DbUtils\n    with ExecHelpers {\n\n  implicit val actualActorSystem = system // Use system for duplicate system and actorSystem.\n  implicit val ec = actualActorSystem.dispatcher\n  implicit val transId = TransactionId.testing\n  implicit val creationId = CreationId.generate()\n\n  val authStore = WhiskAuthStore.datastore()\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  private val whiskConfig = new WhiskConfig(\n    Map(\n      WhiskConfig.actionInvokePerMinuteLimit -> null,\n      WhiskConfig.triggerFirePerMinuteLimit -> null,\n      WhiskConfig.actionInvokeConcurrentLimit -> null,\n      WhiskConfig.runtimesManifest -> null,\n      WhiskConfig.actionSequenceMaxLimit -> null))\n\n  private val entityStore = WhiskEntityStore.datastore()\n  private val producer = stub[MessageProducer]\n\n  private val defaultUserMemory: ByteSize = 1024.MB\n  private val invokerInstance = InvokerInstanceId(0, userMemory = defaultUserMemory)\n  private val schedulerInstanceId = SchedulerInstanceId(\"0\")\n\n  /* Subject document needed for the second test */\n  private val invocationNamespace = EntityName(\"invocationSpace\")\n  private val uuid = UUID()\n  private val ak = BasicAuthenticationAuthKey(uuid, Secret())\n  private val ns = Namespace(invocationNamespace, uuid)\n  private val auth = WhiskAuth(Subject(), Set(WhiskNamespace(ns, ak)))\n\n  private val schedulerHost = \"127.17.0.1\"\n\n  private val rpcPort = 13001\n\n  override def beforeAll() = {\n    put(authStore, auth)\n  }\n\n  override def afterEach(): Unit = {\n    cleanup()\n  }\n\n  private def fakeMessageProvider(consumer: TestConnector): MessagingProvider = {\n    new MessagingProvider {\n      override def getConsumer(\n        whiskConfig: WhiskConfig,\n        groupId: String,\n        topic: String,\n        maxPeek: Int,\n        maxPollInterval: FiniteDuration)(implicit logging: Logging, actorSystem: ActorSystem): MessageConsumer =\n        consumer\n\n      override def getProducer(config: WhiskConfig, maxRequestSize: Option[ByteSize])(\n        implicit logging: Logging,\n        actorSystem: ActorSystem): MessageProducer = consumer.getProducer()\n\n      override def ensureTopic(config: WhiskConfig,\n                               topic: String,\n                               topicConfig: String,\n                               maxMessageBytes: Option[ByteSize])(implicit logging: Logging): Try[Unit] = Try {}\n    }\n  }\n\n  def sendAckToScheduler(producer: MessageProducer)(schedulerInstanceId: SchedulerInstanceId,\n                                                    ackMessage: ContainerCreationAckMessage): Future[ResultMetadata] = {\n    val topic = s\"creationAck${schedulerInstanceId.asString}\"\n    producer.send(topic, ackMessage)\n  }\n\n  private def createAckMsg(creationMessage: ContainerCreationMessage,\n                           error: Option[ContainerCreationError],\n                           reason: Option[String]) = {\n    ContainerCreationAckMessage(\n      creationMessage.transid,\n      creationMessage.creationId,\n      creationMessage.invocationNamespace,\n      creationMessage.action,\n      creationMessage.revision,\n      creationMessage.whiskActionMetaData,\n      invokerInstance,\n      creationMessage.schedulerHost,\n      creationMessage.rpcPort,\n      creationMessage.retryCount,\n      error,\n      reason)\n  }\n\n  it should \"forward ContainerCreationMessage to containerPool\" in {\n    val pool = TestProbe()\n    val mockConsumer = new TestConnector(\"fakeTopic\", 4, true)\n    val msgProvider = fakeMessageProvider(mockConsumer)\n\n    val consumer =\n      new ContainerMessageConsumer(\n        invokerInstance,\n        pool.ref,\n        entityStore,\n        whiskConfig,\n        msgProvider,\n        200.milliseconds,\n        500,\n        sendAckToScheduler(producer))\n\n    val exec = CodeExecAsString(RuntimeManifest(\"nodejs:20\", ImageName(\"testImage\")), \"testCode\", None)\n    val action =\n      WhiskAction(EntityPath(\"testns\"), EntityName(\"testAction\"), exec, limits = ActionLimits(TimeLimit(1.minute)))\n    put(entityStore, action)\n    val execMetadata =\n      CodeExecMetaDataAsString(exec.manifest, entryPoint = exec.entryPoint)\n    val actionMetadata =\n      WhiskActionMetaData(\n        action.namespace,\n        action.name,\n        execMetadata,\n        action.parameters,\n        action.limits,\n        action.version,\n        action.publish,\n        action.annotations)\n\n    val msg =\n      ContainerCreationMessage(\n        transId,\n        invocationNamespace.asString,\n        action.fullyQualifiedName(true),\n        DocRevision.empty,\n        actionMetadata,\n        schedulerInstanceId,\n        schedulerHost,\n        rpcPort,\n        creationId = creationId)\n\n    mockConsumer.send(msg)\n\n    pool.expectMsgPF() {\n      case CreationContainer(_, _) => true\n    }\n  }\n\n  it should \"send ack(failed) to scheduler when failed to get action from DB \" in {\n    val pool = TestProbe()\n    val creationConsumer = new TestConnector(\"creation\", 4, true)\n    val msgProvider = fakeMessageProvider(creationConsumer)\n\n    val ackTopic = \"ack\"\n    val ackConsumer = new TestConnector(ackTopic, 4, true)\n\n    val consumer =\n      new ContainerMessageConsumer(\n        invokerInstance,\n        pool.ref,\n        entityStore,\n        whiskConfig,\n        msgProvider,\n        200.milliseconds,\n        500,\n        sendAckToScheduler(ackConsumer.getProducer()))\n\n    val exec = CodeExecAsString(RuntimeManifest(\"nodejs:20\", ImageName(\"testImage\")), \"testCode\", None)\n    val whiskAction =\n      WhiskAction(EntityPath(\"testns\"), EntityName(\"testAction2\"), exec, limits = ActionLimits(TimeLimit(1.minute)))\n    val execMetadata =\n      CodeExecMetaDataAsString(exec.manifest, entryPoint = exec.entryPoint)\n    val actionMetadata =\n      WhiskActionMetaData(\n        whiskAction.namespace,\n        whiskAction.name,\n        execMetadata,\n        whiskAction.parameters,\n        whiskAction.limits,\n        whiskAction.version,\n        whiskAction.publish,\n        whiskAction.annotations)\n\n    val creationMessage =\n      ContainerCreationMessage(\n        transId,\n        invocationNamespace.asString,\n        whiskAction.fullyQualifiedName(true),\n        DocRevision.empty,\n        actionMetadata,\n        schedulerInstanceId,\n        schedulerHost,\n        rpcPort,\n        creationId = creationId)\n\n    // action doesn't exist\n    val ackMessage = createAckMsg(creationMessage, Some(DBFetchError), Some(Messages.actionRemovedWhileInvoking))\n    creationConsumer.send(creationMessage)\n\n    within(5.seconds) {\n      utilRetry({\n        val buffer = ackConsumer.peek(50.millisecond)\n        buffer.size shouldBe 1\n        buffer.head._1 shouldBe ackTopic\n        new String(buffer.head._4, StandardCharsets.UTF_8) shouldBe ackMessage.serialize\n      }, 10, Some(500.millisecond))\n      pool.expectNoMessage(2.seconds)\n    }\n\n    // action exist but version mismatch\n    put(entityStore, whiskAction)\n    val actualCreationMessage = creationMessage.copy(revision = DocRevision(\"1-fake\"))\n    val fetchErrorAckMessage =\n      createAckMsg(actualCreationMessage, Some(DBFetchError), Some(Messages.actionMismatchWhileInvoking))\n    creationConsumer.send(actualCreationMessage)\n\n    within(5.seconds) {\n      utilRetry({\n        val buffer2 = ackConsumer.peek(50.millisecond)\n        buffer2.size shouldBe 1\n        buffer2.head._1 shouldBe ackTopic\n        new String(buffer2.head._4, StandardCharsets.UTF_8) shouldBe fetchErrorAckMessage.serialize\n      }, 10, Some(500.millisecond))\n      pool.expectNoMessage(2.seconds)\n    }\n  }\n\n  it should \"drop messages of warm-up action\" in {\n    val pool = TestProbe()\n    val mockConsumer = new TestConnector(\"fakeTopic\", 4, true)\n    val msgProvider = fakeMessageProvider(mockConsumer)\n\n    val consumer =\n      new ContainerMessageConsumer(\n        invokerInstance,\n        pool.ref,\n        entityStore,\n        whiskConfig,\n        msgProvider,\n        200.milliseconds,\n        500,\n        sendAckToScheduler(producer))\n\n    val exec = CodeExecAsString(RuntimeManifest(\"nodejs:20\", ImageName(\"testImage\")), \"testCode\", None)\n    val action =\n      WhiskAction(\n        WarmUp.warmUpAction.namespace.toPath,\n        WarmUp.warmUpAction.name,\n        exec,\n        limits = ActionLimits(TimeLimit(1.minute)))\n    val doc = put(entityStore, action)\n    val execMetadata =\n      CodeExecMetaDataAsString(exec.manifest, entryPoint = exec.entryPoint)\n\n    val actionMetadata =\n      WhiskActionMetaData(\n        action.namespace,\n        action.name,\n        execMetadata,\n        action.parameters,\n        action.limits,\n        action.version,\n        action.publish,\n        action.annotations)\n\n    val msg =\n      ContainerCreationMessage(\n        transId,\n        invocationNamespace.asString,\n        action.fullyQualifiedName(false),\n        DocRevision.empty,\n        actionMetadata,\n        schedulerInstanceId,\n        schedulerHost,\n        rpcPort,\n        creationId = creationId)\n\n    mockConsumer.send(msg)\n\n    pool.expectNoMessage(1.seconds)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/invoker/test/DefaultInvokerServerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker.test\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.{OK, Unauthorized}\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport common.StreamLogging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.v2.{NotSupportedPoolState, TotalContainerPoolState}\nimport org.apache.openwhisk.core.invoker.Invoker.InvokerEnabled\nimport org.apache.openwhisk.core.invoker.{DefaultInvokerServer, InvokerCore}\nimport org.apache.openwhisk.http.BasicHttpService\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.Future\n\n/**\n * Tests InvokerServer API.\n */\n@RunWith(classOf[JUnitRunner])\nclass DefaultInvokerServerTests\n    extends AnyFlatSpec\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with ScalatestRouteTest\n    with Matchers\n    with StreamLogging\n    with MockFactory {\n\n  def transid() = TransactionId(\"tid\")\n\n  val systemUsername = \"username\"\n  val systemPassword = \"password\"\n\n  val reactive = new TestInvokerReactive\n  val server = new DefaultInvokerServer(reactive, systemUsername, systemPassword)\n\n  override protected def afterEach(): Unit = reactive.reset()\n\n  /** DefaultInvokerServer API tests */\n  behavior of \"DefaultInvokerServer API\"\n\n  it should \"enable invoker\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Post(s\"/enable\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      reactive.enableCount shouldBe 1\n      reactive.disableCount shouldBe 0\n    }\n  }\n\n  it should \"disable invoker\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Post(s\"/disable\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 1\n    }\n  }\n\n  it should \"check if invoker is enabled\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Get(s\"/isEnabled\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      Unmarshal(responseEntity)\n        .to[String]\n        .map(response => {\n          InvokerEnabled.parseJson(response) shouldEqual InvokerEnabled(true)\n        })\n    }\n  }\n\n  it should \"not enable invoker with invalid credential\" in {\n    implicit val tid = transid()\n    val invalidCredentials = BasicHttpCredentials(\"invaliduser\", \"invalidpass\")\n    Post(s\"/enable\") ~> addCredentials(invalidCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 0\n    }\n  }\n\n  it should \"not disable invoker with invalid credential\" in {\n    implicit val tid = transid()\n    val invalidCredentials = BasicHttpCredentials(\"invaliduser\", \"invalidpass\")\n    Post(s\"/disable\") ~> addCredentials(invalidCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 0\n    }\n  }\n\n  it should \"not enable invoker with empty credential\" in {\n    implicit val tid = transid()\n    Post(s\"/enable\") ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 0\n    }\n  }\n\n  it should \"not disable invoker with empty credential\" in {\n    implicit val tid = transid()\n    Post(s\"/disable\") ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 0\n    }\n  }\n\n}\n\nclass TestInvokerReactive extends InvokerCore with BasicHttpService {\n  var enableCount = 0\n  var disableCount = 0\n\n  override def enable(): String = {\n    enableCount += 1\n    s\"\"\n  }\n\n  override def disable(): String = {\n    disableCount += 1\n    s\"\"\n  }\n\n  override def isEnabled(): String = {\n    complete(InvokerEnabled(true).serialize())\n    s\"\"\n  }\n\n  override def backfillPrewarm(): String = {\n    \"\"\n  }\n\n  override def getPoolState(): Future[Either[NotSupportedPoolState, TotalContainerPoolState]] = {\n    Future.successful(Left(NotSupportedPoolState()))\n  }\n\n  def reset(): Unit = {\n    enableCount = 0\n    disableCount = 0\n  }\n\n  /**\n   * Gets the routes implemented by the HTTP service.\n   *\n   * @param transid the id for the transaction (every request is assigned an id)\n   */\n  override def routes(implicit transid: TransactionId): Route = ???\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/invoker/test/FPCInvokerServerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker.test\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.{OK, Unauthorized}\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal\nimport common.StreamLogging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.containerpool.v2.{NotSupportedPoolState, TotalContainerPoolState}\nimport org.apache.openwhisk.core.invoker.Invoker.InvokerEnabled\nimport org.apache.openwhisk.core.invoker.{FPCInvokerServer, InvokerCore}\nimport org.apache.openwhisk.http.BasicHttpService\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.Future\n\n/**\n * Tests InvokerServerV2 API.\n */\n@RunWith(classOf[JUnitRunner])\nclass FPCInvokerServerTests\n    extends AnyFlatSpec\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with ScalatestRouteTest\n    with Matchers\n    with StreamLogging\n    with MockFactory {\n\n  def transid() = TransactionId(\"tid\")\n\n  val systemUsername = \"username\"\n  val systemPassword = \"password\"\n\n  val reactive = new TestFPCInvokerReactive\n  val server = new FPCInvokerServer(reactive, systemUsername, systemPassword)\n\n  override protected def afterEach(): Unit = reactive.reset()\n\n  /** FPCInvokerServer API tests */\n  behavior of \"FPCInvokerServer API\"\n\n  it should \"enable invoker\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Post(s\"/enable\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      reactive.enableCount shouldBe 1\n      reactive.disableCount shouldBe 0\n    }\n  }\n\n  it should \"disable invoker\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Post(s\"/disable\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 1\n    }\n  }\n\n  it should \"check if invoker is enabled\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Get(s\"/isEnabled\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      Unmarshal(responseEntity)\n        .to[String]\n        .map(response => {\n          InvokerEnabled.parseJson(response) shouldEqual InvokerEnabled(true)\n        })\n    }\n  }\n\n  it should \"not enable invoker with invalid credential\" in {\n    implicit val tid = transid()\n    val invalidCredentials = BasicHttpCredentials(\"invaliduser\", \"invalidpass\")\n    Post(s\"/enable\") ~> addCredentials(invalidCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 0\n    }\n  }\n\n  it should \"not disable invoker with invalid credential\" in {\n    implicit val tid = transid()\n    val invalidCredentials = BasicHttpCredentials(\"invaliduser\", \"invalidpass\")\n    Post(s\"/disable\") ~> addCredentials(invalidCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 0\n    }\n  }\n\n  it should \"not enable invoker with empty credential\" in {\n    implicit val tid = transid()\n    Post(s\"/enable\") ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 0\n    }\n  }\n\n  it should \"not disable invoker with empty credential\" in {\n    implicit val tid = transid()\n    Post(s\"/disable\") ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      reactive.enableCount shouldBe 0\n      reactive.disableCount shouldBe 0\n    }\n  }\n}\n\nclass TestFPCInvokerReactive extends InvokerCore with BasicHttpService {\n  var enableCount = 0\n  var disableCount = 0\n\n  override def enable(): String = {\n    enableCount += 1\n    \"\"\n  }\n\n  override def disable(): String = {\n    disableCount += 1\n    \"\"\n  }\n\n  override def isEnabled(): String = {\n    complete(InvokerEnabled(true).serialize())\n    \"\"\n  }\n\n  override def backfillPrewarm(): String = {\n    \"\"\n  }\n\n  override def getPoolState(): Future[Either[NotSupportedPoolState, TotalContainerPoolState]] = {\n    Future.successful(Left(NotSupportedPoolState()))\n  }\n\n  def reset(): Unit = {\n    enableCount = 0\n    disableCount = 0\n  }\n\n  /**\n   * Gets the routes implemented by the HTTP service.\n   *\n   * @param transid the id for the transaction (every request is assigned an id)\n   */\n  override def routes(implicit transid: TransactionId): Route = ???\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/invoker/test/InstanceIdAssignerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker.test\n\nimport common.StreamLogging\nimport org.apache.curator.test.TestingServer\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.core.invoker.InstanceIdAssigner\n\n@RunWith(classOf[JUnitRunner])\nclass InstanceIdAssignerTests extends AnyFlatSpec with Matchers with StreamLogging with BeforeAndAfterEach {\n  behavior of \"Id Assignment\"\n\n  private var zkServer: TestingServer = _\n\n  override protected def beforeEach(): Unit = {\n    zkServer = new TestingServer()\n  }\n\n  override protected def afterEach(): Unit = {\n    zkServer.stop()\n  }\n\n  it should \"assign fresh id\" in {\n    val assigner = new InstanceIdAssigner(zkServer.getConnectString)\n    assigner.setAndGetId(\"foo\") shouldBe 0\n  }\n\n  it should \"reuse id if exists\" in {\n    val assigner = new InstanceIdAssigner(zkServer.getConnectString)\n    assigner.setAndGetId(\"foo\") shouldBe 0\n    assigner.setAndGetId(\"bar\") shouldBe 1\n    assigner.setAndGetId(\"bar\") shouldBe 1\n  }\n\n  it should \"attempt to overwrite id for unique name if overwrite set\" in {\n    val assigner = new InstanceIdAssigner(zkServer.getConnectString)\n    assigner.setAndGetId(\"foo\") shouldBe 0\n    assigner.setAndGetId(\"bar\", Some(0)) shouldBe 0\n  }\n\n  it should \"overwrite an id for unique name that already exists and reset overwritten id\" in {\n    val assigner = new InstanceIdAssigner(zkServer.getConnectString)\n    assigner.setAndGetId(\"foo\") shouldBe 0\n    assigner.setAndGetId(\"bar\", Some(0)) shouldBe 0\n    assigner.setAndGetId(\"foo\") shouldBe 1\n    assigner.setAndGetId(\"cat\") shouldBe 2\n  }\n\n  it should \"fail to overwrite an id too large for the invoker pool size\" in {\n    val assigner = new InstanceIdAssigner(zkServer.getConnectString)\n    assigner.setAndGetId(\"foo\") shouldBe 0\n    assertThrows[IllegalArgumentException](assigner.setAndGetId(\"bar\", Some(2)))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/invoker/test/InvokerBootUpTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker.test\n\nimport java.nio.charset.StandardCharsets\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.TestKit\nimport common.WskTestHelpers\nimport org.apache.openwhisk.common.InvokerState.Healthy\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.connector.InvokerResourceMessage\nimport org.apache.openwhisk.core.containerpool.v2.InvokerHealthManager.healthActionNamePrefix\nimport org.apache.openwhisk.core.entity.InvokerInstanceId\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.namespacePrefix\nimport org.apache.openwhisk.core.etcd.EtcdKV.{InstanceKeys, InvokerKeys}\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdConfig}\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport pureconfig.loadConfigOrThrow\nimport org.apache.openwhisk.core.entity.size._\n\nimport scala.collection.JavaConverters._\nimport scala.concurrent.ExecutionContextExecutor\nimport scala.concurrent.duration._\nimport pureconfig.generic.auto._\n\n@RunWith(classOf[JUnitRunner])\nclass InvokerBootUpTests\n    extends TestKit(ActorSystem(\"SchedulerFlow\"))\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with WskTestHelpers\n    with ScalaFutures {\n  private implicit val ec: ExecutionContextExecutor = system.dispatcher\n\n  private val systemNamespace = \"whisk.system\"\n  private val etcd = EtcdClient.apply(loadConfigOrThrow[EtcdConfig](ConfigKeys.etcd))\n\n  override def afterAll(): Unit = {\n    etcd.close()\n    super.afterAll()\n  }\n\n  behavior of \"Invoker Etcd Key\"\n  it should \"haven't health action key\" in {\n    val healthActionPrefix = s\"$namespacePrefix/namespace/$systemNamespace/$systemNamespace/$healthActionNamePrefix\"\n    awaitAssert({\n      etcd.getPrefix(healthActionPrefix).futureValue.getKvsList.size() shouldBe 0\n    }, 10.seconds)\n  }\n\n  it should \"have lease key\" in {\n    val leasePrefix = s\"$namespacePrefix/instance\"\n    awaitAssert({\n      val leases = etcd.getPrefix(leasePrefix).futureValue.getKvsList.asScala.toArray\n\n      // validate size\n      leases.length > 0\n\n      // validate key\n      for (i <- leases.indices) {\n        val invokerId = InvokerInstanceId(i, userMemory = 256.MB)\n        leases(i).getKey.toString(StandardCharsets.UTF_8) shouldBe InstanceKeys.instanceLease(invokerId)\n      }\n    }, 10.seconds)\n  }\n\n  it should \"have invoker key\" in {\n    val invokerPrefix = InvokerKeys.prefix\n    awaitAssert(\n      {\n        val invokers = etcd.getPrefix(invokerPrefix).futureValue.getKvsList.asScala.toArray\n\n        // validate size\n        invokers.length > 0\n\n        for (i <- invokers.indices) {\n          val invokerId = InvokerInstanceId(i, uniqueName = Some(s\"$i\"), userMemory = 256.MB)\n          // validate key\n          invokers(i).getKey.toString(StandardCharsets.UTF_8) shouldBe InvokerKeys.health(invokerId)\n\n          // validate if all invoker is healthy\n          InvokerResourceMessage\n            .parse(invokers(i).getValue.toString(StandardCharsets.UTF_8))\n            .map { resource =>\n              resource.status shouldBe Healthy.asString\n            }\n        }\n      },\n      10.seconds)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/invoker/test/NamespaceBlacklistTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.invoker.test\n\nimport common.{StreamLogging, WskActorSystem}\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json._\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.invoker.NamespaceBlacklist\nimport org.apache.openwhisk.utils.{retry => testRetry}\n\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass NamespaceBlacklistTests\n    extends AnyFlatSpec\n    with Matchers\n    with DbUtils\n    with ScalaFutures\n    with IntegrationPatience\n    with WskActorSystem\n    with StreamLogging {\n\n  behavior of \"NamespaceBlacklist\"\n\n  implicit val tid = TransactionId.testing\n\n  val authStore = WhiskAuthStore.datastore()\n\n  val limitsAndAuths = Seq(\n    new LimitEntity(EntityName(\"testnamespace1\"), UserLimits(invocationsPerMinute = Some(0))),\n    new LimitEntity(EntityName(\"testnamespace2\"), UserLimits(concurrentInvocations = Some(0))),\n    new LimitEntity(\n      EntityName(\"testnamespace3\"),\n      UserLimits(invocationsPerMinute = Some(1), concurrentInvocations = Some(1))))\n\n  /* Subject document needed for the second test */\n  val uuid4 = UUID()\n  val uuid5 = UUID()\n  val ak4 = BasicAuthenticationAuthKey(uuid4, Secret())\n  val ak5 = BasicAuthenticationAuthKey(uuid5, Secret())\n  val ns4 = Namespace(EntityName(\"different1\"), uuid4)\n  val ns5 = Namespace(EntityName(\"different2\"), uuid5)\n  val blockedSubject = new ExtendedAuth(Subject(), Set(WhiskNamespace(ns4, ak4), WhiskNamespace(ns5, ak5)), true)\n\n  val blockedNamespacesCount = 2 + blockedSubject.namespaces.size\n\n  private def authToIdentities(auth: WhiskAuth): Set[Identity] = {\n    auth.namespaces.map { ns =>\n      Identity(auth.subject, ns.namespace, ns.authkey)\n    }\n  }\n\n  private def limitToIdentity(limit: LimitEntity): Identity = {\n    val namespace = limit.docid.id.dropRight(\"/limits\".length)\n    Identity(limit.subject, Namespace(EntityName(namespace), UUID()), BasicAuthenticationAuthKey(UUID(), Secret()))\n  }\n\n  override def beforeAll() = {\n    limitsAndAuths foreach (put(authStore, _))\n    put(authStore, blockedSubject)\n    waitOnView(authStore, blockedNamespacesCount, NamespaceBlacklist.view)\n  }\n\n  override def afterAll() = {\n    cleanup()\n    super.afterAll()\n  }\n\n  it should \"mark a namespace as blocked if limit is 0 in database or if one of its subjects is blocked\" in {\n    val blacklist = new NamespaceBlacklist(authStore)\n\n    testRetry({\n      blacklist.refreshBlacklist().futureValue should have size blockedNamespacesCount\n    }, 60, Some(1.second))\n\n    limitsAndAuths.map(limitToIdentity).map(blacklist.isBlacklisted) shouldBe Seq(true, true, false)\n    authToIdentities(blockedSubject).toSeq.map(blacklist.isBlacklisted) shouldBe Seq(true, true)\n  }\n\n  class LimitEntity(name: EntityName, limits: UserLimits) extends WhiskAuth(Subject(), namespaces = Set.empty) {\n    override def docid = DocId(s\"${name.name}/limits\")\n\n    override def toJson = UserLimits.serdes.write(limits).asJsObject\n  }\n\n  class ExtendedAuth(subject: Subject, namespaces: Set[WhiskNamespace], blocked: Boolean)\n      extends WhiskAuth(subject, namespaces) {\n    override def toJson = JsObject(super.toJson.fields + (\"blocked\" -> JsBoolean(blocked)))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/limits/ActionLimitsTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.limits\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.ContentTooLarge\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.BadGateway\nimport java.io.File\nimport java.io.PrintWriter\nimport java.time.Instant\n\nimport scala.concurrent.duration.{Duration, DurationInt}\nimport scala.language.postfixOps\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common.ActivationResult\nimport common.TestHelpers\nimport common.TestUtils\nimport common.TestUtils.{BAD_REQUEST, DONTCARE_EXIT, SUCCESS_EXIT}\nimport common.TimingHelpers\nimport common.WhiskProperties\nimport common.rest.WskRestOperations\nimport common.WskProps\nimport common.WskTestHelpers\nimport common.WskActorSystem\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.entity.{\n  ActivationEntityLimit,\n  ActivationResponse,\n  ByteSize,\n  Exec,\n  IntraConcurrencyLimit,\n  LogLimit,\n  MemoryLimit,\n  TimeLimit\n}\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.http.Messages\n\n@RunWith(classOf[JUnitRunner])\nclass ActionLimitsTests extends TestHelpers with WskTestHelpers with WskActorSystem with TimingHelpers {\n\n  implicit val wskprops = WskProps()\n  val wsk = new WskRestOperations\n\n  val defaultSleepAction = TestUtils.getTestActionFilename(\"sleep.js\")\n  val allowedActionDuration = 10 seconds\n\n  val testActionsDir = WhiskProperties.getFileRelativeToWhiskHome(\"tests/dat/actions\")\n  val actionCodeLimit = Exec.sizeLimit\n\n  val openFileAction = TestUtils.getTestActionFilename(\"openFiles.js\")\n  val openFileLimit = 1024\n  // Allow for already opened files in container.\n  // Attention: do not change this value without a thorough check. It ensures that\n  // OpenWhisk users have a minimum number of file descriptors available in their actions.\n  // If changes in a managed runtime lead to a decrease of file descriptors available for\n  // action code, this may break existing actions.\n  // Examples:\n  // * With the introduction of Node.js 10, this was changed from \"openFileLimit - 15\" to\n  //   \"openFileLimit - 20\".\n  // * With Docker 18.09.3, we observed test failures and changed to \"openFileLimit - 24\".\n  // * With the introduction of Node.js 20, this was changed from \"openFileLimit - 24\" to\n  //   \"openFileLimit - 30\".\n  val minExpectedOpenFiles = openFileLimit - 30\n\n  behavior of \"Action limits\"\n\n  /**\n   * Helper class for the integration test following below.\n   * @param timeout the action timeout limit to be set in test\n   * @param memory the action memory size limit to be set in test\n   * @param logs the action log size limit to be set in test\n   * @param concurrency the action concurrency limit to be set in test\n   * @param ec the expected exit code when creating the action\n   */\n  sealed case class PermutationTestParameter(timeout: Option[Duration] = None,\n                                             memory: Option[ByteSize] = None,\n                                             logs: Option[ByteSize] = None,\n                                             concurrency: Option[Int] = None,\n                                             ec: Int = SUCCESS_EXIT) {\n    override def toString: String =\n      s\"timeout: ${toTimeoutString}, memory: ${toMemoryString}, logsize: ${toLogsString}, concurrency: ${toConcurrencyString}\"\n\n    val toTimeoutString = timeout match {\n      case None                                    => \"None\"\n      case Some(TimeLimit.MIN_DURATION)            => s\"${TimeLimit.MIN_DURATION} (= min)\"\n      case Some(TimeLimit.STD_DURATION)            => s\"${TimeLimit.STD_DURATION} (= std)\"\n      case Some(TimeLimit.MAX_DURATION)            => s\"${TimeLimit.MAX_DURATION} (= max)\"\n      case Some(t) if (t < TimeLimit.MIN_DURATION) => s\"${t} (< min)\"\n      case Some(t) if (t > TimeLimit.MAX_DURATION) => s\"${t} (> max)\"\n      case Some(t)                                 => s\"${t} (allowed)\"\n    }\n\n    val toMemoryString = memory match {\n      case None                                    => \"None\"\n      case Some(MemoryLimit.MIN_MEMORY)            => s\"${MemoryLimit.MIN_MEMORY.toMB.MB} (= min)\"\n      case Some(MemoryLimit.STD_MEMORY)            => s\"${MemoryLimit.STD_MEMORY.toMB.MB} (= std)\"\n      case Some(MemoryLimit.MAX_MEMORY)            => s\"${MemoryLimit.MAX_MEMORY.toMB.MB} (= max)\"\n      case Some(m) if (m < MemoryLimit.MIN_MEMORY) => s\"${m.toMB.MB} (< min)\"\n      case Some(m) if (m > MemoryLimit.MAX_MEMORY) => s\"${m.toMB.MB} (> max)\"\n      case Some(m)                                 => s\"${m.toMB.MB} (allowed)\"\n    }\n\n    val toLogsString = logs match {\n      case None                                  => \"None\"\n      case Some(LogLimit.MIN_LOGSIZE)            => s\"${LogLimit.MIN_LOGSIZE} (= min)\"\n      case Some(LogLimit.STD_LOGSIZE)            => s\"${LogLimit.STD_LOGSIZE} (= std)\"\n      case Some(LogLimit.MAX_LOGSIZE)            => s\"${LogLimit.MAX_LOGSIZE} (= max)\"\n      case Some(l) if (l < LogLimit.MIN_LOGSIZE) => s\"${l} (< min)\"\n      case Some(l) if (l > LogLimit.MAX_LOGSIZE) => s\"${l} (> max)\"\n      case Some(l)                               => s\"${l} (allowed)\"\n    }\n    val toConcurrencyString = concurrency match {\n      case None                                                  => \"None\"\n      case Some(IntraConcurrencyLimit.MIN_CONCURRENT)            => s\"${IntraConcurrencyLimit.MIN_CONCURRENT} (= min)\"\n      case Some(IntraConcurrencyLimit.STD_CONCURRENT)            => s\"${IntraConcurrencyLimit.STD_CONCURRENT} (= std)\"\n      case Some(IntraConcurrencyLimit.MAX_CONCURRENT)            => s\"${IntraConcurrencyLimit.MAX_CONCURRENT} (= max)\"\n      case Some(c) if (c < IntraConcurrencyLimit.MIN_CONCURRENT) => s\"${c} (< min)\"\n      case Some(c) if (c > IntraConcurrencyLimit.MAX_CONCURRENT) => s\"${c} (> max)\"\n      case Some(c)                                               => s\"${c} (allowed)\"\n    }\n    val toExpectedResultString: String = if (ec == SUCCESS_EXIT) \"allow\" else \"reject\"\n  }\n\n  val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean)\n\n  val perms = { // Assert for valid permutations that the values are set correctly\n    for {\n      time <- Seq(None, Some(TimeLimit.MIN_DURATION), Some(TimeLimit.MAX_DURATION))\n      mem <- Seq(None, Some(MemoryLimit.MIN_MEMORY), Some(MemoryLimit.MAX_MEMORY))\n      log <- Seq(None, Some(LogLimit.MIN_LOGSIZE), Some(LogLimit.MAX_LOGSIZE))\n      concurrency <- if (!concurrencyEnabled || (IntraConcurrencyLimit.MIN_CONCURRENT == IntraConcurrencyLimit.MAX_CONCURRENT)) {\n        Seq(None, Some(IntraConcurrencyLimit.MIN_CONCURRENT))\n      } else {\n        Seq(None, Some(IntraConcurrencyLimit.MIN_CONCURRENT), Some(IntraConcurrencyLimit.MAX_CONCURRENT))\n      }\n    } yield PermutationTestParameter(time, mem, log, concurrency)\n  } ++\n    // Add variations for negative tests\n    Seq(\n      PermutationTestParameter(Some(0.milliseconds), None, None, None, BAD_REQUEST), // timeout that is lower than allowed\n      PermutationTestParameter(Some(TimeLimit.MAX_DURATION.plus(1 second)), None, None, None, BAD_REQUEST), // timeout that is slightly higher than allowed\n      PermutationTestParameter(Some(TimeLimit.MAX_DURATION * 10), None, None, None, BAD_REQUEST), // timeout that is much higher than allowed\n      PermutationTestParameter(None, Some(0.MB), None, None, BAD_REQUEST), // memory limit that is lower than allowed\n      PermutationTestParameter(None, None, None, Some(0), BAD_REQUEST), // concurrency limit that is lower than allowed\n      PermutationTestParameter(None, Some(MemoryLimit.MAX_MEMORY + 1.MB), None, None, BAD_REQUEST), // memory limit that is slightly higher than allowed\n      PermutationTestParameter(None, Some((MemoryLimit.MAX_MEMORY.toMB * 5).MB), None, None, BAD_REQUEST), // memory limit that is much higher than allowed\n      PermutationTestParameter(None, None, Some((LogLimit.MAX_LOGSIZE.toMB * 5).MB), None, BAD_REQUEST), // log size limit that is much higher than allowed\n      PermutationTestParameter(None, None, None, Some(Int.MaxValue), BAD_REQUEST)) // concurrency limit that is much higher than allowed\n\n  /**\n   * Integration test to verify that valid timeout, memory, log size, and concurrency limits are accepted\n   * when creating an action while any invalid limit is rejected.\n   *\n   * At the first sight, this test looks like a typical unit test that should not be performed\n   * as an integration test. It is performed as an integration test requiring an OpenWhisk\n   * deployment to verify that limit settings of the tested deployment fit with the values\n   * used in this test.\n   */\n  perms.foreach { parm =>\n    it should s\"${parm.toExpectedResultString} creation of an action with these limits: ${parm}\" in withAssetCleaner(\n      wskprops) { (wp, assetHelper) =>\n      val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n\n      // Limits to assert, standard values if CLI omits certain values\n      val limits = JsObject(\n        \"timeout\" -> parm.timeout.getOrElse(TimeLimit.STD_DURATION).toMillis.toJson,\n        \"memory\" -> parm.memory.getOrElse(MemoryLimit.STD_MEMORY).toMB.toInt.toJson,\n        \"logs\" -> parm.logs.getOrElse(LogLimit.STD_LOGSIZE).toMB.toInt.toJson,\n        \"concurrency\" -> parm.concurrency.getOrElse(IntraConcurrencyLimit.STD_CONCURRENT).toJson)\n\n      val name = \"ActionLimitTests-\" + Instant.now.toEpochMilli\n      val createResult = assetHelper.withCleaner(wsk.action, name, confirmDelete = (parm.ec == SUCCESS_EXIT)) {\n        (action, _) =>\n          val result = action.create(\n            name,\n            file,\n            logsize = parm.logs,\n            memory = parm.memory,\n            timeout = parm.timeout,\n            concurrency = parm.concurrency,\n            expectedExitCode = DONTCARE_EXIT)\n          withClue(s\"Unexpected result when creating action '${name}':\\n${result.toString}\\nFailed assertion:\") {\n            result.exitCode should be(parm.ec)\n          }\n          result\n      }\n\n      if (parm.ec == SUCCESS_EXIT) {\n        val JsObject(parsedAction) = wsk.action.get(name).respBody\n        parsedAction(\"limits\") shouldBe limits\n      } else {\n        createResult.stderr should include(\"allowed threshold\")\n      }\n    }\n  }\n\n  /**\n   * Test an action that exceeds its specified time limit. Explicitly specify a rather low time\n   * limit to keep the test's runtime short. Invoke an action that sleeps for the specified time\n   * limit plus one second.\n   */\n  it should \"error with a proper warning if the action exceeds its time limits\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"TestActionCausingTimeout-\" + System.currentTimeMillis()\n      assetHelper.withCleaner(wsk.action, name, confirmDelete = true) { (action, _) =>\n        action.create(name, Some(defaultSleepAction), timeout = Some(allowedActionDuration))\n      }\n\n      val run = wsk.action.invoke(name, Map(\"sleepTimeInMs\" -> allowedActionDuration.plus(1 second).toMillis.toJson))\n      withActivation(wsk.activation, run) { result =>\n        withClue(\"Activation result not as expected:\") {\n          result.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n          result.response.result.get.asJsObject.fields(\"error\") shouldBe {\n            Messages.timedoutActivation(allowedActionDuration, init = false).toJson\n          }\n          result.duration.toInt should be >= allowedActionDuration.toMillis.toInt\n        }\n      }\n  }\n\n  /**\n   * Test an action that tightly stays within its specified time limit. Explicitly specify a rather low time\n   * limit to keep the test's runtime short. Invoke an action that sleeps for the specified time\n   * limit minus one second.\n   */\n  it should \"succeed on an action staying within its time limits\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"TestActionCausingNoTimeout-\" + System.currentTimeMillis()\n    assetHelper.withCleaner(wsk.action, name, confirmDelete = true) { (action, _) =>\n      action.create(name, Some(defaultSleepAction), timeout = Some(allowedActionDuration))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"sleepTimeInMs\" -> allowedActionDuration.minus(1 second).toMillis.toJson))\n    withActivation(wsk.activation, run) { result =>\n      withClue(\"Activation result not as expected:\") {\n        result.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.Success)\n        result.response.result.get.toString should include(\"\"\"Terminated successfully after around\"\"\")\n      }\n    }\n  }\n\n  it should \"succeed but truncate logs, if log size exceeds its limit\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val bytesPerLine = 16\n      val allowedSize = 1 megabytes\n      val name = \"TestActionCausingExceededLogs\"\n      assetHelper.withCleaner(wsk.action, name, confirmDelete = true) {\n        val actionName = TestUtils.getTestActionFilename(\"dosLogs.js\")\n        (action, _) =>\n          action.create(name, Some(actionName), logsize = Some(allowedSize))\n      }\n\n      // Add 10% to allowed size to exceed limit\n      val attemptedSize = (allowedSize.toBytes * 1.1).toLong.bytes\n\n      val run = wsk.action.invoke(name, Map(\"payload\" -> attemptedSize.toBytes.toJson))\n      withActivation(wsk.activation, run, totalWait = 120 seconds) { response =>\n        val lines = response.logs.get\n        lines.last should include(Messages.truncateLogs(allowedSize))\n      }\n  }\n\n  it should s\"successfully invoke an action with a payload close to the limit (${ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT.toMB} MB)\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val name = \"TestActionCausingJustInBoundaryResult\"\n    assetHelper.withCleaner(wsk.action, name) {\n      val actionName = TestUtils.getTestActionFilename(\"echo.js\")\n      (action, _) =>\n        action.create(name, Some(actionName), timeout = Some(15.seconds))\n    }\n\n    val allowedSize = ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT.toBytes\n\n    // Needs some bytes grace since activation message is not only the payload.\n    val args = Map(\"p\" -> (\"a\" * (allowedSize - 750).toInt).toJson)\n    val start = Instant.now\n    val rr = wsk.action.invoke(name, args, blocking = true, expectedExitCode = TestUtils.SUCCESS_EXIT)\n    Instant.now.toEpochMilli - start.toEpochMilli should be < 15000L // Ensure activation was not retrieved via DB polling\n    val activation = wsk.parseJsonString(rr.respData).convertTo[ActivationResult]\n\n    activation.response.success shouldBe true\n\n    // The payload is echoed and thus the backchannel supports the limit as well.\n    activation.response.result shouldBe Some(args.toJson)\n  }\n\n  Seq(true, false).foreach { blocking =>\n    it should s\"succeed but truncate result, if result exceeds its limit (blocking: $blocking)\" in withAssetCleaner(\n      wskprops) { (wp, assetHelper) =>\n      val name = \"TestActionCausingExcessiveResult\"\n      assetHelper.withCleaner(wsk.action, name) {\n        val actionName = TestUtils.getTestActionFilename(\"sizedResult.js\")\n        (action, _) =>\n          action.create(name, Some(actionName), timeout = Some(15.seconds))\n      }\n\n      val allowedSize = ActivationEntityLimit.MAX_ACTIVATION_ENTITY_LIMIT.toBytes\n\n      def checkResponse(activation: ActivationResult) = {\n        val response = activation.response\n        response.success shouldBe false\n        response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)\n        val msg = response.result.get.asJsObject.fields(ActivationResponse.ERROR_FIELD).convertTo[String]\n        val expected = Messages.truncatedResponse((allowedSize + 10).B, allowedSize.B)\n        withClue(s\"is: ${msg.take(expected.length)}\\nexpected: $expected\") {\n          msg.startsWith(expected) shouldBe true\n        }\n        msg.endsWith(\"a\") shouldBe true\n      }\n\n      // this tests an active ack failure to post from invoker\n      val args = Map(\"size\" -> (allowedSize + 1).toJson, \"char\" -> \"a\".toJson)\n      val code = if (blocking) BadGateway.intValue else TestUtils.ACCEPTED\n      if (blocking) {\n        val start = Instant.now\n        val rr = wsk.action.invoke(name, args, blocking = blocking, expectedExitCode = code)\n        Instant.now.toEpochMilli - start.toEpochMilli should be < 15000L // Ensure activation was not retrieved via DB polling\n        checkResponse(wsk.parseJsonString(rr.respData).convertTo[ActivationResult])\n      } else {\n        val rr = wsk.action.invoke(name, args, blocking = blocking, expectedExitCode = code)\n        withActivation(wsk.activation, rr, totalWait = 120 seconds) { checkResponse(_) }\n      }\n    }\n  }\n\n  it should \"succeed with one log line\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"TestActionWithLogs\"\n    assetHelper.withCleaner(wsk.action, name, confirmDelete = true) {\n      val actionName = TestUtils.getTestActionFilename(\"dosLogs.js\")\n      (action, _) =>\n        action.create(name, Some(actionName))\n    }\n\n    val run = wsk.action.invoke(name)\n    withActivation(wsk.activation, run) { response =>\n      val logs = response.logs.get\n      withClue(logs) { logs.size shouldBe 1 }\n      logs.head should include(\"123456789abcdef\")\n\n      response.response.status shouldBe \"success\"\n      response.response.result shouldBe Some(JsObject(\"msg\" -> 1.toJson))\n    }\n  }\n\n  it should \"fail on creating an action with exec which is too big\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"TestActionCausingExecTooBig\"\n\n    val actionCode = new File(s\"$testActionsDir${File.separator}$name.js\")\n    actionCode.createNewFile()\n    val pw = new PrintWriter(actionCode)\n    pw.write(\"a\" * (actionCodeLimit.toBytes + 1).toInt)\n    pw.close\n\n    assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) =>\n      action.create(name, Some(actionCode.getAbsolutePath), expectedExitCode = ContentTooLarge.intValue)\n    }\n\n    actionCode.delete\n  }\n\n  /**\n   * Test an action that does not exceed the allowed number of open files.\n   */\n  it should \"successfully invoke an action when it is within nofile limit\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"TestFileLimitGood-\" + System.currentTimeMillis()\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(openFileAction))\n      }\n\n      val run = wsk.action.invoke(name, Map(\"numFiles\" -> minExpectedOpenFiles.toJson))\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.success shouldBe true\n        activation.response.result.get shouldBe {\n          JsObject(\"filesToOpen\" -> minExpectedOpenFiles.toJson, \"filesOpen\" -> minExpectedOpenFiles.toJson)\n        }\n      }\n  }\n\n  /**\n   * Test an action that should fail to open way too many files.\n   */\n  it should \"fail to invoke an action when it exceeds nofile limit\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"TestFileLimitBad-\" + System.currentTimeMillis()\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(openFileAction))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"numFiles\" -> (openFileLimit + 1).toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.success shouldBe false\n\n      val error = activation.response.result.get.asJsObject.fields(\"error\").asJsObject\n      error.fields(\"filesToOpen\") shouldBe (openFileLimit + 1).toJson\n\n      error.fields(\"message\") shouldBe {\n        JsObject(\n          \"code\" -> \"EMFILE\".toJson,\n          \"errno\" -> (-24).toJson,\n          \"path\" -> \"/dev/zero\".toJson,\n          \"syscall\" -> \"open\".toJson)\n      }\n\n      val JsNumber(n) = error.fields(\"filesOpen\")\n      n.toInt should be >= minExpectedOpenFiles\n\n      activation.logs\n        .getOrElse(List.empty)\n        .count(_.contains(\"ERROR: opened files = \")) shouldBe 1\n    }\n  }\n\n  it should \"be able to run memory intensive actions multiple times by running the GC in the action\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val name = \"TestNodeJsMemoryActionAbleToRunOften\"\n    assetHelper.withCleaner(wsk.action, name, confirmDelete = true) {\n      val allowedMemory = 512 megabytes\n      val actionName = TestUtils.getTestActionFilename(\"memoryWithGC.js\")\n      (action, _) =>\n        action.create(name, Some(actionName), memory = Some(allowedMemory))\n    }\n\n    for (a <- 1 to 10) {\n      val run = wsk.action.invoke(name, Map(\"payload\" -> \"128\".toJson), blocking = true)\n      withActivation(wsk.activation, run) { response =>\n        response.response.status shouldBe \"success\"\n        response.response.result shouldBe Some(JsObject(\"msg\" -> \"OK, buffer of size 128 MB has been filled.\".toJson))\n      }\n    }\n  }\n\n  it should \"be able to run a memory intensive actions\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"TestNodeJsInvokeHighMemory\"\n    val allowedMemory = MemoryLimit.MAX_MEMORY\n    assetHelper.withCleaner(wsk.action, name, confirmDelete = true) {\n      val actionName = TestUtils.getTestActionFilename(\"memoryWithGC.js\")\n      (action, _) =>\n        action.create(name, Some(actionName), memory = Some(allowedMemory))\n    }\n    // Don't try to allocate all the memory on invoking the action, as the maximum memory is set for the whole container\n    // and not only for the user action.\n    val run = wsk.action.invoke(name, Map(\"payload\" -> (allowedMemory.toMB - 56).toJson))\n    withActivation(wsk.activation, run) { response =>\n      response.response.status shouldBe \"success\"\n    }\n  }\n\n  it should \"be aborted when exceeding its memory limits\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"TestNodeJsMemoryExceeding\"\n    assetHelper.withCleaner(wsk.action, name, confirmDelete = true) {\n      val allowedMemory = MemoryLimit.MIN_MEMORY\n      val actionName = TestUtils.getTestActionFilename(\"memoryWithGC.js\")\n      (action, _) =>\n        action.create(name, Some(actionName), memory = Some(allowedMemory))\n    }\n\n    val payload = MemoryLimit.MIN_MEMORY.toMB * 2\n    val run = wsk.action.invoke(name, Map(\"payload\" -> payload.toJson))\n    withActivation(wsk.activation, run) {\n      _.response.result.get.asJsObject.fields(\"error\") shouldBe Messages.memoryExhausted.toJson\n    }\n  }\n\n  /**\n   * Test that a heavy logging action is interrupted within its timeout limits.\n   */\n  it should \"interrupt the heavy logging action within its time limits\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = s\"NodeJsTestLoggingActionCausingTimeout-${System.currentTimeMillis()}\"\n      assetHelper.withCleaner(wsk.action, name, confirmDelete = true) { (action, _) =>\n        action.create(\n          name,\n          Some(TestUtils.getTestActionFilename(\"loggingTimeout.js\")),\n          timeout = Some(allowedActionDuration))\n      }\n      val duration = allowedActionDuration + 3.minutes\n      val checkDuration = allowedActionDuration + 1.minutes\n      val run =\n        wsk.action.invoke(name, Map(\"durationMillis\" -> duration.toMillis.toJson, \"delayMillis\" -> 100.toJson))\n      withActivation(wsk.activation, run) { result =>\n        withClue(\"Activation result not as expected:\") {\n          result.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n          result.response.result.get.asJsObject\n            .fields(\"error\") shouldBe Messages.timedoutActivation(allowedActionDuration, init = false).toJson\n          val logs = result.logs.get\n          logs.last should include(Messages.logWarningDeveloperError)\n\n          val parseLogTime = (line: String) => Instant.parse(line.split(' ').head)\n          val startTime = parseLogTime(logs.head)\n          val endTime = parseLogTime(logs.last)\n          between(startTime, endTime).toMillis should be < checkDuration.toMillis\n        }\n      }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/limits/ConcurrencyTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.limits\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes\nimport common._\nimport common.rest.WskRestOperations\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.ContainerPoolConfig\nimport org.apache.openwhisk.core.entity.MemoryLimit\nimport org.apache.openwhisk.core.entity.size._\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.concurrent.Await\nimport scala.concurrent.Future\nimport scala.concurrent.duration.DurationInt\nimport spray.json.DefaultJsonProtocol._\nimport spray.json.JsObject\nimport spray.json._\n\n@RunWith(classOf[JUnitRunner])\nclass ConcurrencyTests extends TestHelpers with WskTestHelpers with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  val wsk = new WskRestOperations\n\n  //This action will receive concurrent activation requests, and not return results (for any of the pending activations)\n  //until a specified number of activations (using param requestCount) are received at the same container.\n  val concurrentAction = TestUtils.getTestActionFilename(\"concurrent.js\")\n\n  //NOTE: this test will only succeed if:\n  // whisk.container-pool.pekko-client = \"true\" (only the pekko client properly handles concurrent requests to action containers)\n  // whisk.container-factory.container-args.extra-args.env.0 = \"__OW_ALLOW_CONCURRENT=true\" (only action containers that tolerate concurrency can be tested - this enables concurrency in nodejs runtime)\n\n  behavior of \"Action concurrency limits\"\n\n  //This tests generates a concurrent load against the concurrent.js action with concurrency set to 5\n  it should \"execute activations concurrently when concurrency > 1 \" in withAssetCleaner(wskprops) {\n    assume(Option(WhiskProperties.getProperty(\"whisk.action.concurrency\", \"False\")).exists(_.toBoolean))\n\n    (wp, assetHelper) =>\n      val name = \"TestConcurrentAction\"\n      assetHelper.withCleaner(wsk.action, name, confirmDelete = true) {\n        val actionName = TestUtils.getTestActionFilename(\"concurrent.js\")\n        (action, _) =>\n          //disable log collection since concurrent activation requires specialized log processing\n          // (at action runtime and using specialized LogStore)\n          action.create(name, Some(actionName), logsize = Some(0.bytes), concurrency = Some(5))\n      }\n      //warm the container (concurrent activations with no warmed container, will cause multiple containers to be used - so we force one to warm up)\n      val run = wsk.action.invoke(name, Map(\"warm\" -> 1.toJson), blocking = true)\n      withActivation(wsk.activation, run) { response =>\n        val logs = response.logs.get\n        withClue(logs) { logs.size shouldBe 0 }\n\n        response.response.status shouldBe \"success\"\n        response.response.result shouldBe Some(JsObject(\"warm\" -> 1.toJson))\n      }\n\n      //read configs to determine max concurrency support - currently based on single invoker and invokerUserMemory config\n      val busyThreshold =\n        (loadConfigOrThrow[ContainerPoolConfig](ConfigKeys.containerPool).userMemory / MemoryLimit.STD_MEMORY).toInt\n\n      //run maximum allowed concurrent actions via Futures\n      val requestCount = busyThreshold\n      println(s\"executing $requestCount activations\")\n      val runs = (1 to requestCount).map { _ =>\n        Future {\n          //within the action, return (Promise.resolve) only after receiving $requestCount activations\n          wsk.action.invoke(name, Map(\"requestCount\" -> requestCount.toJson), blocking = true)\n        }\n      }\n\n      //none of the actions will complete till the requestCount is reached\n      Await.result(Future.sequence(runs), 30.seconds).foreach { run =>\n        withActivation(wsk.activation, run) { response =>\n          val logs = response.logs.get\n          withClue(logs) { logs.size shouldBe 0 }\n          response.response.status shouldBe \"success\"\n          //expect exactly $requestCount activations concurrently\n          response.response.result shouldBe Some(JsObject(\"msg\" -> s\"Received $requestCount activations.\".toJson))\n        }\n      }\n  }\n\n  it should \"allow concurrent activations to gracefully complete when one fails\" in withAssetCleaner(wskprops) {\n    assume(Option(WhiskProperties.getProperty(\"whisk.action.concurrency\", \"False\")).exists(_.toBoolean))\n    (wp, assetHelper) =>\n      val name = \"TestFailingConcurrentAction1\"\n      assetHelper.withCleaner(wsk.action, name, confirmDelete = true) {\n        //this action fails by returning an empty promise\n        val actionName = TestUtils.getTestActionFilename(\"concurrentFail1.js\")\n        (action, _) =>\n          //disable log collection since concurrent activation requires specialized log processing\n          // (at action runtime and using specialized LogStore)\n          action.create(name, Some(actionName), logsize = Some(0.bytes), concurrency = Some(2))\n      }\n      //with concurrency 2, at least some of the 3 activations will fail, but not all\n      val requestCount = 3\n      println(s\"executing $requestCount activations\")\n      val runs = (1 to requestCount).map { i =>\n        Future {\n          //within the action, return empty promise on one specific invocation\n          val params: Map[String, JsValue] = if (i == 2) {\n            Map(\"fail\" -> true.toJson)\n          } else {\n            Map.empty\n          }\n          val result = wsk.action.invoke(name, params, blocking = true, expectedExitCode = TestUtils.DONTCARE_EXIT)\n          result\n        }\n      }\n      val results = Await.result(Future.sequence(runs), 30.seconds)\n      //some will be 200, some will be 400, but all should be completed (no forced acks that take > 30s)\n      results.count(_.statusCode == StatusCodes.OK) should be > 0\n      results.count(_.statusCode == StatusCodes.BadGateway) should be > 0\n  }\n  it should \"allow concurrent activations to gracefully complete when one fails catastrophically\" in withAssetCleaner(\n    wskprops) {\n    assume(Option(WhiskProperties.getProperty(\"whisk.action.concurrency\", \"False\")).exists(_.toBoolean))\n    (wp, assetHelper) =>\n      val name = \"TestFailingConcurrentAction2\"\n      assetHelper.withCleaner(wsk.action, name, confirmDelete = true) {\n        //this action does a process.exit() on the 5th activation\n        val actionName = TestUtils.getTestActionFilename(\"concurrentFail2.js\")\n        (action, _) =>\n          //disable log collection since concurrent activation requires specialized log processing\n          // (at action runtime and using specialized LogStore)\n          action.create(name, Some(actionName), logsize = Some(0.bytes), concurrency = Some(2))\n      }\n      //we'll make every container fail every other activation, so with at least 2 to each container, all will fail\n      val requestCount = 4\n      println(s\"executing $requestCount activations\")\n      val runs = (1 to requestCount).map { i =>\n        Future {\n          //within the action, exite the nodejs process on second invocation\n          //use default params\n          val result = wsk.action.invoke(name, blocking = true, expectedExitCode = TestUtils.DONTCARE_EXIT)\n          result\n        }\n      }\n      val results = Await.result(Future.sequence(runs), 30.seconds)\n      //some will no 200, since each each container gets at least 5 concurrent activations,\n      //and each container crashes on the 5 activation.\n      results.count(_.statusCode == StatusCodes.OK) shouldBe 0\n      results.count(_.statusCode == StatusCodes.BadGateway) shouldBe 4\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/limits/MaxActionDurationTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.limits\n\nimport java.io.File\n\nimport scala.concurrent.duration.DurationInt\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common.{ConcurrencyHelpers, TestHelpers, TestUtils, WskActorSystem, WskProps, WskTestHelpers}\nimport common.rest.WskRestOperations\nimport org.apache.openwhisk.core.entity._\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.core.entity.TimeLimit\nimport org.scalatest.tagobjects.Slow\n\n/**\n * Tests for action duration limits. These tests require a deployed backend.\n */\n@RunWith(classOf[JUnitRunner])\nclass MaxActionDurationTests extends TestHelpers with WskTestHelpers with WskActorSystem with ConcurrencyHelpers {\n\n  implicit val wskprops = WskProps()\n  val wsk = new WskRestOperations\n\n  /**\n   * Purpose of the following integration test is to verify that the action proxy\n   * supports the configured maximum action time limit and does not interrupt a\n   * running action before the invoker does.\n   *\n   * Action proxies have to run actions potentially endlessly. It's the invoker's\n   * duty to enforce action time limits.\n   *\n   * Background: in the past, the Node.js action proxy terminated an action\n   * before it actually reached its maximum action time limit.\n   *\n   * Swift is not tested because it uses the same action proxy as Python.\n   *\n   * ATTENTION: this test runs for at least TimeLimit.MAX_DURATION + 1 minute.\n   * With default settings, this is around 6 minutes.\n   */\n  \"node-, python, and java-action\" should s\"run up to the max allowed duration (${TimeLimit.MAX_DURATION})\" taggedAs (Slow) in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    // When you add more runtimes, keep in mind, how many actions can be processed in parallel by the Invokers!\n    val runtimes = Map(\"node\" -> \"helloDeadline.js\", \"python\" -> \"sleep.py\", \"java\" -> \"sleep.jar\")\n      .filter {\n        case (_, name) =>\n          new File(TestUtils.getTestActionFilename(name)).exists()\n      }\n\n    concurrently(runtimes.toSeq, TimeLimit.MAX_DURATION + 2.minutes) {\n      case (k, name) =>\n        println(s\"Testing action kind '${k}' with action '${name}'\")\n        assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n          val main = if (k == \"java\") Some(\"Sleep\") else None\n          action.create(\n            name,\n            Some(TestUtils.getTestActionFilename(name)),\n            timeout = Some(TimeLimit.MAX_DURATION),\n            main = main)\n        }\n\n        val run = wsk.action.invoke(\n          name,\n          Map(\"forceHang\" -> true.toJson, \"sleepTimeInMs\" -> (TimeLimit.MAX_DURATION + 30.seconds).toMillis.toJson))\n\n        withActivation(\n          wsk.activation,\n          run,\n          initialWait = 1.minute,\n          pollPeriod = 1.minute,\n          totalWait = TimeLimit.MAX_DURATION + 2.minutes) { activation =>\n          withClue(\"Activation result not as expected:\") {\n            activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n            activation.response.result shouldBe Some(\n              JsObject(\"error\" -> Messages.timedoutActivation(TimeLimit.MAX_DURATION, init = false).toJson))\n            activation.duration.toInt should be >= TimeLimit.MAX_DURATION.toMillis.toInt\n          }\n        }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/FPCPoolBalancerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer.test\n\nimport org.apache.pekko.actor.{ActorRef, ActorRefFactory, ActorSystem, Props}\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.testkit.TestProbe\nimport common.{StreamLogging, WskActorSystem}\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Offline, Unhealthy}\nimport org.apache.openwhisk.common.{InvokerHealth, Logging, TransactionId}\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.connector.test.TestConnector\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.apache.openwhisk.core.etcd.EtcdKV.{InvokerKeys, ThrottlingKeys}\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdConfig}\nimport org.apache.openwhisk.core.loadBalancer.{FPCPoolBalancer, FeedFactory, ShardingContainerPoolBalancerConfig}\nimport org.apache.openwhisk.core.scheduler.{SchedulerEndpoints, SchedulerStates}\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.apache.openwhisk.utils.{retry => utilRetry}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport pureconfig._\nimport pureconfig.generic.auto._\n\nimport scala.collection.mutable.ListBuffer\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContextExecutor, Future}\nimport scala.language.postfixOps\nimport scala.util.Try\n\n@RunWith(classOf[JUnitRunner])\nclass FPCPoolBalancerTests\n    extends AnyFlatSpecLike\n    with Matchers\n    with StreamLogging\n    with ExecHelpers\n    with MockFactory\n    with ScalaFutures\n    with WskActorSystem\n    with BeforeAndAfterEach\n    with DbUtils {\n\n  private implicit val transId = TransactionId.testing\n  implicit val ece: ExecutionContextExecutor = actorSystem.dispatcher\n  private val etcd = EtcdClient(loadConfigOrThrow[EtcdConfig](ConfigKeys.etcd))\n\n  private val testInvocationNamespace = \"test-invocation-namespace\"\n\n  private var httpBound: Option[Http.ServerBinding] = None\n\n  override def afterAll(): Unit = {\n    httpBound.foreach(_.unbind())\n    etcd.close()\n    super.afterAll()\n  }\n\n  private val whiskConfig = new WhiskConfig(ExecManifest.requiredProperties)\n  private def feedProbe(connector: Option[TestConnector] = None) = new FeedFactory {\n    def createFeed(f: ActorRefFactory, m: MessagingProvider, p: (Array[Byte]) => Future[Unit]): ActorRef =\n      connector\n        .map { c =>\n          f.actorOf(Props {\n            new MessageFeed(\"activeack\", logging, c, 128, 1.second, p)\n          })\n        }\n        .getOrElse(TestProbe().testActor)\n  }\n\n  private val lbConfig: ShardingContainerPoolBalancerConfig =\n    loadConfigOrThrow[ShardingContainerPoolBalancerConfig](ConfigKeys.loadbalancer)\n\n  private def fakeMessageProvider(consumer: TestConnector): MessagingProvider = {\n    new MessagingProvider {\n      override def getConsumer(\n        whiskConfig: WhiskConfig,\n        groupId: String,\n        topic: String,\n        maxPeek: Int,\n        maxPollInterval: FiniteDuration)(implicit logging: Logging, actorSystem: ActorSystem): MessageConsumer =\n        consumer\n\n      override def getProducer(config: WhiskConfig, maxRequestSize: Option[ByteSize])(\n        implicit logging: Logging,\n        actorSystem: ActorSystem): MessageProducer = consumer.getProducer()\n\n      override def ensureTopic(config: WhiskConfig,\n                               topic: String,\n                               topicConfig: String,\n                               maxMessageBytes: Option[ByteSize])(implicit logging: Logging): Try[Unit] = Try {}\n    }\n  }\n\n  private val etcdDocsToDelete = ListBuffer[(EtcdClient, String)]()\n  private def etcdPut(etcdClient: EtcdClient, key: String, value: String) = {\n    etcdDocsToDelete += ((etcdClient, key))\n    Await.result(etcdClient.put(key, value), 10.seconds)\n  }\n\n  override def afterEach(): Unit = {\n    etcdDocsToDelete.map { etcd =>\n      Try {\n        Await.result(etcd._1.del(etcd._2), 10.seconds)\n      }\n    }\n    etcdDocsToDelete.clear()\n    cleanup()\n    super.afterEach()\n  }\n\n  it should \"watch the throttler flag from ETCD, and keep them in memory\" in {\n    val mockConsumer = new TestConnector(\"fake\", 4, true)\n    val messageProvider = fakeMessageProvider(mockConsumer)\n    val poolBalancer =\n      new FPCPoolBalancer(whiskConfig, ControllerInstanceId(\"0\"), etcd, feedProbe(), lbConfig, messageProvider)\n    val action = FullyQualifiedEntityName(EntityPath(\"testns/pkg\"), EntityName(\"action\"))\n\n    val actionKey = ThrottlingKeys.action(testInvocationNamespace, action)\n    val namespaceKey = ThrottlingKeys.namespace(EntityName(testInvocationNamespace))\n\n    Thread.sleep(1000) // wait for the watcher active\n\n    // set the throttle flag to true for action, the checkThrottle should return true\n    etcdPut(etcd, actionKey, \"true\")\n    utilRetry(\n      poolBalancer.checkThrottle(EntityPath(testInvocationNamespace), action.fullPath.asString) shouldBe true,\n      10)\n\n    // set the throttle flag to false for action, the checkThrottle should return false\n    etcdPut(etcd, actionKey, \"false\")\n    utilRetry(\n      poolBalancer.checkThrottle(EntityPath(testInvocationNamespace), action.fullPath.asString) shouldBe false,\n      10)\n\n    // set the throttle flag to true for action's namespace, the checkThrottle should still return false\n    etcdPut(etcd, namespaceKey, \"true\")\n    utilRetry(\n      poolBalancer.checkThrottle(EntityPath(testInvocationNamespace), action.fullPath.asString) shouldBe false,\n      10)\n\n    // delete the action throttle flag, then the checkThrottle should return true\n    Await.result(etcd.del(actionKey), 10.seconds)\n    utilRetry(\n      poolBalancer.checkThrottle(EntityPath(testInvocationNamespace), action.fullPath.asString) shouldBe true,\n      10)\n\n    // set the throttle flag to false for action's namespace, the checkThrottle should return false\n    etcdPut(etcd, namespaceKey, \"false\")\n    utilRetry(\n      poolBalancer.checkThrottle(EntityPath(testInvocationNamespace), action.fullPath.asString) shouldBe false,\n      10)\n\n    // delete the namespace throttle flag, the checkThrottle should return false\n    Await.result(etcd.del(namespaceKey), 10.seconds)\n    utilRetry(\n      poolBalancer.checkThrottle(EntityPath(testInvocationNamespace), action.fullPath.asString) shouldBe false,\n      10)\n  }\n\n  it should \"return the InvokerHealth\" in {\n    val mockConsumer = new TestConnector(\"fake\", 4, true)\n    val messageProvider = fakeMessageProvider(mockConsumer)\n    val poolBalancer =\n      new FPCPoolBalancer(whiskConfig, ControllerInstanceId(\"0\"), etcd, feedProbe(), lbConfig, messageProvider)\n    val invokers = IndexedSeq(\n      InvokerInstanceId(0, Some(\"0\"), userMemory = 0 bytes),\n      InvokerInstanceId(1, Some(\"1\"), userMemory = 0 bytes),\n      InvokerInstanceId(2, Some(\"2\"), userMemory = 0 bytes))\n\n    val resource1 = InvokerResourceMessage(Healthy.asString, 0, 0, 0, Seq.empty[String], Seq.empty[String])\n    val resource2 = InvokerResourceMessage(Unhealthy.asString, 0, 0, 0, Seq.empty[String], Seq.empty[String])\n    val resource3 = InvokerResourceMessage(Offline.asString, 0, 0, 0, Seq.empty[String], Seq.empty[String])\n\n    etcdPut(etcd, InvokerKeys.health(invokers(0)), resource1.serialize)\n    etcdPut(etcd, InvokerKeys.health(invokers(1)), resource2.serialize)\n    etcdPut(etcd, InvokerKeys.health(invokers(2)), resource3.serialize)\n\n    val expectedHealth = IndexedSeq(\n      InvokerHealth(invokers(0), Healthy),\n      InvokerHealth(invokers(1), Unhealthy),\n      InvokerHealth(invokers(2), Offline))\n\n    poolBalancer\n      .invokerHealth()\n      .futureValue\n      .map(i => i.id.toString -> i.status.asString) should contain theSameElementsAs expectedHealth.map(i =>\n      i.id.toString -> i.status.asString)\n  }\n\n  it should \"return Offline for missing invokers\" in {\n    val mockConsumer = new TestConnector(\"fake\", 4, true)\n    val messageProvider = fakeMessageProvider(mockConsumer)\n    val poolBalancer =\n      new FPCPoolBalancer(whiskConfig, ControllerInstanceId(\"0\"), etcd, feedProbe(), lbConfig, messageProvider)\n    val invokers = IndexedSeq(\n      InvokerInstanceId(0, Some(\"0\"), userMemory = 0 bytes),\n      InvokerInstanceId(1, Some(\"1\"), userMemory = 0 bytes),\n      InvokerInstanceId(2, Some(\"2\"), userMemory = 0 bytes),\n      InvokerInstanceId(3, Some(\"3\"), userMemory = 0 bytes),\n      InvokerInstanceId(4, Some(\"4\"), userMemory = 0 bytes),\n      InvokerInstanceId(5, Some(\"5\"), userMemory = 0 bytes))\n\n    val resource1 = InvokerResourceMessage(Healthy.asString, 0, 0, 0, Seq.empty[String], Seq.empty[String])\n    val resource2 = InvokerResourceMessage(Unhealthy.asString, 0, 0, 0, Seq.empty[String], Seq.empty[String])\n\n    etcdPut(etcd, InvokerKeys.health(invokers(0)), resource1.serialize)\n    etcdPut(etcd, InvokerKeys.health(invokers(5)), resource2.serialize)\n\n    val expectedHealth = IndexedSeq(\n      InvokerHealth(invokers(0), Healthy),\n      InvokerHealth(invokers(1), Offline),\n      InvokerHealth(invokers(2), Offline),\n      InvokerHealth(invokers(3), Offline),\n      InvokerHealth(invokers(4), Offline),\n      InvokerHealth(invokers(5), Unhealthy))\n\n    poolBalancer\n      .invokerHealth()\n      .futureValue\n      .map(i => i.id.toString -> i.status.asString) should contain theSameElementsAs expectedHealth.map(i =>\n      i.id.toString -> i.status.asString)\n  }\n\n  it should \"loads scheduler endpoints from specified clusterName only\" in {\n    val host = \"127.0.0.1\"\n    val rpcPort1 = 19090\n    val rpcPort2 = 19091\n    val rpcPort3 = 19092\n    val rpcPort4 = 19090\n    val rpcPort5 = 19091\n    val rpcPort6 = 19092\n    val pekkoPort = 0\n    val mockConsumer = new TestConnector(\"fake\", 4, true)\n    val messageProvider = fakeMessageProvider(mockConsumer)\n    val clusterName1 = loadConfigOrThrow[String](ConfigKeys.whiskClusterName)\n    val clusterName2 = \"clusterName2\"\n\n    etcd.put(\n      s\"$clusterName1/scheduler/0\",\n      SchedulerStates(SchedulerInstanceId(\"0\"), queueSize = 0, SchedulerEndpoints(host, rpcPort1, pekkoPort)).serialize)\n    etcd.put(\n      s\"$clusterName1/scheduler/1\",\n      SchedulerStates(SchedulerInstanceId(\"1\"), queueSize = 0, SchedulerEndpoints(host, rpcPort2, pekkoPort)).serialize)\n    etcd.put(\n      s\"$clusterName1/scheduler/2\",\n      SchedulerStates(SchedulerInstanceId(\"2\"), queueSize = 0, SchedulerEndpoints(host, rpcPort3, pekkoPort)).serialize)\n    etcd.put(\n      s\"$clusterName2/scheduler/3\",\n      SchedulerStates(SchedulerInstanceId(\"3\"), queueSize = 0, SchedulerEndpoints(host, rpcPort4, pekkoPort)).serialize)\n    etcd.put(\n      s\"$clusterName2/scheduler/4\",\n      SchedulerStates(SchedulerInstanceId(\"4\"), queueSize = 0, SchedulerEndpoints(host, rpcPort5, pekkoPort)).serialize)\n    etcd.put(\n      s\"$clusterName2/scheduler/5\",\n      SchedulerStates(SchedulerInstanceId(\"5\"), queueSize = 0, SchedulerEndpoints(host, rpcPort6, pekkoPort)).serialize)\n    val poolBalancer =\n      new FPCPoolBalancer(\n        whiskConfig,\n        ControllerInstanceId(\"0\"),\n        etcd,\n        feedProbe(),\n        lbConfig,\n        messagingProvider = messageProvider)\n    // Make sure poolBalancer instance is initialized\n    Thread.sleep(5.seconds.toMillis)\n    poolBalancer.getSchedulerEndpoint().toList.length shouldBe 3\n    poolBalancer.getSchedulerEndpoint().values.foreach { scheduler =>\n      List(\"scheduler0\", \"scheduler1\", \"scheduler2\") should contain(scheduler.sid.toString)\n    }\n    // Delete etcd data finally\n    List(\n      s\"$clusterName1/scheduler/0\",\n      s\"$clusterName1/scheduler/1\",\n      s\"$clusterName1/scheduler/2\",\n      s\"$clusterName2/scheduler/3\",\n      s\"$clusterName2/scheduler/4\",\n      s\"$clusterName2/scheduler/5\").foreach(etcd.del)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/InvokerSupervisionTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer.test\n\nimport scala.collection.mutable\nimport scala.concurrent.Await\nimport scala.concurrent.duration._\nimport scala.concurrent.Future\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.pekko.actor.ActorRefFactory\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.actor.FSM\nimport org.apache.pekko.actor.FSM.CurrentState\nimport org.apache.pekko.actor.FSM.SubscribeTransitionCallBack\nimport org.apache.pekko.actor.FSM.Transition\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.testkit.ImplicitSender\nimport org.apache.pekko.testkit.TestFSMRef\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.pekko.testkit.TestProbe\nimport org.apache.pekko.util.Timeout\nimport common.{LoggedFunction, StreamLogging}\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Offline, Unhealthy, Unresponsive}\nimport org.apache.openwhisk.common.{InvokerHealth, InvokerState, TransactionId}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.{ActivationMessage, PingMessage, ResultMetadata}\nimport org.apache.openwhisk.core.entity.ActivationId.ActivationIdGenerator\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.loadBalancer.ActivationRequest\nimport org.apache.openwhisk.core.loadBalancer.GetStatus\nimport org.apache.openwhisk.core.loadBalancer.InvocationFinishedResult\nimport org.apache.openwhisk.core.loadBalancer.InvocationFinishedMessage\nimport org.apache.openwhisk.core.loadBalancer.InvokerActor\nimport org.apache.openwhisk.core.loadBalancer.InvokerPool\nimport org.apache.openwhisk.utils.retry\nimport org.apache.openwhisk.core.connector.test.TestConnector\nimport org.apache.openwhisk.core.entity.ControllerInstanceId\n\n@RunWith(classOf[JUnitRunner])\nclass InvokerSupervisionTests\n    extends TestKit(ActorSystem(\"InvokerSupervision\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with MockFactory\n    with StreamLogging {\n\n  val config = new WhiskConfig(ExecManifest.requiredProperties)\n  val defaultUserMemory: ByteSize = 1024.MB\n\n  ExecManifest.initialize(config)\n\n  override def afterAll: Unit = {\n    TestKit.shutdownActorSystem(system)\n  }\n\n  implicit val timeout = Timeout(5.seconds)\n\n  /** Imitates a StateTimeout in the FSM */\n  def timeout(actor: ActorRef) = actor ! FSM.StateTimeout\n\n  /** Queries all invokers for their state */\n  def allStates(pool: ActorRef) =\n    Await.result(pool.ask(GetStatus).mapTo[IndexedSeq[(InvokerInstanceId, InvokerState)]], timeout.duration)\n\n  /** Helper to generate a list of (InstanceId, InvokerState) */\n  def zipWithInstance(list: IndexedSeq[InvokerState]) = list.zipWithIndex.map {\n    case (state, index) => new InvokerHealth(InvokerInstanceId(index, userMemory = defaultUserMemory), state)\n  }\n\n  val pC = new TestConnector(\"pingFeedTtest\", 4, false) {}\n\n  behavior of \"InvokerPool\"\n\n  it should \"successfully create invokers in its pool on ping and keep track of statechanges\" in {\n    val invoker5 = TestProbe()\n    val invoker2 = TestProbe()\n\n    val invoker5Instance = InvokerInstanceId(5, userMemory = defaultUserMemory)\n    val invoker2Instance = InvokerInstanceId(2, userMemory = defaultUserMemory)\n\n    val children = mutable.Queue(invoker5.ref, invoker2.ref)\n    val childFactory = (f: ActorRefFactory, instance: InvokerInstanceId) => children.dequeue()\n\n    val sendActivationToInvoker = stubFunction[ActivationMessage, InvokerInstanceId, Future[ResultMetadata]]\n    val supervisor = system.actorOf(InvokerPool.props(childFactory, sendActivationToInvoker, pC))\n\n    within(timeout.duration) {\n      // create first invoker\n      val ping0 = PingMessage(invoker5Instance)\n      supervisor ! ping0\n      invoker5.expectMsgType[SubscribeTransitionCallBack] // subscribe to the actor\n      invoker5.expectMsg(ping0)\n\n      invoker5.send(supervisor, CurrentState(invoker5.ref, Healthy))\n      allStates(supervisor) shouldBe zipWithInstance(IndexedSeq(Offline, Offline, Offline, Offline, Offline, Healthy))\n\n      // create second invoker\n      val ping1 = PingMessage(invoker2Instance)\n      supervisor ! ping1\n      invoker2.expectMsgType[SubscribeTransitionCallBack]\n      invoker2.expectMsg(ping1)\n\n      invoker2.send(supervisor, CurrentState(invoker2.ref, Healthy))\n      allStates(supervisor) shouldBe zipWithInstance(IndexedSeq(Offline, Offline, Healthy, Offline, Offline, Healthy))\n\n      // ping the first invoker again\n      supervisor ! ping0\n      invoker5.expectMsg(ping0)\n\n      allStates(supervisor) shouldBe zipWithInstance(IndexedSeq(Offline, Offline, Healthy, Offline, Offline, Healthy))\n\n      // one invoker goes offline\n      invoker2.send(supervisor, Transition(invoker2.ref, Healthy, Offline))\n      allStates(supervisor) shouldBe zipWithInstance(IndexedSeq(Offline, Offline, Offline, Offline, Offline, Healthy))\n    }\n  }\n\n  it should \"forward the ActivationResult to the appropriate invoker\" in {\n    val invoker = TestProbe()\n    val invokerInstance = InvokerInstanceId(0, userMemory = defaultUserMemory)\n    val invokerName = s\"invoker${invokerInstance.toInt}\"\n    val childFactory = (f: ActorRefFactory, instance: InvokerInstanceId) => invoker.ref\n    val sendActivationToInvoker = stubFunction[ActivationMessage, InvokerInstanceId, Future[ResultMetadata]]\n\n    val supervisor = system.actorOf(InvokerPool.props(childFactory, sendActivationToInvoker, pC))\n\n    within(timeout.duration) {\n      // Create one invoker\n      val ping0 = PingMessage(invokerInstance)\n      supervisor ! ping0\n      invoker.expectMsgType[SubscribeTransitionCallBack] // subscribe to the actor\n      invoker.expectMsg(ping0)\n      invoker.send(supervisor, CurrentState(invoker.ref, Healthy))\n      allStates(supervisor) shouldBe zipWithInstance(IndexedSeq(Healthy))\n\n      // Send message and expect receive in invoker\n      val msg = InvocationFinishedMessage(invokerInstance, InvocationFinishedResult.Success)\n      supervisor ! msg\n      invoker.expectMsg(msg)\n    }\n  }\n\n  it should \"forward an ActivationMessage to the sendActivation-Method\" in {\n    val invoker = TestProbe()\n    val invokerInstance = InvokerInstanceId(0, userMemory = defaultUserMemory)\n    val invokerName = s\"invoker${invokerInstance.toInt}\"\n    val childFactory = (f: ActorRefFactory, instance: InvokerInstanceId) => invoker.ref\n\n    val sendActivationToInvoker = LoggedFunction { (a: ActivationMessage, b: InvokerInstanceId) =>\n      Future.successful(ResultMetadata(invokerName, 0, 0))\n    }\n\n    val supervisor = system.actorOf(InvokerPool.props(childFactory, sendActivationToInvoker, pC))\n\n    // Send ActivationMessage to InvokerPool\n    val uuid = UUID()\n    val activationMessage = ActivationMessage(\n      transid = TransactionId.invokerHealth,\n      action = FullyQualifiedEntityName(EntityPath(\"whisk.system/utils\"), EntityName(\"date\")),\n      revision = DocRevision.empty,\n      user = Identity(\n        Subject(\"unhealthyInvokerCheck\"),\n        Namespace(EntityName(\"unhealthyInvokerCheck\"), uuid),\n        BasicAuthenticationAuthKey(uuid, Secret())),\n      activationId = new ActivationIdGenerator {}.make(),\n      rootControllerIndex = ControllerInstanceId(\"0\"),\n      blocking = false,\n      content = None,\n      initArgs = Set.empty,\n      lockedArgs = Map.empty)\n    val msg = ActivationRequest(activationMessage, invokerInstance)\n\n    supervisor ! msg\n\n    // Verify, that MessageProducer will receive a call to send the message\n    retry(sendActivationToInvoker.calls should have size 1, N = 3, waitBeforeRetry = Some(500.milliseconds))\n  }\n\n  behavior of \"InvokerActor\"\n\n  it should \"start and stay unhealthy while min threshold is not met\" in {\n    val invoker =\n      TestFSMRef(new InvokerActor(InvokerInstanceId(0, userMemory = defaultUserMemory), ControllerInstanceId(\"0\")))\n    invoker.stateName shouldBe Unhealthy\n\n    (1 to InvokerActor.bufferErrorTolerance + 1).foreach { _ =>\n      invoker ! InvocationFinishedMessage(\n        InvokerInstanceId(0, userMemory = defaultUserMemory),\n        InvocationFinishedResult.SystemError)\n      invoker.stateName shouldBe Unhealthy\n    }\n\n    (1 to InvokerActor.bufferSize - InvokerActor.bufferErrorTolerance - 1).foreach { _ =>\n      invoker ! InvocationFinishedMessage(\n        InvokerInstanceId(0, userMemory = defaultUserMemory),\n        InvocationFinishedResult.Success)\n      invoker.stateName shouldBe Unhealthy\n    }\n\n    invoker ! InvocationFinishedMessage(\n      InvokerInstanceId(0, userMemory = defaultUserMemory),\n      InvocationFinishedResult.Success)\n    invoker.stateName shouldBe Healthy\n  }\n\n  it should \"revert to unhealthy during initial startup if there is a failed test activation\" in {\n    assume(InvokerActor.bufferErrorTolerance >= 3)\n\n    val invoker =\n      TestFSMRef(new InvokerActor(InvokerInstanceId(0, userMemory = defaultUserMemory), ControllerInstanceId(\"0\")))\n    invoker.stateName shouldBe Unhealthy\n\n    invoker ! InvocationFinishedMessage(\n      InvokerInstanceId(0, userMemory = defaultUserMemory),\n      InvocationFinishedResult.SystemError)\n    invoker.stateName shouldBe Unhealthy\n\n    invoker ! InvocationFinishedMessage(\n      InvokerInstanceId(0, userMemory = defaultUserMemory),\n      InvocationFinishedResult.Success)\n    invoker.stateName shouldBe Healthy\n\n    invoker ! InvocationFinishedMessage(\n      InvokerInstanceId(0, userMemory = defaultUserMemory),\n      InvocationFinishedResult.SystemError)\n    invoker.stateName shouldBe Unhealthy\n  }\n\n  // unHealthy -> offline\n  // offline -> unhealthy\n  it should \"start unhealthy, go offline if the state times out and goes unhealthy on a successful ping again\" in {\n    val pool = TestProbe()\n    val invoker =\n      pool.system.actorOf(\n        InvokerActor.props(InvokerInstanceId(0, userMemory = defaultUserMemory), ControllerInstanceId(\"0\")))\n\n    within(timeout.duration) {\n      pool.send(invoker, SubscribeTransitionCallBack(pool.ref))\n      pool.expectMsg(CurrentState(invoker, Unhealthy))\n      timeout(invoker)\n      pool.expectMsg(Transition(invoker, Unhealthy, Offline))\n\n      invoker ! PingMessage(InvokerInstanceId(0, userMemory = defaultUserMemory))\n      pool.expectMsg(Transition(invoker, Offline, Unhealthy))\n    }\n  }\n\n  // unhealthy -> healthy -> unhealthy -> healthy\n  it should \"goto healthy again, if unhealthy and error buffer has enough successful invocations\" in {\n    val pool = TestProbe()\n    val invoker =\n      pool.system.actorOf(\n        InvokerActor.props(InvokerInstanceId(0, userMemory = defaultUserMemory), ControllerInstanceId(\"0\")))\n\n    within(timeout.duration) {\n      pool.send(invoker, SubscribeTransitionCallBack(pool.ref))\n      pool.expectMsg(CurrentState(invoker, Unhealthy))\n\n      (1 to InvokerActor.bufferSize).foreach { _ =>\n        invoker ! InvocationFinishedMessage(\n          InvokerInstanceId(0, userMemory = defaultUserMemory),\n          InvocationFinishedResult.Success)\n      }\n      pool.expectMsg(Transition(invoker, Unhealthy, Healthy))\n\n      // Fill buffer with errors\n      (1 to InvokerActor.bufferSize).foreach { _ =>\n        invoker ! InvocationFinishedMessage(\n          InvokerInstanceId(0, userMemory = defaultUserMemory),\n          InvocationFinishedResult.SystemError)\n      }\n      pool.expectMsg(Transition(invoker, Healthy, Unhealthy))\n\n      // Fill buffer with successful invocations to become healthy again (one below errorTolerance)\n      (1 to InvokerActor.bufferSize - InvokerActor.bufferErrorTolerance).foreach { _ =>\n        invoker ! InvocationFinishedMessage(\n          InvokerInstanceId(0, userMemory = defaultUserMemory),\n          InvocationFinishedResult.Success)\n      }\n      pool.expectMsg(Transition(invoker, Unhealthy, Healthy))\n    }\n  }\n\n  // unhealthy -> healthy -> overloaded -> healthy\n  it should \"goto healthy again, if overloaded and error buffer has enough successful invocations\" in {\n    val pool = TestProbe()\n    val invoker =\n      pool.system.actorOf(\n        InvokerActor.props(InvokerInstanceId(0, userMemory = defaultUserMemory), ControllerInstanceId(\"0\")))\n\n    within(timeout.duration) {\n      pool.send(invoker, SubscribeTransitionCallBack(pool.ref))\n      pool.expectMsg(CurrentState(invoker, Unhealthy))\n\n      (1 to InvokerActor.bufferSize).foreach { _ =>\n        invoker ! InvocationFinishedMessage(\n          InvokerInstanceId(0, userMemory = defaultUserMemory),\n          InvocationFinishedResult.Success)\n      }\n      pool.expectMsg(Transition(invoker, Unhealthy, Healthy))\n\n      // Fill buffer with timeouts\n      (1 to InvokerActor.bufferSize).foreach { _ =>\n        invoker ! InvocationFinishedMessage(\n          InvokerInstanceId(0, userMemory = defaultUserMemory),\n          InvocationFinishedResult.Timeout)\n      }\n      pool.expectMsg(Transition(invoker, Healthy, Unresponsive))\n\n      // Fill buffer with successful invocations to become healthy again (one below errorTolerance)\n      (1 to InvokerActor.bufferSize - InvokerActor.bufferErrorTolerance).foreach { _ =>\n        invoker ! InvocationFinishedMessage(\n          InvokerInstanceId(0, userMemory = defaultUserMemory),\n          InvocationFinishedResult.Success)\n      }\n      pool.expectMsg(Transition(invoker, Unresponsive, Healthy))\n    }\n  }\n\n  // unhealthy -> offline\n  // offline -> unhealthy\n  it should \"go offline when unhealthy, if the state times out and go unhealthy on a successful ping again\" in {\n    val pool = TestProbe()\n    val invoker =\n      pool.system.actorOf(\n        InvokerActor.props(InvokerInstanceId(0, userMemory = defaultUserMemory), ControllerInstanceId(\"0\")))\n\n    within(timeout.duration) {\n      pool.send(invoker, SubscribeTransitionCallBack(pool.ref))\n      pool.expectMsg(CurrentState(invoker, Unhealthy))\n\n      timeout(invoker)\n      pool.expectMsg(Transition(invoker, Unhealthy, Offline))\n\n      invoker ! PingMessage(InvokerInstanceId(0, userMemory = defaultUserMemory))\n      pool.expectMsg(Transition(invoker, Offline, Unhealthy))\n    }\n  }\n\n  // unhealthy -> offline\n  // offline -> off\n  it should \"go offline when unhealthy and disabled invoker ping received and stay offline if disabled ping received while offline\" in {\n    val invoker =\n      TestFSMRef(new InvokerActor(InvokerInstanceId(0, userMemory = defaultUserMemory), ControllerInstanceId(\"0\")))\n    invoker.stateName shouldBe Unhealthy\n    invoker ! PingMessage(InvokerInstanceId(0, userMemory = defaultUserMemory), Some(false))\n    invoker.stateName shouldBe Offline\n    invoker ! PingMessage(InvokerInstanceId(0, userMemory = defaultUserMemory), Some(false))\n    invoker.stateName shouldBe Offline\n  }\n\n  it should \"start timer to send test actions when unhealthy\" in {\n    val invoker =\n      TestFSMRef(new InvokerActor(InvokerInstanceId(0, userMemory = defaultUserMemory), ControllerInstanceId(\"0\")))\n    invoker.stateName shouldBe Unhealthy\n\n    invoker.isTimerActive(InvokerActor.timerName) shouldBe true\n\n    // Fill buffer with successful invocations to become healthy again (one below errorTolerance)\n    (1 to InvokerActor.bufferSize - InvokerActor.bufferErrorTolerance).foreach { _ =>\n      invoker ! InvocationFinishedMessage(\n        InvokerInstanceId(0, userMemory = defaultUserMemory),\n        InvocationFinishedResult.Success)\n    }\n    invoker.stateName shouldBe Healthy\n\n    invoker.isTimerActive(InvokerActor.timerName) shouldBe false\n  }\n\n  // healthy -> offline\n  it should \"go offline from healthy immediately when disabled invoker ping received\" in {\n    val invoker =\n      TestFSMRef(new InvokerActor(InvokerInstanceId(0, userMemory = defaultUserMemory), ControllerInstanceId(\"0\")))\n    invoker.stateName shouldBe Unhealthy\n\n    invoker.isTimerActive(InvokerActor.timerName) shouldBe true\n\n    // Fill buffer with successful invocations to become healthy again (one below errorTolerance)\n    (1 to InvokerActor.bufferSize - InvokerActor.bufferErrorTolerance).foreach { _ =>\n      invoker ! InvocationFinishedMessage(\n        InvokerInstanceId(0, userMemory = defaultUserMemory),\n        InvocationFinishedResult.Success)\n    }\n    invoker.stateName shouldBe Healthy\n\n    invoker.isTimerActive(InvokerActor.timerName) shouldBe false\n\n    invoker ! PingMessage(InvokerInstanceId(0, userMemory = defaultUserMemory), Some(false))\n    invoker.stateName shouldBe Offline\n  }\n\n  it should \"initially store invoker status with its full id - instance/uniqueName/displayedName\" in {\n    val invoker0 = TestProbe()\n    val children = mutable.Queue(invoker0.ref)\n    val childFactory = (f: ActorRefFactory, instance: InvokerInstanceId) => children.dequeue()\n\n    val sendActivationToInvoker = stubFunction[ActivationMessage, InvokerInstanceId, Future[ResultMetadata]]\n    val supervisor = system.actorOf(InvokerPool.props(childFactory, sendActivationToInvoker, pC))\n\n    val invokerInstance = InvokerInstanceId(0, Some(\"10.x.x.x\"), Some(\"invoker-xyz\"), userMemory = defaultUserMemory)\n\n    within(timeout.duration) {\n\n      val ping = PingMessage(invokerInstance)\n\n      supervisor ! ping\n\n      invoker0.expectMsgType[SubscribeTransitionCallBack]\n      invoker0.expectMsg(ping)\n\n      allStates(supervisor) shouldBe IndexedSeq(new InvokerHealth(invokerInstance, Offline))\n    }\n  }\n\n  it should \"update the invoker instance id after it was restarted\" in {\n    val invoker0 = TestProbe()\n    val children = mutable.Queue(invoker0.ref)\n    val childFactory = (f: ActorRefFactory, instance: InvokerInstanceId) => children.dequeue()\n\n    val sendActivationToInvoker = stubFunction[ActivationMessage, InvokerInstanceId, Future[ResultMetadata]]\n    val supervisor = system.actorOf(InvokerPool.props(childFactory, sendActivationToInvoker, pC))\n\n    val invokerInstance = InvokerInstanceId(0, Some(\"10.x.x.x\"), Some(\"invoker-xyz\"), userMemory = defaultUserMemory)\n\n    val invokerAfterRestart =\n      InvokerInstanceId(0, Some(\"10.x.x.x\"), Some(\"invoker-zyx\"), userMemory = defaultUserMemory)\n\n    within(timeout.duration) {\n      val ping = PingMessage(invokerInstance)\n\n      supervisor ! ping\n\n      invoker0.expectMsgType[SubscribeTransitionCallBack]\n      invoker0.expectMsg(ping)\n\n      invoker0.send(supervisor, CurrentState(invoker0.ref, Unhealthy))\n\n      val newPing = PingMessage(invokerAfterRestart)\n\n      supervisor ! newPing\n\n      allStates(supervisor) shouldBe IndexedSeq(new InvokerHealth(invokerAfterRestart, Unhealthy))\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/ShardingContainerPoolBalancerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.loadBalancer.test\n\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.pekko.actor.ActorRefFactory\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.TestProbe\nimport common.{StreamLogging, WhiskProperties}\n\nimport java.nio.charset.StandardCharsets\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Offline, Unhealthy}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.concurrent.Await\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport org.apache.openwhisk.common.{InvokerHealth, Logging, NestedSemaphore, TransactionId}\nimport org.apache.openwhisk.core.entity.FullyQualifiedEntityName\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.{\n  ActivationMessage,\n  CompletionMessage,\n  Message,\n  MessageConsumer,\n  MessageProducer,\n  MessagingProvider,\n  ResultMetadata\n}\nimport org.apache.openwhisk.core.entity.ActivationId\nimport org.apache.openwhisk.core.entity.BasicAuthenticationAuthKey\nimport org.apache.openwhisk.core.entity.ControllerInstanceId\nimport org.apache.openwhisk.core.entity.EntityName\nimport org.apache.openwhisk.core.entity.EntityPath\nimport org.apache.openwhisk.core.entity.ExecManifest\nimport org.apache.openwhisk.core.entity.Identity\nimport org.apache.openwhisk.core.entity.InvokerInstanceId\nimport org.apache.openwhisk.core.entity.MemoryLimit\nimport org.apache.openwhisk.core.entity.Namespace\nimport org.apache.openwhisk.core.entity.Secret\nimport org.apache.openwhisk.core.entity.Subject\nimport org.apache.openwhisk.core.entity.UUID\nimport org.apache.openwhisk.core.entity.WhiskActionMetaData\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.apache.openwhisk.core.entity.ByteSize\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.apache.openwhisk.core.loadBalancer.FeedFactory\nimport org.apache.openwhisk.core.loadBalancer.InvokerPoolFactory\nimport org.apache.openwhisk.core.loadBalancer._\n\n/**\n * Unit tests for the ContainerPool object.\n *\n * These tests test only the \"static\" methods \"schedule\" and \"remove\"\n * of the ContainerPool object.\n */\n@RunWith(classOf[JUnitRunner])\nclass ShardingContainerPoolBalancerTests\n    extends AnyFlatSpec\n    with Matchers\n    with StreamLogging\n    with ExecHelpers\n    with MockFactory {\n  behavior of \"ShardingContainerPoolBalancerState\"\n\n  val defaultUserMemory: ByteSize = 1024.MB\n\n  def healthy(i: Int, memory: ByteSize = defaultUserMemory) =\n    new InvokerHealth(InvokerInstanceId(i, userMemory = memory), Healthy)\n  def unhealthy(i: Int) = new InvokerHealth(InvokerInstanceId(i, userMemory = defaultUserMemory), Unhealthy)\n  def offline(i: Int) = new InvokerHealth(InvokerInstanceId(i, userMemory = defaultUserMemory), Offline)\n\n  def semaphores(count: Int, max: Int): IndexedSeq[NestedSemaphore[FullyQualifiedEntityName]] =\n    IndexedSeq.fill(count)(new NestedSemaphore[FullyQualifiedEntityName](max))\n\n  def lbConfig(blackboxFraction: Double, managedFraction: Option[Double] = None) =\n    ShardingContainerPoolBalancerConfig(\n      managedFraction.getOrElse(1.0 - blackboxFraction),\n      blackboxFraction,\n      1,\n      1.minute)\n\n  it should \"update invoker's state, growing the slots data and keeping valid old data\" in {\n    // start empty\n    val slots = 10\n    val memoryPerSlot = MemoryLimit.MIN_MEMORY\n    val memory = memoryPerSlot * slots\n    val state = ShardingContainerPoolBalancerState()(lbConfig(0.5))\n    state.invokers shouldBe 'empty\n    state.blackboxInvokers shouldBe 'empty\n    state.managedInvokers shouldBe 'empty\n    state.invokerSlots shouldBe 'empty\n    state.managedStepSizes shouldBe Seq.empty\n    state.blackboxStepSizes shouldBe Seq.empty\n\n    // apply one update, verify everything is updated accordingly\n    val update1 = IndexedSeq(healthy(0, memory))\n    state.updateInvokers(update1)\n\n    state.invokers shouldBe update1\n    state.blackboxInvokers shouldBe update1 // fallback to at least one\n    state.managedInvokers shouldBe update1 // fallback to at least one\n    state.invokerSlots should have size update1.size\n    state.invokerSlots.head.availablePermits shouldBe memory.toMB\n    state.managedStepSizes shouldBe Seq(1)\n    state.blackboxStepSizes shouldBe Seq(1)\n\n    // acquire a slot to alter invoker state\n    state.invokerSlots.head.tryAcquire(memoryPerSlot.toMB.toInt)\n    state.invokerSlots.head.availablePermits shouldBe (memory - memoryPerSlot).toMB.toInt\n\n    // apply second update, growing the state\n    val update2 =\n      IndexedSeq(healthy(0, memory), healthy(1, memory * 2))\n    state.updateInvokers(update2)\n\n    state.invokers shouldBe update2\n    state.managedInvokers shouldBe IndexedSeq(update2.head)\n    state.blackboxInvokers shouldBe IndexedSeq(update2.last)\n    state.invokerSlots should have size update2.size\n    state.invokerSlots.head.availablePermits shouldBe (memory - memoryPerSlot).toMB.toInt\n    state.invokerSlots(1).tryAcquire(memoryPerSlot.toMB.toInt)\n    state.invokerSlots(1).availablePermits shouldBe memory.toMB * 2 - memoryPerSlot.toMB\n    state.managedStepSizes shouldBe Seq(1)\n    state.blackboxStepSizes shouldBe Seq(1)\n  }\n\n  it should \"allow managed partition to overlap with blackbox for small N\" in {\n    Seq(0.1, 0.2, 0.3, 0.4, 0.5).foreach { bf =>\n      val state = ShardingContainerPoolBalancerState()(lbConfig(bf))\n\n      (1 to 100).toSeq.foreach { i =>\n        state.updateInvokers((1 to i).map(_ => healthy(1, MemoryLimit.STD_MEMORY)))\n\n        withClue(s\"invoker count $bf $i:\") {\n          state.managedInvokers.length should be <= i\n          state.blackboxInvokers should have size Math.max(1, (bf * i).toInt)\n\n          val m = state.managedInvokers.length\n          val b = state.blackboxInvokers.length\n          bf match {\n            // written out explicitly for clarity\n            case 0.1 if i < 10 => m + b shouldBe i + 1\n            case 0.2 if i < 5  => m + b shouldBe i + 1\n            case 0.3 if i < 4  => m + b shouldBe i + 1\n            case 0.4 if i < 3  => m + b shouldBe i + 1\n            case 0.5 if i < 2  => m + b shouldBe i + 1\n            case _             => m + b shouldBe i\n          }\n        }\n      }\n    }\n  }\n\n  it should \"return the same pools if managed- and blackbox-pools are overlapping\" in {\n\n    val state = ShardingContainerPoolBalancerState()(lbConfig(1.0, Some(1.0)))\n    (1 to 100).foreach { i =>\n      state.updateInvokers((1 to i).map(_ => healthy(1, MemoryLimit.STD_MEMORY)))\n    }\n\n    state.managedInvokers should have size 100\n    state.blackboxInvokers should have size 100\n\n    state.managedInvokers shouldBe state.blackboxInvokers\n  }\n\n  it should \"update the cluster size, adjusting the invoker slots accordingly\" in {\n    val slots = 10\n    val memoryPerSlot = MemoryLimit.MIN_MEMORY\n    val memory = memoryPerSlot * slots\n    val state = ShardingContainerPoolBalancerState()(lbConfig(0.5))\n    state.updateInvokers(IndexedSeq(healthy(0, memory), healthy(1, memory * 2)))\n\n    state.invokerSlots.head.tryAcquire(memoryPerSlot.toMB.toInt)\n    state.invokerSlots.head.availablePermits shouldBe (memory - memoryPerSlot).toMB\n\n    state.invokerSlots(1).tryAcquire(memoryPerSlot.toMB.toInt)\n    state.invokerSlots(1).availablePermits shouldBe memory.toMB * 2 - memoryPerSlot.toMB\n\n    state.updateCluster(2)\n    state.invokerSlots.head.availablePermits shouldBe memory.toMB / 2 // state reset + divided by 2\n    state.invokerSlots(1).availablePermits shouldBe memory.toMB\n  }\n\n  it should \"fallback to a size of 1 (alone) if cluster size is < 1\" in {\n    val slots = 10\n    val memoryPerSlot = MemoryLimit.MIN_MEMORY\n    val memory = memoryPerSlot * slots\n    val state = ShardingContainerPoolBalancerState()(lbConfig(0.5))\n    state.updateInvokers(IndexedSeq(healthy(0, memory)))\n\n    state.invokerSlots.head.availablePermits shouldBe memory.toMB\n\n    state.updateCluster(2)\n    state.invokerSlots.head.availablePermits shouldBe memory.toMB / 2\n\n    state.updateCluster(0)\n    state.invokerSlots.head.availablePermits shouldBe memory.toMB\n\n    state.updateCluster(-1)\n    state.invokerSlots.head.availablePermits shouldBe memory.toMB\n  }\n\n  it should \"set the threshold to 1 if the cluster is bigger than there are slots on 1 invoker\" in {\n    val slots = 10\n    val memoryPerSlot = MemoryLimit.MIN_MEMORY\n    val memory = memoryPerSlot * slots\n    val state = ShardingContainerPoolBalancerState()(lbConfig(0.5))\n    state.updateInvokers(IndexedSeq(healthy(0, memory)))\n\n    state.invokerSlots.head.availablePermits shouldBe memory.toMB\n\n    state.updateCluster(20)\n\n    state.invokerSlots.head.availablePermits shouldBe MemoryLimit.MIN_MEMORY.toMB\n  }\n  val namespace = EntityPath(\"testspace\")\n  val name = EntityName(\"testname\")\n  val fqn = FullyQualifiedEntityName(namespace, name)\n\n  behavior of \"schedule\"\n\n  implicit val transId = TransactionId.testing\n\n  it should \"return None on an empty invoker list\" in {\n    ShardingContainerPoolBalancer.schedule(\n      1,\n      fqn,\n      IndexedSeq.empty,\n      IndexedSeq.empty,\n      MemoryLimit.MIN_MEMORY.toMB.toInt,\n      index = 0,\n      step = 2) shouldBe None\n  }\n\n  it should \"return None if no invokers are healthy\" in {\n    val invokerCount = 3\n    val invokerSlots = semaphores(invokerCount, 3)\n    val invokers = (0 until invokerCount).map(unhealthy)\n\n    ShardingContainerPoolBalancer.schedule(\n      1,\n      fqn,\n      invokers,\n      invokerSlots,\n      MemoryLimit.MIN_MEMORY.toMB.toInt,\n      index = 0,\n      step = 2) shouldBe None\n  }\n\n  it should \"choose the first available invoker, jumping in stepSize steps, falling back to randomized scheduling once all invokers are full\" in {\n    val invokerCount = 3\n    val slotPerInvoker = 3\n    val invokerSlots = semaphores(invokerCount + 3, slotPerInvoker) // needs to be offset by 3 as well\n    val invokers = (0 until invokerCount).map(i => healthy(i + 3)) // offset by 3 to asset InstanceId is returned\n\n    val expectedResult = Seq(3, 3, 3, 5, 5, 5, 4, 4, 4)\n    val result = expectedResult.map { _ =>\n      ShardingContainerPoolBalancer\n        .schedule(1, fqn, invokers, invokerSlots, 1, index = 0, step = 2)\n        .get\n        ._1\n        .toInt\n    }\n\n    result shouldBe expectedResult\n\n    val bruteResult = (0 to 100).map { _ =>\n      ShardingContainerPoolBalancer\n        .schedule(1, fqn, invokers, invokerSlots, 1, index = 0, step = 2)\n        .get\n    }\n\n    bruteResult.map(_._1.toInt) should contain allOf (3, 4, 5)\n    bruteResult.map(_._2) should contain only true\n  }\n\n  it should \"ignore unhealthy or offline invokers\" in {\n    val invokers = IndexedSeq(healthy(0), unhealthy(1), offline(2), healthy(3))\n    val slotPerInvoker = 3\n    val invokerSlots = semaphores(invokers.size, slotPerInvoker)\n\n    val expectedResult = Seq(0, 0, 0, 3, 3, 3)\n    val result = expectedResult.map { _ =>\n      ShardingContainerPoolBalancer\n        .schedule(1, fqn, invokers, invokerSlots, 1, index = 0, step = 1)\n        .get\n        ._1\n        .toInt\n    }\n\n    result shouldBe expectedResult\n\n    // more schedules will result in randomized invokers, but the unhealthy and offline invokers should not be part\n    val bruteResult = (0 to 100).map { _ =>\n      ShardingContainerPoolBalancer\n        .schedule(1, fqn, invokers, invokerSlots, 1, index = 0, step = 1)\n        .get\n    }\n\n    bruteResult.map(_._1.toInt) should contain allOf (0, 3)\n    bruteResult.map(_._1.toInt) should contain noneOf (1, 2)\n    bruteResult.map(_._2) should contain only true\n  }\n\n  it should \"only take invokers that have enough free slots\" in {\n    val invokerCount = 3\n    // Each invoker has 4 slots\n    val invokerSlots = semaphores(invokerCount, 4)\n    val invokers = (0 until invokerCount).map(i => healthy(i))\n\n    // Ask for three slots -> First invoker should be used\n    ShardingContainerPoolBalancer\n      .schedule(1, fqn, invokers, invokerSlots, 3, index = 0, step = 1)\n      .get\n      ._1\n      .toInt shouldBe 0\n    // Ask for two slots -> Second invoker should be used\n    ShardingContainerPoolBalancer\n      .schedule(1, fqn, invokers, invokerSlots, 2, index = 0, step = 1)\n      .get\n      ._1\n      .toInt shouldBe 1\n    // Ask for 1 slot -> First invoker should be used\n    ShardingContainerPoolBalancer\n      .schedule(1, fqn, invokers, invokerSlots, 1, index = 0, step = 1)\n      .get\n      ._1\n      .toInt shouldBe 0\n    // Ask for 4 slots -> Third invoker should be used\n    ShardingContainerPoolBalancer\n      .schedule(1, fqn, invokers, invokerSlots, 4, index = 0, step = 1)\n      .get\n      ._1\n      .toInt shouldBe 2\n    // Ask for 2 slots -> Second invoker should be used\n    ShardingContainerPoolBalancer\n      .schedule(1, fqn, invokers, invokerSlots, 2, index = 0, step = 1)\n      .get\n      ._1\n      .toInt shouldBe 1\n\n    invokerSlots.foreach(_.availablePermits shouldBe 0)\n  }\n\n  behavior of \"pairwiseCoprimeNumbersUntil\"\n\n  it should \"return an empty set for malformed inputs\" in {\n    ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(0) shouldBe Seq.empty\n    ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(-1) shouldBe Seq.empty\n  }\n\n  it should \"return all coprime numbers until the number given\" in {\n    ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(1) shouldBe Seq(1)\n    ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(2) shouldBe Seq(1)\n    ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(3) shouldBe Seq(1, 2)\n    ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(4) shouldBe Seq(1, 3)\n    ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(5) shouldBe Seq(1, 2, 3)\n    ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(9) shouldBe Seq(1, 2, 5, 7)\n    ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(10) shouldBe Seq(1, 3, 7)\n  }\n\n  behavior of \"concurrent actions\"\n  it should \"allow concurrent actions to be scheduled to same invoker without affecting memory slots\" in {\n    val invokerCount = 3\n    // Each invoker has 2 slots, each action has concurrency 3\n    val slots = 2\n    val invokerSlots = semaphores(invokerCount, slots)\n    val concurrency = 3\n    val invokers = (0 until invokerCount).map(i => healthy(i))\n\n    (0 until invokerCount).foreach { i =>\n      (1 to slots).foreach { s =>\n        (1 to concurrency).foreach { c =>\n          ShardingContainerPoolBalancer\n            .schedule(concurrency, fqn, invokers, invokerSlots, 1, 0, 1)\n            .get\n            ._1\n            .toInt shouldBe i\n          invokerSlots\n            .lift(i)\n            .get\n            .concurrentState(fqn)\n            .availablePermits shouldBe concurrency - c\n        }\n      }\n    }\n\n  }\n\n  val config = new WhiskConfig(ExecManifest.requiredProperties)\n  val invokerMem = 2000.MB\n  val concurrencyEnabled = Option(WhiskProperties.getProperty(\"whisk.action.concurrency\")).exists(_.toBoolean)\n  val concurrency = if (concurrencyEnabled) 5 else 1\n  val actionMem = 256.MB\n  val actionMetaData =\n    WhiskActionMetaData(\n      namespace,\n      name,\n      jsMetaData(Some(\"jsMain\"), false),\n      limits = actionLimits(actionMem, concurrency))\n  val maxContainers = invokerMem.toMB.toInt / actionMetaData.limits.memory.megabytes\n  val numInvokers = 3\n  val maxActivations = maxContainers * numInvokers * concurrency\n\n  //run a separate test for each variant of 1..n concurrently-ish arriving activations, to exercise:\n  // - no containers started\n  // - containers started but no concurrency room\n  // - no concurrency room and no memory room to launch new containers\n  //(1 until maxActivations).foreach { i =>\n  (75 until maxActivations).foreach { i =>\n    it should s\"reflect concurrent processing $i state in containerSlots\" in {\n      //each batch will:\n      // - submit activations concurrently\n      // - wait for activation submission to messaging system (mostly to detect which invoker was assiged\n      // - verify remaining concurrency slots available\n      // - complete activations concurrently\n      // - verify concurrency/memory slots are released\n      testActivationBatch(i)\n    }\n  }\n\n  def mockMessaging(): MessagingProvider = {\n    val messaging = stub[MessagingProvider]\n    val producer = stub[MessageProducer]\n    val consumer = stub[MessageConsumer]\n    (messaging\n      .getProducer(_: WhiskConfig, _: Option[ByteSize])(_: Logging, _: ActorSystem))\n      .when(*, *, *, *)\n      .returns(producer)\n    (messaging\n      .getConsumer(_: WhiskConfig, _: String, _: String, _: Int, _: FiniteDuration)(_: Logging, _: ActorSystem))\n      .when(*, *, *, *, *, *, *)\n      .returns(consumer)\n    (producer\n      .send(_: String, _: Message, _: Int))\n      .when(*, *, *)\n      .returns(Future.successful(ResultMetadata(\"fake\", 0, 0)))\n\n    messaging\n  }\n\n  def testActivationBatch(numActivations: Int): Unit = {\n    //setup mock messaging\n    val feedProbe = new FeedFactory {\n      def createFeed(f: ActorRefFactory, m: MessagingProvider, p: (Array[Byte]) => Future[Unit]) =\n        TestProbe().testActor\n\n    }\n    val invokerPoolProbe = new InvokerPoolFactory {\n      override def createInvokerPool(\n        actorRefFactory: ActorRefFactory,\n        messagingProvider: MessagingProvider,\n        messagingProducer: MessageProducer,\n        sendActivationToInvoker: (MessageProducer, ActivationMessage, InvokerInstanceId) => Future[ResultMetadata],\n        monitor: Option[ActorRef]): ActorRef =\n        TestProbe().testActor\n    }\n    val balancer =\n      new ShardingContainerPoolBalancer(config, ControllerInstanceId(\"0\"), feedProbe, invokerPoolProbe, mockMessaging)\n\n    val invokers = IndexedSeq.tabulate(numInvokers) { i =>\n      new InvokerHealth(InvokerInstanceId(i, userMemory = invokerMem), Healthy)\n    }\n    balancer.schedulingState.updateInvokers(invokers)\n    val invocationNamespace = EntityName(\"invocationSpace\")\n\n    val fqn = actionMetaData.fullyQualifiedName(true)\n    val hash =\n      ShardingContainerPoolBalancer.generateHash(invocationNamespace, actionMetaData.fullyQualifiedName(false))\n    val home = hash % invokers.size\n    val stepSizes = ShardingContainerPoolBalancer.pairwiseCoprimeNumbersUntil(invokers.size)\n    val stepSize = stepSizes(hash % stepSizes.size)\n    val uuid = UUID()\n    //initiate activation\n    val published = (0 until numActivations).map { _ =>\n      val aid = ActivationId.generate()\n      val msg = ActivationMessage(\n        TransactionId.testing,\n        actionMetaData.fullyQualifiedName(true),\n        actionMetaData.rev,\n        Identity(Subject(), Namespace(invocationNamespace, uuid), BasicAuthenticationAuthKey(uuid, Secret())),\n        aid,\n        ControllerInstanceId(\"0\"),\n        blocking = false,\n        content = None,\n        initArgs = Set.empty,\n        lockedArgs = Map.empty)\n\n      //send activation to loadbalancer\n      aid -> balancer.publish(actionMetaData.toExecutableWhiskAction.get, msg)\n\n    }.toMap\n\n    val activations = published.values\n    val ids = published.keys\n\n    //wait for activation submissions\n    Await.ready(Future.sequence(activations.toList), 10.seconds)\n\n    val maxActivationsPerInvoker = concurrency * maxContainers\n    //verify updated concurrency slots\n\n    def rem(count: Int) =\n      if (count % concurrency > 0) {\n        concurrency - (count % concurrency)\n      } else {\n        0\n      }\n\n    //assert available permits per invoker are as expected\n    var nextInvoker = home\n    ids.toList.grouped(maxActivationsPerInvoker).zipWithIndex.foreach { g =>\n      val remaining = rem(g._1.size)\n      val concurrentState = balancer.schedulingState._invokerSlots\n        .lift(nextInvoker)\n        .get\n        .concurrentState(fqn)\n      concurrentState.availablePermits shouldBe remaining\n      concurrentState.counter shouldBe g._1.size\n      nextInvoker = (nextInvoker + stepSize) % numInvokers\n    }\n\n    //complete all\n    val acks = ids.map { aid =>\n      val invoker = balancer.activationSlots(aid).invokerName\n      completeActivation(invoker, balancer, aid)\n    }\n\n    Await.ready(Future.sequence(acks), 10.seconds)\n\n    //verify invokers go back to unused state\n    invokers.foreach { i =>\n      val concurrentState = balancer.schedulingState._invokerSlots\n        .lift(i.id.toInt)\n        .get\n        .concurrentState\n        .get(fqn)\n\n      concurrentState shouldBe None\n      balancer.schedulingState._invokerSlots.lift(i.id.toInt).map { i =>\n        i.availablePermits shouldBe invokerMem.toMB\n      }\n\n    }\n  }\n\n  def completeActivation(invoker: InvokerInstanceId, balancer: ShardingContainerPoolBalancer, aid: ActivationId) = {\n    //complete activation\n    val ack =\n      CompletionMessage(TransactionId.testing, aid, Some(false), invoker).serialize.getBytes(StandardCharsets.UTF_8)\n    balancer.processAcknowledgement(ack)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/FPCSchedulerFlowTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler\n\nimport java.nio.charset.StandardCharsets\nimport java.util.concurrent.TimeUnit\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.{TestKit, TestProbe}\nimport com.ibm.etcd.api.Event.EventType\nimport com.ibm.etcd.client.kv.WatchUpdate\nimport common.rest.WskRestOperations\nimport common.{ActivationResponse => _, _}\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.containerpool.ContainerProxyTimeoutConfig\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.{\n  containerPrefix,\n  inProgressPrefix,\n  namespacePrefix,\n  warmedPrefix\n}\nimport org.apache.openwhisk.core.etcd.EtcdKV.{QueueKeys, ThrottlingKeys}\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdConfig}\nimport org.apache.openwhisk.core.scheduler.queue.QueueConfig\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.utils.retry\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport pureconfig.loadConfigOrThrow\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport pureconfig.generic.auto._\n\nimport scala.collection.JavaConverters._\nimport scala.collection.mutable\nimport scala.concurrent.ExecutionContextExecutor\nimport scala.concurrent.duration.{DurationInt, FiniteDuration}\nimport scala.language.postfixOps\nimport scala.util.Random\nimport scala.util.control.Breaks._\n\n@RunWith(classOf[JUnitRunner])\nclass FPCSchedulerFlowTests\n    extends TestKit(ActorSystem(\"SchedulerFlow\"))\n    with AnyFlatSpecLike\n    with BeforeAndAfterAll\n    with WskTestHelpers\n    with ScalaFutures {\n  private implicit val ece: ExecutionContextExecutor = system.dispatcher\n  private val wsk = new WskRestOperations\n  private val defaultAction: Some[String] = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n  private val namespace = \"schedulerFlowNamespace\"\n\n  private val queueConfig = loadConfigOrThrow[QueueConfig](ConfigKeys.schedulerQueue)\n  private val containerConfig = loadConfigOrThrow[ContainerProxyTimeoutConfig](ConfigKeys.containerProxyTimeouts)\n  private val idleGrace = queueConfig.idleGrace\n  private val flushGrace = queueConfig.flushGrace\n  private val stopGrace = queueConfig.stopGrace\n  private val pauseGrace = containerConfig.pauseGrace\n\n  private val creationJobBaseTimeout = loadConfigOrThrow[FiniteDuration](ConfigKeys.schedulerInProgressJobRetention)\n\n  private var monitor: Option[TestProbe] = None\n  private val etcd = EtcdClient.apply(loadConfigOrThrow[EtcdConfig](ConfigKeys.etcd))\n\n  private val clusterName = loadConfigOrThrow[String](ConfigKeys.whiskClusterName)\n\n  val wskadmin = new RunCliCmd {\n    override def baseCommand: mutable.Buffer[String] = WskAdmin.baseCommand\n  }\n  private val auth = BasicAuthenticationAuthKey()\n  implicit val wskprops = WskProps(authKey = auth.compact, namespace = namespace)\n\n  private def getPrefixFromInProgressContainerKey(key: String): String = {\n    val prefixWithRevision = key.split(\"/creationId\")\n    val prefix = prefixWithRevision(0).split(\"/\").dropRight(3)\n    s\"${prefix.mkString(\"/\")}/\"\n  }\n\n  private def getPrefixFromContainerKey(key: String): String = {\n    val prefixWithRevision = key.split(\"/invoker\")\n    val prefix = prefixWithRevision(0).split(\"/\").dropRight(1)\n    s\"${prefix.mkString(\"/\")}/\"\n  }\n\n  private def watchEtcd(res: WatchUpdate): Unit = {\n    res.getEvents.asScala.foreach { event =>\n      val key = event.getKv.getKey.toString(StandardCharsets.UTF_8)\n      // only watch specified namespace\n      if (key.contains(namespace)) {\n        val processedKey =\n          if (key.startsWith(inProgressPrefix))\n            getPrefixFromInProgressContainerKey(key)\n          else if (key.startsWith(warmedPrefix))\n            getPrefixFromContainerKey(key)\n          else if (key.startsWith(namespacePrefix))\n            getPrefixFromContainerKey(key)\n          else\n            key\n        event.getType match {\n          // since warmed container will be exist for a long time, we will not watch the deletion of it\n          case EventType.DELETE if (!key.startsWith(warmedPrefix)) =>\n            monitor.foreach(_.ref ! DeleteEvent(processedKey))\n          case EventType.PUT =>\n            monitor.foreach(_.ref ! PutEvent(processedKey))\n          case _ =>\n        }\n      }\n    }\n  }\n\n  private val watcher = etcd.watchAllKeys(watchEtcd)\n\n  override def beforeAll(): Unit = {\n    wskadmin.cli(Seq(\"user\", \"create\", namespace, \"-u\", auth.compact))\n    retry(etcd.getCount(\"queue/\").futureValue shouldBe 0, 100, Some(2.seconds)) // wait all other queues timed out\n    super.beforeAll()\n  }\n\n  override def afterAll(): Unit = {\n    watcher.cancel(true)\n    watcher.close()\n    wskadmin.cli(Seq(\"user\", \"delete\", namespace))\n    etcd.close()\n    super.afterAll()\n  }\n\n  private def checkNormalFlow(watcher: TestProbe, fqn: FullyQualifiedEntityName, error: Boolean = false): Unit = {\n    // create one queue and one container\n    watcher.expectMsgAllOf(\n      20.seconds,\n      PutEvent(QueueKeys.queue(namespace, fqn, true)),\n      PutEvent(ThrottlingKeys.namespace(fqn.namespace)),\n      PutEvent(ThrottlingKeys.action(namespace, fqn)),\n      PutEvent(containerPrefix(inProgressPrefix, namespace, fqn)),\n      PutEvent(containerPrefix(namespacePrefix, namespace, fqn)),\n      DeleteEvent(containerPrefix(inProgressPrefix, namespace, fqn)))\n\n    val additionalContainers = checkAdditionalContainers(watcher, fqn, error)\n\n    // if container is failed to create or run activation, it will not goto Paused state\n    if (error) {\n      (0 to additionalContainers).foreach { _ =>\n        watcher.expectMsg(pauseGrace + 5.seconds, DeleteEvent(containerPrefix(namespacePrefix, namespace, fqn)))\n      }\n    } else {\n      if (additionalContainers >= 0) {\n        // only one container will goto warmed state\n        var messages =\n          Seq.fill[Any](additionalContainers + 1)(DeleteEvent(containerPrefix(namespacePrefix, namespace, fqn)))\n        messages :+= PutEvent(containerPrefix(warmedPrefix, namespace, fqn))\n        watcher.expectMsgAllOf(pauseGrace + 5.seconds, messages: _*)\n      }\n    }\n\n    // delete queue after timed out\n    watcher.expectMsgAllOf(\n      2 * (idleGrace + stopGrace) + 5.seconds,\n      DeleteEvent(QueueKeys.queue(namespace, fqn, true)),\n      DeleteEvent(ThrottlingKeys.namespace(fqn.namespace)),\n      DeleteEvent(ThrottlingKeys.action(namespace, fqn)))\n  }\n\n  private def checkAdditionalContainers(watcher: TestProbe, fqn: FullyQualifiedEntityName, error: Boolean): Int = {\n    // it may create more containers for old action\n    var additionalContainers = 0\n    breakable {\n      while (true) {\n        try {\n          watcher.expectMsgAllOf(\n            PutEvent(containerPrefix(inProgressPrefix, namespace, fqn)),\n            PutEvent(containerPrefix(namespacePrefix, namespace, fqn)),\n            DeleteEvent(containerPrefix(inProgressPrefix, namespace, fqn)))\n          additionalContainers += 1\n        } catch {\n          case t: Throwable =>\n            // it got one container deletion message for container failure case\n            if (t.getMessage.contains(\"got 1\"))\n              additionalContainers -= 1\n            else if (t.getMessage.contains(\"got 2\")) {\n              if (error) {\n                additionalContainers -= 2\n              } else {\n                additionalContainers -= 1\n              }\n            }\n            break\n        }\n      }\n    }\n    additionalContainers\n  }\n\n  behavior of \"Wsk actions\"\n\n  // TODO: Fix throttling event timing issues - events arrive out of order\n  ignore should \"invoke an action successfully\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val watcher = TestProbe()\n    monitor = Some(watcher)\n    val name = \"hello\"\n    val fqn = FullyQualifiedEntityName(EntityPath(namespace), EntityName(name), Some(SemVer()))\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, defaultAction)\n    }\n\n    withActivation(wsk.activation, wsk.action.invoke(name, Map(\"payload\" -> \"stranger\".toJson))) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"payload\" -> \"hello, stranger!\".toJson))\n    }\n\n    checkNormalFlow(watcher, fqn)\n  }\n\n  // TODO: Fix throttling event timing issues - events arrive out of order\n  ignore should \"invoke an action successfully while updating it\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val watcher = TestProbe()\n    monitor = Some(watcher)\n    val name = \"updating\"\n    val fqn = FullyQualifiedEntityName(EntityPath(namespace), EntityName(name), Some(SemVer()))\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, defaultAction)\n    }\n\n    withActivation(wsk.activation, wsk.action.invoke(name, Map(\"payload\" -> \"stranger\".toJson))) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"payload\" -> \"hello, stranger!\".toJson))\n    }\n\n    // create one queue and one container\n    watcher.expectMsgAllOf(\n      20.seconds,\n      PutEvent(QueueKeys.queue(namespace, fqn, true)),\n      PutEvent(ThrottlingKeys.namespace(fqn.namespace)),\n      PutEvent(ThrottlingKeys.action(namespace, fqn)),\n      PutEvent(containerPrefix(inProgressPrefix, namespace, fqn)),\n      PutEvent(containerPrefix(namespacePrefix, namespace, fqn)),\n      DeleteEvent(containerPrefix(inProgressPrefix, namespace, fqn)))\n\n    wsk.action.create(name, Some(TestUtils.getTestActionFilename(\"echo.js\")), update = true)\n\n    val additionalContainers = checkAdditionalContainers(watcher, fqn, false)\n    if (additionalContainers >= 0) {\n      // only one container will goto warmed state\n      var messages =\n        Seq.fill[Any](additionalContainers + 1)(DeleteEvent(containerPrefix(namespacePrefix, namespace, fqn)))\n      messages :+= PutEvent(containerPrefix(warmedPrefix, namespace, fqn))\n      watcher.expectMsgAllOf(pauseGrace + 5.seconds, messages: _*)\n    }\n\n    val newFqn = fqn.copy(version = Some(SemVer(0, 0, 2))) // version is updated from 0.0.1 to 0.0.2\n\n    withActivation(wsk.activation, wsk.action.invoke(name, Map(\"payload\" -> \"stranger\".toJson))) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"payload\" -> \"stranger\".toJson))\n    }\n\n    // create 1 new container\n    watcher.expectMsgAllOf(\n      PutEvent(containerPrefix(inProgressPrefix, namespace, newFqn)),\n      PutEvent(containerPrefix(namespacePrefix, namespace, newFqn)),\n      DeleteEvent(containerPrefix(inProgressPrefix, namespace, newFqn)),\n    )\n\n    // pause new containers and delete additional new containers(if created)\n    val additionalNewContainers = checkAdditionalContainers(watcher, newFqn, false)\n    if (additionalNewContainers >= 0) {\n      // only one container will goto warmed state\n      var messages =\n        Seq.fill[Any](additionalNewContainers + 1)(DeleteEvent(containerPrefix(namespacePrefix, namespace, newFqn)))\n      messages :+= PutEvent(containerPrefix(warmedPrefix, namespace, newFqn))\n      watcher.expectMsgAllOf(pauseGrace + 5.seconds, messages: _*)\n    }\n\n    watcher.expectMsgAllOf(\n      2 * (idleGrace + stopGrace) + 5.seconds,\n      DeleteEvent(QueueKeys.queue(namespace, fqn, true)),\n      DeleteEvent(ThrottlingKeys.namespace(fqn.namespace)),\n      DeleteEvent(ThrottlingKeys.action(namespace, fqn)))\n  }\n\n  // TODO: Fix throttling event timing issues - events arrive out of order\n  ignore should \"invoke an action that exits during initialization and get appropriate error\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val watcher = TestProbe()\n    monitor = Some(watcher)\n    val name = \"abort init\"\n    val fqn = FullyQualifiedEntityName(EntityPath(namespace), EntityName(name), Some(SemVer()))\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"initexit.js\")))\n    }\n\n    withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>\n      val response = activation.response\n      response.result.get.asJsObject().getFields(\"error\") shouldBe Seq(Messages.abnormalInitialization.toJson)\n      response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n    }\n\n    checkNormalFlow(watcher, fqn, true)\n  }\n\n  // TODO: Fix throttling event timing issues - events arrive out of order\n  ignore should \"invoke an action that hangs during initialization and get appropriate error\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val watcher = TestProbe()\n    monitor = Some(watcher)\n    val name = \"hang init\"\n    val fqn = FullyQualifiedEntityName(EntityPath(namespace), EntityName(name), Some(SemVer()))\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"initforever.js\")), timeout = Some(3 seconds))\n    }\n\n    withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>\n      val response = activation.response\n      response.result.get.asJsObject().getFields(\"error\") shouldBe Seq(\n        Messages.timedoutActivation(3 seconds, true).toJson)\n      response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n    }\n\n    checkNormalFlow(watcher, fqn, true)\n  }\n\n  // TODO: Fix throttling event timing issues - events arrive out of order\n  ignore should \"invoke an action that exits during run and get appropriate error\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val watcher = TestProbe()\n      monitor = Some(watcher)\n      val name = \"abort run\"\n      val fqn = FullyQualifiedEntityName(EntityPath(namespace), EntityName(name), Some(SemVer()))\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"runexit.js\")))\n      }\n\n      withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>\n        val response = activation.response\n        response.result.get.asJsObject().getFields(\"error\") shouldBe Seq(Messages.abnormalRun.toJson)\n        response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n      }\n\n      checkNormalFlow(watcher, fqn, true)\n  }\n\n  // TODO: Fix throttling event timing issues - events arrive out of order\n  ignore should \"create, and invoke an action that utilizes an invalid docker container with appropriate error\" in withAssetCleaner(\n    wskprops) {\n    val watcher = TestProbe()\n    val name = \"invalidDockerContainer\"\n    val fqn = FullyQualifiedEntityName(EntityPath(namespace), EntityName(name), Some(SemVer()))\n    val containerName = s\"bogus${Random.alphanumeric.take(16).mkString.toLowerCase}\"\n    val inProgressContainerkey = containerPrefix(inProgressPrefix, namespace, fqn)\n    watcher.ignoreMsg {\n      case PutEvent(key)    => key == inProgressContainerkey\n      case DeleteEvent(key) => key == inProgressContainerkey\n    }\n    monitor = Some(watcher)\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, name) {\n        // docker name is a randomly generate string\n        (action, _) =>\n          action.create(name, None, docker = Some(containerName))\n      }\n\n      val run = wsk.action.invoke(name)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError)\n        activation.response.result.get\n          .asJsObject()\n          .getFields(\"error\") shouldBe Seq(s\"Failed to pull container image '$containerName'.\".toJson)\n      }\n\n      val timeout = creationJobBaseTimeout.toSeconds * 3\n      // create one queue and failed to create container\n      watcher.expectMsgAllOf(\n        FiniteDuration(timeout, TimeUnit.SECONDS),\n        PutEvent(QueueKeys.queue(namespace, fqn, true)),\n        PutEvent(ThrottlingKeys.namespace(fqn.namespace)),\n        PutEvent(ThrottlingKeys.action(namespace, fqn)))\n\n      // delete queue after timed out\n      watcher.expectMsgAllOf(\n        flushGrace + 5.seconds,\n        DeleteEvent(QueueKeys.queue(namespace, fqn, true)),\n        DeleteEvent(ThrottlingKeys.namespace(fqn.namespace)),\n        DeleteEvent(ThrottlingKeys.action(namespace, fqn)))\n  }\n\n  // TODO: Fix throttling event timing issues - events arrive out of order\n  ignore should \"invoke a long action several times successfully\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val watcher = TestProbe()\n    val name = \"hello-long\"\n    val fqn = FullyQualifiedEntityName(EntityPath(namespace), EntityName(name), Some(SemVer()))\n    // ignore inProgressContainers&Throttling&warmedContainer as it may create many containers and some of them may failed or not used,\n    // which make them hard to monitor\n    val inProgressContainerkey = containerPrefix(inProgressPrefix, namespace, fqn)\n    val warmedContainerKey = containerPrefix(warmedPrefix, namespace, fqn)\n    watcher.ignoreMsg {\n      case PutEvent(key) =>\n        key == inProgressContainerkey || key.startsWith(s\"${clusterName}/throttling\") || key == warmedContainerKey || key\n          .contains(\"invalidDockerContainer\")\n      case DeleteEvent(key) =>\n        key == inProgressContainerkey || key.startsWith(s\"${clusterName}/throttling\") || key.contains(\n          \"invalidDockerContainer\")\n    }\n    monitor = Some(watcher)\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"sleep.js\")))\n    }\n\n    val runs = (0 to 4).map(_ => wsk.action.invoke(name, Map(\"sleepTimeInMs\" -> 30000.toJson)))\n    runs.foreach { run =>\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n        activation.response.result.get.toString should include(\"\"\"Terminated successfully after around\"\"\")\n      }\n    }\n\n    // create one queue and five containers at least\n    watcher.expectMsgAllOf(\n      20.seconds,\n      PutEvent(QueueKeys.queue(namespace, fqn, true)),\n      PutEvent(containerPrefix(namespacePrefix, namespace, fqn)),\n      PutEvent(containerPrefix(namespacePrefix, namespace, fqn)),\n      PutEvent(containerPrefix(namespacePrefix, namespace, fqn)),\n      PutEvent(containerPrefix(namespacePrefix, namespace, fqn)),\n      PutEvent(containerPrefix(namespacePrefix, namespace, fqn)))\n\n    // since it may create more than 5 containers, ignore these containers\n    var additionalContainers = 0\n    breakable {\n      while (true) {\n        try {\n          watcher.expectMsg(PutEvent(containerPrefix(namespacePrefix, namespace, fqn)))\n          additionalContainers += 1\n        } catch {\n          case t: Throwable =>\n            // need to minus 1 as it already got a DeleteEvent(existingContainers)\n            if (t.getMessage.contains(s\"found DeleteEvent(${containerPrefix(namespacePrefix, namespace, fqn)})\")) {\n              additionalContainers -= 1\n            }\n            break\n        }\n      }\n    }\n\n    // delete all 5 + additionalContainers containers after time out\n    val containers = (1 to 5 + additionalContainers).toList.map { _ =>\n      DeleteEvent(containerPrefix(namespacePrefix, namespace, fqn))\n    }\n    watcher.expectMsgAllOf(pauseGrace + 5.seconds, containers: _*)\n\n    // delete queue after timed out\n    watcher.expectMsg(2 * (idleGrace + stopGrace) + 5.seconds, DeleteEvent(QueueKeys.queue(namespace, fqn, true)))\n  }\n}\n\ncase class DeleteEvent(key: String)\ncase class PutEvent(key: String)\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/FPCSchedulerServerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler\n\nimport org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._\nimport org.apache.pekko.http.scaladsl.model.StatusCodes._\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.server.Route\nimport org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest\nimport common.StreamLogging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.connector.StatusData\nimport org.apache.openwhisk.core.entity.{ActivationId, SchedulerInstanceId}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\n\nimport scala.concurrent.Future\n\n/**\n * Tests SchedulerServer API.\n */\n@RunWith(classOf[JUnitRunner])\nclass FPCSchedulerServerTests\n    extends AnyFlatSpec\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll\n    with ScalatestRouteTest\n    with Matchers\n    with StreamLogging\n    with MockFactory {\n\n  def transid() = TransactionId(\"tid\")\n\n  val systemUsername = \"username\"\n  val systemPassword = \"password\"\n\n  val queues = List((SchedulerInstanceId(\"0\"), 2), (SchedulerInstanceId(\"1\"), 3))\n  val creationCount = 1\n  val testQueueSize = 2\n  val activationIds = (1 to 10).map(_ => ActivationId.generate()).toList\n  val statusDatas = List(\n    StatusData(\"testns1\", \"testaction1\", activationIds, \"Running\", \"RunningData\"),\n    StatusData(\"testns2\", \"testaction2\", activationIds.take(5), \"Running\", \"RunningData\"))\n\n  // Create scheduler\n  val scheduler = new TestScheduler(queues, creationCount, testQueueSize, statusDatas)\n  val server = new FPCSchedulerServer(scheduler, systemUsername, systemPassword)\n\n  override protected def afterEach(): Unit = scheduler.reset()\n\n  /** FPCSchedulerServer API tests */\n  behavior of \"FPCSchedulerServer API\"\n\n  // POST /disable\n  it should \"disable scheduler\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Post(s\"/disable\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      scheduler.shutdownCount shouldBe 1\n    }\n  }\n\n  // GET /state\n  it should \"get scheduler state\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Get(s\"/state\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      responseAs[JsObject] shouldBe (Map(\"creationCount\" -> creationCount.toString) ++ Map(\n        \"queue\" -> queues.map(_._2).sum.toString)).toJson\n    }\n  }\n\n  // GET /queue/total\n  it should \"get total queue\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Get(s\"/queues/count\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      responseAs[String] shouldBe testQueueSize.toString\n    }\n  }\n\n  // GET /queue/status\n  it should \"get all queue status\" in {\n    implicit val tid = transid()\n    val validCredentials = BasicHttpCredentials(systemUsername, systemPassword)\n    Get(s\"/queues\") ~> addCredentials(validCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(OK)\n      responseAs[List[JsObject]] shouldBe statusDatas.map(_.toJson)\n    }\n  }\n\n  // POST /disable with invalid credential\n  it should \"not call scheduler api with invalid credential\" in {\n    implicit val tid = transid()\n    val invalidCredentials = BasicHttpCredentials(\"invaliduser\", \"invalidpass\")\n    Post(s\"/disable\") ~> addCredentials(invalidCredentials) ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      scheduler.shutdownCount shouldBe 0\n    }\n  }\n\n  // POST /disable with empty credential\n  it should \"not call scheduler api with empty credential\" in {\n    implicit val tid = transid()\n    Post(s\"/disable\") ~> Route.seal(server.routes(tid)) ~> check {\n      status should be(Unauthorized)\n      scheduler.shutdownCount shouldBe 0\n    }\n  }\n\n}\n\nclass TestScheduler(schedulerStates: List[(SchedulerInstanceId, Int)],\n                    creationCount: Int,\n                    queueSize: Int,\n                    statusDatas: List[StatusData])\n    extends SchedulerCore {\n  var shutdownCount = 0\n\n  override def getState: Future[(List[(SchedulerInstanceId, Int)], Int)] =\n    Future.successful(schedulerStates, creationCount)\n\n  override def getQueueSize: Future[Int] = Future.successful(queueSize)\n\n  override def getQueueStatusData: Future[List[StatusData]] = Future.successful(statusDatas)\n\n  override def disable(): Unit = shutdownCount += 1\n\n  def reset(): Unit = {\n    shutdownCount = 0\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/container/test/ContainerManagerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.container.test\n\nimport org.apache.pekko.actor.FSM.{CurrentState, SubscribeTransitionCallBack}\nimport org.apache.pekko.actor.{ActorRef, ActorRefFactory, ActorSystem}\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit, TestProbe}\nimport com.ibm.etcd.api.{KeyValue, RangeResponse}\nimport common.{StreamLogging, WskActorSystem}\nimport org.apache.openwhisk.common.InvokerState.{Healthy, Unhealthy}\nimport org.apache.openwhisk.common.{GracefulShutdown, InvokerHealth, Logging, TransactionId}\nimport org.apache.openwhisk.core.connector.ContainerCreationError.{\n  NoAvailableInvokersError,\n  NoAvailableResourceInvokersError\n}\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.containerpool.{ContainerId, Uninitialized}\nimport org.apache.openwhisk.core.database.test.DbUtils\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.containerPrefix\nimport org.apache.openwhisk.core.etcd.EtcdKV.{ContainerKeys, InvokerKeys}\nimport org.apache.openwhisk.core.etcd.EtcdType._\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdConfig}\nimport org.apache.openwhisk.core.scheduler.container.{ScheduledPair, _}\nimport org.apache.openwhisk.core.scheduler.message.{\n  ContainerCreation,\n  ContainerDeletion,\n  FailedCreationJob,\n  RegisterCreationJob,\n  ReschedulingCreationJob\n}\n\nimport scala.language.postfixOps\nimport org.apache.openwhisk.core.scheduler.queue.{MemoryQueueKey, MemoryQueueValue, QueuePool}\nimport org.apache.openwhisk.core.service.{WatchEndpointInserted, WatchEndpointRemoved}\nimport org.apache.openwhisk.core.{ConfigKeys, WhiskConfig}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport pureconfig.loadConfigOrThrow\nimport spray.json.{JsArray, JsBoolean, JsString}\nimport pureconfig.generic.auto._\n\nimport scala.collection.concurrent.TrieMap\nimport scala.collection.mutable\nimport scala.concurrent.Future\nimport scala.concurrent.duration.{FiniteDuration, _}\n\n@RunWith(classOf[JUnitRunner])\nclass ContainerManagerTests\n    extends TestKit(ActorSystem(\"ContainerManager\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with StreamLogging {\n\n  val config = new WhiskConfig(ExecManifest.requiredProperties)\n  ExecManifest.initialize(config)\n\n  val testInvocationNamespace = \"test-invocation-namespace\"\n  val testNamespace = \"test-namespace\"\n  val testAction = \"test-action\"\n  val testfqn = FullyQualifiedEntityName(EntityPath(testNamespace), EntityName(testAction))\n  val blackboxInvocation = false\n  val testCreationId = CreationId.generate()\n  val testRevision = DocRevision(\"1-testRev\")\n  val testMemory = 256.MB\n  val testResources = Seq.empty[String]\n  val resourcesStrictPolicy = false\n\n  val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n  val action = ExecutableWhiskAction(EntityPath(testNamespace), EntityName(testAction), exec)\n  val execMetadata = CodeExecMetaDataAsString(exec.manifest, entryPoint = exec.entryPoint)\n  val actionMetadata =\n    WhiskActionMetaData(\n      action.namespace,\n      action.name,\n      execMetadata,\n      action.parameters,\n      action.limits,\n      action.version,\n      action.publish,\n      action.annotations)\n\n  val invokers: List[InvokerHealth] = List(\n    InvokerHealth(InvokerInstanceId(0, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    InvokerHealth(InvokerInstanceId(1, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    InvokerHealth(InvokerInstanceId(2, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    InvokerHealth(InvokerInstanceId(3, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    InvokerHealth(InvokerInstanceId(4, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    InvokerHealth(InvokerInstanceId(5, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    InvokerHealth(InvokerInstanceId(6, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    InvokerHealth(InvokerInstanceId(7, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    InvokerHealth(InvokerInstanceId(8, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    InvokerHealth(InvokerInstanceId(9, userMemory = testMemory, tags = Seq.empty[String]), Healthy))\n\n  val testsid = SchedulerInstanceId(\"0\")\n\n  val schedulerHost = \"127.17.0.1\"\n  val rpcPort = 13001\n\n  override def afterAll(): Unit = {\n    logLines.foreach(println)\n    QueuePool.clear()\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  override def beforeEach(): Unit = {\n    QueuePool.clear()\n  }\n\n  def mockMessaging(receiver: Option[ActorRef] = None): MessagingProvider = {\n    val messaging = stub[MessagingProvider]\n    val producer = receiver.map(fakeProducer).getOrElse(stub[MessageProducer])\n    val consumer = stub[MessageConsumer]\n    (messaging\n      .getProducer(_: WhiskConfig, _: Option[ByteSize])(_: Logging, _: ActorSystem))\n      .when(*, *, *, *)\n      .returns(producer)\n    (messaging\n      .getConsumer(_: WhiskConfig, _: String, _: String, _: Int, _: FiniteDuration)(_: Logging, _: ActorSystem))\n      .when(*, *, *, *, *, *, *)\n      .returns(consumer)\n    // this is a stub producer\n    if (receiver.isEmpty) {\n      (producer\n        .send(_: String, _: Message, _: Int))\n        .when(*, *, *)\n        .returns(Future.successful(ResultMetadata(\"fake\", 0, 0)))\n    }\n\n    messaging\n  }\n\n  private def fakeProducer(receiver: ActorRef) = new MessageProducer {\n\n    /** Count of messages sent. */\n    override def sentCount(): Long = 0\n\n    /** Sends msg to topic. This is an asynchronous operation. */\n    override def send(topic: String, msg: Message, retry: Int): Future[ResultMetadata] = {\n      val message = s\"$topic-$msg\"\n      receiver ! message\n\n      Future.successful(ResultMetadata(topic, 0, -1))\n    }\n\n    /** Closes producer. */\n    override def close(): Unit = {}\n  }\n\n  def expectGetInvokers(etcd: EtcdClient, invokers: List[InvokerHealth] = invokers): Unit = {\n    (etcd\n      .getPrefix(_: String))\n      .expects(InvokerKeys.prefix)\n      .returning(Future.successful {\n        invokers\n          .foldLeft(RangeResponse.newBuilder()) { (builder, invoker) =>\n            val msg = InvokerResourceMessage(\n              invoker.status.asString,\n              invoker.id.userMemory.toMB,\n              invoker.id.busyMemory.getOrElse(0.MB).toMB,\n              0,\n              invoker.id.tags,\n              invoker.id.dedicatedNamespaces)\n\n            builder.addKvs(\n              KeyValue\n                .newBuilder()\n                .setKey(InvokerKeys.health(invoker.id))\n                .setValue(msg.toString)\n                .build())\n          }\n          .build()\n      })\n  }\n\n  /** Registers the transition callback and expects the first message */\n  def registerCallback(c: ActorRef) = {\n    c ! SubscribeTransitionCallBack(testActor)\n    expectMsg(CurrentState(c, Uninitialized))\n  }\n\n  def factory(t: TestProbe)(f: ActorRefFactory): ActorRef = t.ref\n\n  behavior of \"ContainerManager\"\n\n  it should \"create container\" in {\n    val mockEtcd = mock[EtcdClient]\n    (mockEtcd\n      .getPrefix(_: String))\n      .expects(*)\n      .returning(Future.successful {\n        RangeResponse.newBuilder().build()\n      })\n    expectGetInvokers(mockEtcd)\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n\n    val manager =\n      system.actorOf(\n        ContainerManager.props(factory(mockJobManager), mockMessaging(), testsid, mockEtcd, config, mockWatcher.ref))\n\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        FullyQualifiedEntityName(EntityPath(\"ns1\"), EntityName(testAction)),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        FullyQualifiedEntityName(EntityPath(\"ns3\"), EntityName(testAction)),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg3 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        FullyQualifiedEntityName(EntityPath(\"ns3\"), EntityName(testAction)),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    val msgs = List(msg1, msg2, msg3)\n    val creationMsg = ContainerCreation(msgs, testMemory, testInvocationNamespace)\n\n    manager ! creationMsg\n\n    mockJobManager.expectMsgPF() {\n      case RegisterCreationJob(`msg1`) => true\n      case RegisterCreationJob(`msg2`) => true\n      case RegisterCreationJob(`msg3`) => true\n    }\n  }\n\n  it should \"try warmed containers first\" in {\n    val mockEtcd = mock[EtcdClient]\n\n    // at first, invoker states look like this.\n    val invokers: List[InvokerHealth] = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 256.MB, tags = Seq.empty[String]), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 256.MB, tags = Seq.empty[String]), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 512.MB, tags = Seq.empty[String]), Healthy),\n    )\n\n    // after then, invoker states changes like this.\n    val updatedInvokers: List[InvokerHealth] = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 512.MB, tags = Seq.empty[String]), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 256.MB, tags = Seq.empty[String]), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 256.MB, tags = Seq.empty[String]), Healthy),\n    )\n    expectGetInvokers(mockEtcd, invokers) // for warm up\n    expectGetInvokers(mockEtcd, invokers) // for first creation\n    expectGetInvokers(mockEtcd, updatedInvokers) // for second creation\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n    val receiver = TestProbe()\n    // ignore warmUp message\n    receiver.ignoreMsg {\n      case s: String => s.contains(\"warmUp\")\n    }\n\n    val manager =\n      system.actorOf(ContainerManager\n        .props(factory(mockJobManager), mockMessaging(Some(receiver.ref)), testsid, mockEtcd, config, mockWatcher.ref))\n\n    // Add warmed containers for action1 and action2 in invoker0 and invoker1 respectively\n    manager ! WatchEndpointInserted(\n      ContainerKeys.warmedPrefix,\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        InvokerInstanceId(0, userMemory = 0.bytes),\n        ContainerId(\"fake\")),\n      \"\",\n      true)\n    manager ! WatchEndpointInserted(\n      ContainerKeys.warmedPrefix,\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn.copy(name = EntityName(\"test-action-2\")),\n        testRevision,\n        InvokerInstanceId(1, userMemory = 0.bytes),\n        ContainerId(\"fake\")),\n      \"\",\n      true)\n\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.copy(name = EntityName(\"test-action-2\")),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg3 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msgs = List(msg1, msg2, msg3)\n\n    // it should reuse 2 warmed containers\n    manager ! ContainerCreation(msgs, 256.MB, testInvocationNamespace)\n\n    // msg1 will use warmed container on invoker0, msg2 use warmed container on invoker1, msg3 use the remainder\n    receiver.expectMsg(s\"invoker0-$msg1\")\n    receiver.expectMsg(s\"invoker1-$msg2\")\n    receiver.expectMsg(s\"invoker2-$msg3\")\n\n    mockJobManager.expectMsgPF() {\n      case RegisterCreationJob(`msg1`) => true\n      case RegisterCreationJob(`msg2`) => true\n      case RegisterCreationJob(`msg3`) => true\n    }\n\n    // remove a warmed container from invoker0\n    manager ! WatchEndpointRemoved(\n      ContainerKeys.warmedPrefix,\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        InvokerInstanceId(0, userMemory = 0.bytes),\n        ContainerId(\"fake\")),\n      \"\",\n      true)\n\n    // remove a warmed container from invoker1\n    manager ! WatchEndpointRemoved(\n      ContainerKeys.warmedPrefix,\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn.copy(name = EntityName(\"test-action-2\")),\n        testRevision,\n        InvokerInstanceId(1, userMemory = 0.bytes),\n        ContainerId(\"fake\")),\n      \"\",\n      true)\n\n    // create a warmed container for action1 in from invoker1\n    manager ! WatchEndpointInserted(\n      ContainerKeys.warmedPrefix,\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        InvokerInstanceId(1, userMemory = 0.bytes),\n        ContainerId(\"fake\")),\n      \"\",\n      true)\n\n    // create a warmed container for action2 in from invoker2\n    manager ! WatchEndpointInserted(\n      ContainerKeys.warmedPrefix,\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn.copy(name = EntityName(\"test-action-2\")),\n        testRevision,\n        InvokerInstanceId(2, userMemory = 0.bytes),\n        ContainerId(\"fake\")),\n      \"\",\n      true)\n\n    // it should reuse 2 warmed containers\n    manager ! ContainerCreation(msgs, 256.MB, testInvocationNamespace)\n\n    // msg1 will use warmed container on invoker1, msg2 use warmed container on invoker2, msg3 use the remainder\n    receiver.expectMsg(s\"invoker1-$msg1\")\n    receiver.expectMsg(s\"invoker2-$msg2\")\n    receiver.expectMsg(s\"invoker0-$msg3\")\n\n    mockJobManager.expectMsgPF() {\n      case RegisterCreationJob(`msg1`) => true\n      case RegisterCreationJob(`msg2`) => true\n      case RegisterCreationJob(`msg3`) => true\n    }\n  }\n\n  it should \"not try warmed containers if revision is unmatched\" in {\n    val mockEtcd = mock[EtcdClient]\n\n    // for test, only invoker2 is healthy, so that no-warmed creations can be only sent to invoker2\n    val invokers: List[InvokerHealth] = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = testMemory, tags = Seq.empty[String]), Unhealthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = testMemory, tags = Seq.empty[String]), Unhealthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    )\n    expectGetInvokers(mockEtcd, invokers)\n    expectGetInvokers(mockEtcd, invokers) // one for warmup\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n    val receiver = TestProbe()\n    // ignore warmUp message\n    receiver.ignoreMsg {\n      case s: String => s.contains(\"warmUp\")\n    }\n\n    val manager =\n      system.actorOf(ContainerManager\n        .props(factory(mockJobManager), mockMessaging(Some(receiver.ref)), testsid, mockEtcd, config, mockWatcher.ref))\n\n    // there are 1 warmed container for `test-namespace/test-action` but with a different revision\n    manager ! WatchEndpointInserted(\n      ContainerKeys.warmedPrefix,\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn,\n        DocRevision(\"2-testRev\"),\n        InvokerInstanceId(0, userMemory = 0.bytes),\n        ContainerId(\"fake\")),\n      \"\",\n      true)\n\n    val msg =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    // it should not reuse the warmed container\n    manager ! ContainerCreation(List(msg), 128.MB, testInvocationNamespace)\n\n    // it should be scheduled to the sole health invoker: invoker2\n    receiver.expectMsg(s\"invoker2-$msg\")\n\n    mockJobManager.expectMsgPF() {\n      case RegisterCreationJob(`msg`) => true\n    }\n  }\n\n  it should \"rescheduling container creation\" in {\n    val mockEtcd = mock[EtcdClient]\n    (mockEtcd\n      .getPrefix(_: String))\n      .expects(*)\n      .returning(Future.successful {\n        RangeResponse.newBuilder().build()\n      })\n    expectGetInvokers(mockEtcd)\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n\n    val manager =\n      system.actorOf(\n        ContainerManager.props(factory(mockJobManager), mockMessaging(), testsid, mockEtcd, config, mockWatcher.ref))\n\n    val reschedulingMsg =\n      ReschedulingCreationJob(\n        TransactionId.testing,\n        testCreationId,\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        actionMetadata,\n        schedulerHost,\n        rpcPort,\n        0)\n\n    val creationMsg = reschedulingMsg.toCreationMessage(testsid, reschedulingMsg.retry + 1)\n\n    manager ! reschedulingMsg\n\n    mockJobManager.expectMsg(RegisterCreationJob(creationMsg))\n  }\n\n  it should \"forward GracefulShutdown to creation job manager\" in {\n    val mockEtcd = mock[EtcdClient]\n    (mockEtcd\n      .getPrefix(_: String))\n      .expects(*)\n      .returning(Future.successful {\n        RangeResponse.newBuilder().build()\n      })\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n\n    val manager =\n      system.actorOf(\n        ContainerManager.props(factory(mockJobManager), mockMessaging(), testsid, mockEtcd, config, mockWatcher.ref))\n\n    manager ! GracefulShutdown\n\n    mockJobManager.expectMsg(GracefulShutdown)\n  }\n\n  it should \"generate random number less than mod\" in {\n    val mod = 10\n    (1 to 100).foreach(_ => {\n      val num = ContainerManager.rng(mod)\n      num should be < mod\n    })\n  }\n\n  it should \"choose invokers\" in {\n    val healthyInvokers: List[InvokerHealth] = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 512.MB), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 512.MB), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 512.MB), Healthy))\n\n    val minMemory = 512.MB\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns1\")),\n        testRevision,\n        actionMetadata.copy(limits = actionMetadata.limits.copy(memory = MemoryLimit(minMemory))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns2\")),\n        testRevision,\n        actionMetadata.copy(limits = actionMetadata.limits.copy(memory = MemoryLimit(minMemory))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg3 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns3\")),\n        testRevision,\n        actionMetadata.copy(limits = actionMetadata.limits.copy(memory = MemoryLimit(minMemory))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msgs = List(msg1, msg2, msg3)\n\n    val pairs = ContainerManager.schedule(healthyInvokers, msgs, minMemory)\n\n    pairs.map(_.msg) should contain theSameElementsAs msgs\n    pairs.flatMap(_.invokerId).foreach { invokerId =>\n      healthyInvokers.map(_.id) should contain(invokerId)\n    }\n  }\n\n  it should \"choose invoker even if there is only one invoker\" in {\n    val healthyInvokers: List[InvokerHealth] = List(InvokerHealth(InvokerInstanceId(0, userMemory = 1024.MB), Healthy))\n\n    val minMemory = 128.MB\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns1\")),\n        testRevision,\n        actionMetadata.copy(limits = actionMetadata.limits.copy(memory = MemoryLimit(minMemory))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns2\")),\n        testRevision,\n        actionMetadata.copy(limits = actionMetadata.limits.copy(memory = MemoryLimit(minMemory))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg3 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns3\")),\n        testRevision,\n        actionMetadata.copy(limits = actionMetadata.limits.copy(memory = MemoryLimit(minMemory))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msgs = List(msg1, msg2, msg3)\n\n    val pairs = ContainerManager.schedule(healthyInvokers, msgs, minMemory)\n\n    pairs.map(_.msg) should contain theSameElementsAs msgs\n    pairs.map(_.invokerId.get.instance).foreach {\n      healthyInvokers.map(_.id.instance) should contain(_)\n    }\n  }\n\n  it should \"filter invokers based on tags\" in {\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns1\")),\n        testRevision,\n        actionMetadata.copy(\n          annotations =\n            Parameters(Annotations.InvokerResourcesAnnotationName, JsArray(JsString(\"cpu\"), JsString(\"memory\")))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns2\")),\n        testRevision,\n        actionMetadata.copy(\n          annotations =\n            Parameters(Annotations.InvokerResourcesAnnotationName, JsArray(JsString(\"memory\"), JsString(\"disk\")))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg3 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns3\")),\n        testRevision,\n        actionMetadata.copy(\n          annotations =\n            Parameters(Annotations.InvokerResourcesAnnotationName, JsArray(JsString(\"disk\"), JsString(\"cpu\")))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg4 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns4\")),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg5 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns5\")),\n        testRevision,\n        actionMetadata.copy(\n          annotations =\n            Parameters(Annotations.InvokerResourcesAnnotationName, JsArray(JsString(\"fake\"))) ++ Parameters(\n              Annotations.InvokerResourcesStrictPolicyAnnotationName,\n              JsBoolean(true))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    val probe = TestProbe()\n    QueuePool.put(\n      MemoryQueueKey(testInvocationNamespace, testfqn.toDocId.asDocInfo(testRevision)),\n      MemoryQueueValue(probe.ref, true))\n\n    val healthyInvokers: List[InvokerHealth] = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 512.MB, tags = Seq(\"cpu\", \"memory\")), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 512.MB, tags = Seq(\"memory\", \"disk\")), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 512.MB, tags = Seq(\"disk\", \"cpu\")), Healthy),\n      InvokerHealth(InvokerInstanceId(3, userMemory = 512.MB), Healthy))\n\n    // for msg1/2/3 we choose the exact invokers for them, for msg4, we choose no tagged invokers first, here is the invoker3\n    // for msg5, there is no available invokers, and the resource strict policy is true, so return an error\n    val pairs = ContainerManager.schedule(\n      healthyInvokers,\n      List(msg1, msg2, msg3, msg4, msg5),\n      msg1.whiskActionMetaData.limits.memory.megabytes.MB) // the memory is same for all msgs\n    pairs should contain theSameElementsAs List(\n      ScheduledPair(msg1, Some(healthyInvokers(0).id), None),\n      ScheduledPair(msg2, Some(healthyInvokers(1).id), None),\n      ScheduledPair(msg3, Some(healthyInvokers(2).id), None),\n      ScheduledPair(msg4, Some(healthyInvokers(3).id), None),\n      ScheduledPair(msg5, None, Some(NoAvailableResourceInvokersError)))\n  }\n\n  it should \"choose tagged invokers when no untagged invoker is available\" in {\n    val msg =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns1\")),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns2\")),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    val probe = TestProbe()\n    QueuePool.put(\n      MemoryQueueKey(testInvocationNamespace, testfqn.toDocId.asDocInfo(testRevision)),\n      MemoryQueueValue(probe.ref, true))\n\n    val healthyInvokers: List[InvokerHealth] =\n      List(InvokerHealth(InvokerInstanceId(0, userMemory = 256.MB, tags = Seq(\"cpu\", \"memory\")), Healthy))\n\n    // there is no available invokers which has no tags, it should choose tagged invokers for msg\n    // and for msg2, it should return no available invokers\n    val pairs =\n      ContainerManager.schedule(healthyInvokers, List(msg, msg2), msg.whiskActionMetaData.limits.memory.megabytes.MB)\n    pairs should contain theSameElementsAs List(\n      ScheduledPair(msg, Some(healthyInvokers(0).id), None),\n      ScheduledPair(msg2, None, Some(NoAvailableInvokersError)))\n  }\n\n  it should \"respect the resource policy while use resource filter\" in {\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns1\")),\n        testRevision,\n        actionMetadata.copy(\n          annotations =\n            Parameters(Annotations.InvokerResourcesAnnotationName, JsArray(JsString(\"non-exist\"))) ++ Parameters(\n              Annotations.InvokerResourcesStrictPolicyAnnotationName,\n              JsBoolean(true))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns2\")),\n        testRevision,\n        actionMetadata.copy(\n          annotations =\n            Parameters(Annotations.InvokerResourcesAnnotationName, JsArray(JsString(\"non-exist\"))) ++ Parameters(\n              Annotations.InvokerResourcesStrictPolicyAnnotationName,\n              JsBoolean(false))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg3 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns3\")),\n        testRevision,\n        actionMetadata.copy(\n          limits = action.limits.copy(memory = MemoryLimit(256.MB)),\n          annotations =\n            Parameters(Annotations.InvokerResourcesAnnotationName, JsArray(JsString(\"non-exist\"))) ++ Parameters(\n              Annotations.InvokerResourcesStrictPolicyAnnotationName,\n              JsBoolean(false))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg4 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.resolve(EntityName(\"ns3\")),\n        testRevision,\n        actionMetadata.copy(\n          limits = action.limits.copy(memory = MemoryLimit(256.MB)),\n          annotations =\n            Parameters(Annotations.InvokerResourcesAnnotationName, JsArray(JsString(\"non-exist\"))) ++ Parameters(\n              Annotations.InvokerResourcesStrictPolicyAnnotationName,\n              JsBoolean(false))),\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    val probe = TestProbe()\n    QueuePool.put(\n      MemoryQueueKey(testInvocationNamespace, testfqn.toDocId.asDocInfo(testRevision)),\n      MemoryQueueValue(probe.ref, true))\n    val healthyInvokers: List[InvokerHealth] =\n      List(\n        InvokerHealth(InvokerInstanceId(0, userMemory = 256.MB, tags = Seq.empty[String]), Healthy),\n        InvokerHealth(InvokerInstanceId(1, userMemory = 256.MB, tags = Seq(\"cpu\", \"memory\")), Healthy))\n\n    // while resourcesStrictPolicy is true, and there is no suitable invokers, return an error\n    val pairs =\n      ContainerManager.schedule(healthyInvokers, List(msg1), msg1.whiskActionMetaData.limits.memory.megabytes.MB)\n    pairs should contain theSameElementsAs List(ScheduledPair(msg1, None, Some(NoAvailableResourceInvokersError)))\n\n    // while resourcesStrictPolicy is false, and there is no suitable invokers, should choose no tagged invokers first,\n    // here is the invoker0\n    val pairs2 =\n      ContainerManager.schedule(healthyInvokers, List(msg2), msg2.whiskActionMetaData.limits.memory.megabytes.MB)\n    pairs2 should contain theSameElementsAs List(ScheduledPair(msg2, Some(healthyInvokers(0).id), None))\n\n    // while resourcesStrictPolicy is false, and there is no suitable invokers, should choose no tagged invokers first,\n    // if there is none, then choose invokers with other tags, if there is still none, return no available invokers\n    val pairs3 = ContainerManager.schedule(\n      healthyInvokers.takeRight(1),\n      List(msg3, msg4),\n      msg3.whiskActionMetaData.limits.memory.megabytes.MB)\n    pairs3 should contain theSameElementsAs List(\n      ScheduledPair(msg3, Some(healthyInvokers(1).id)),\n      ScheduledPair(msg4, None, Some(NoAvailableInvokersError)))\n  }\n\n  it should \"send FailedCreationJob to memory queue when no invokers are available\" in {\n    val mockEtcd = mock[EtcdClient]\n    val probe = TestProbe()\n    (mockEtcd\n      .getPrefix(_: String))\n      .expects(InvokerKeys.prefix)\n      .returning(Future.successful {\n        RangeResponse.newBuilder().build()\n      })\n      .twice()\n\n    val fqn = FullyQualifiedEntityName(EntityPath(\"ns1\"), EntityName(testAction))\n\n    QueuePool.put(\n      MemoryQueueKey(testInvocationNamespace, fqn.toDocId.asDocInfo(testRevision)),\n      MemoryQueueValue(probe.ref, true))\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n\n    val manager =\n      system.actorOf(\n        ContainerManager.props(factory(mockJobManager), mockMessaging(), testsid, mockEtcd, config, mockWatcher.ref))\n\n    val msg =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        fqn,\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    manager ! ContainerCreation(List(msg), testMemory, testInvocationNamespace)\n    probe.expectMsg(\n      FailedCreationJob(\n        msg.creationId,\n        testInvocationNamespace,\n        msg.action,\n        testRevision,\n        NoAvailableInvokersError,\n        NoAvailableInvokersError))\n  }\n\n  it should \"send FailedCreationJob to memory queue when available invoker query fails\" in {\n    val mockEtcd = mock[EtcdClient]\n    val probe = TestProbe()\n    (mockEtcd\n      .getPrefix(_: String))\n      .expects(InvokerKeys.prefix)\n      .returning(Future.failed(new Exception(\"etcd request failed.\")))\n      .twice()\n\n    val fqn = FullyQualifiedEntityName(EntityPath(\"ns1\"), EntityName(testAction))\n\n    QueuePool.put(\n      MemoryQueueKey(testInvocationNamespace, fqn.toDocId.asDocInfo(testRevision)),\n      MemoryQueueValue(probe.ref, true))\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n\n    val manager =\n      system.actorOf(\n        ContainerManager.props(factory(mockJobManager), mockMessaging(), testsid, mockEtcd, config, mockWatcher.ref))\n\n    val msg =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        fqn,\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    manager ! ContainerCreation(List(msg), testMemory, testInvocationNamespace)\n    probe.expectMsg(\n      FailedCreationJob(\n        msg.creationId,\n        testInvocationNamespace,\n        msg.action,\n        testRevision,\n        NoAvailableInvokersError,\n        NoAvailableInvokersError))\n  }\n\n  it should \"schedule to the blackbox invoker when isBlackboxInvocation is true\" in {\n    stream.reset()\n    val mockEtcd = mock[EtcdClient]\n    (mockEtcd\n      .getPrefix(_: String))\n      .expects(*)\n      .returning(Future.successful {\n        RangeResponse.newBuilder().build()\n      })\n    expectGetInvokers(mockEtcd)\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n\n    val manager =\n      system.actorOf(\n        ContainerManager.props(factory(mockJobManager), mockMessaging(), testsid, mockEtcd, config, mockWatcher.ref))\n\n    val exec = BlackBoxExec(ExecManifest.ImageName(\"image\"), None, None, native = false, binary = false)\n    val action = ExecutableWhiskAction(EntityPath(testNamespace), EntityName(testAction), exec)\n    val execMetadata = BlackBoxExecMetaData(exec.image, exec.entryPoint, exec.native, exec.binary)\n    val actionMetadata =\n      WhiskActionMetaData(\n        action.namespace,\n        action.name,\n        execMetadata,\n        action.parameters,\n        action.limits,\n        action.version,\n        action.publish,\n        action.annotations)\n\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        FullyQualifiedEntityName(EntityPath(\"ns1\"), EntityName(testAction)),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    val msgs = List(msg1)\n    val creationMsg = ContainerCreation(msgs, testMemory, testInvocationNamespace)\n\n    manager ! creationMsg\n\n    mockJobManager.expectMsgPF() {\n      case RegisterCreationJob(`msg1`) => true\n    }\n\n    Thread.sleep(1000)\n\n    // blackbox invoker number = 10 * 0.1 = 1, so the last blackbox invoker will be scheduled\n    // Because the debugging invoker is excluded, it sends a message to invoker9.\n    stream.toString should include(s\"posting to invoker9\")\n  }\n\n  it should \"delete container\" in {\n    val mockEtcd = mock[EtcdClient]\n    val invokers: List[InvokerHealth] = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    )\n    val fqn = FullyQualifiedEntityName(EntityPath(\"ns1\"), EntityName(testAction))\n\n    expectGetInvokers(mockEtcd, invokers)\n\n    // both warmed and existing containers are in all invokers.\n    (mockEtcd\n      .getPrefix(_: String))\n      .expects(containerPrefix(ContainerKeys.namespacePrefix, testInvocationNamespace, fqn))\n      .returning(Future.successful {\n        invokers\n          .foldLeft(RangeResponse.newBuilder()) { (builder, invoker) =>\n            builder.addKvs(\n              KeyValue\n                .newBuilder()\n                .setKey(\n                  ContainerKeys.existingContainers(\n                    testInvocationNamespace,\n                    fqn,\n                    testRevision,\n                    Some(invoker.id),\n                    Some(ContainerId(\"testContainer\"))))\n                .build())\n          }\n          .build()\n      })\n\n    (mockEtcd\n      .getPrefix(_: String))\n      .expects(containerPrefix(ContainerKeys.warmedPrefix, testInvocationNamespace, fqn))\n      .returning(Future.successful {\n        invokers\n          .foldLeft(RangeResponse.newBuilder()) { (builder, invoker) =>\n            builder.addKvs(KeyValue\n              .newBuilder()\n              .setKey(ContainerKeys\n                .warmedContainers(testInvocationNamespace, fqn, testRevision, invoker.id, ContainerId(\"testContainer\")))\n              .build())\n          }\n          .build()\n      })\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n    val receiver = TestProbe()\n\n    val manager =\n      system.actorOf(ContainerManager\n        .props(factory(mockJobManager), mockMessaging(Some(receiver.ref)), testsid, mockEtcd, config, mockWatcher.ref))\n\n    // Consume warmUp messages for all invokers\n    (0 to 2).foreach { i =>\n      receiver.expectMsgPF() {\n        case msg: String if msg.contains(\"warmUp\") && msg.contains(s\"invoker$i\") => true\n      }\n    }\n\n    val msg = ContainerDeletionMessage(\n      TransactionId.containerDeletion,\n      testInvocationNamespace,\n      fqn,\n      testRevision,\n      actionMetadata)\n    val deletionMessage = ContainerDeletion(testInvocationNamespace, fqn, testRevision, actionMetadata)\n\n    manager ! deletionMessage\n\n    val expectedMsgs = invokers.map(i => s\"invoker${i.id.instance}-$msg\")\n\n    // Expect all 3 deletion messages\n    expectedMsgs.foreach { expectedMsg =>\n      receiver.expectMsgPF() {\n        case msg: String if msg == expectedMsg => true\n      }\n    }\n  }\n\n  it should \"allow managed partition to overlap with blackbox for small N\" in {\n    Seq((0.1, 0.9), (0.2, 0.8), (0.3, 0.7), (0.4, 0.6), (0.5, 0.5)).foreach { fraction =>\n      val blackboxFraction = fraction._1\n      val managedFraction = fraction._2\n\n      (1 to 100).toSeq.foreach { i =>\n        val m = Math.max(1, Math.ceil(i.toDouble * managedFraction).toInt)\n        val b = Math.max(1, Math.floor(i.toDouble * blackboxFraction).toInt)\n\n        m should be <= i\n        b shouldBe Math.max(1, (blackboxFraction * i).toInt)\n\n        blackboxFraction match {\n          case 0.1 if i < 10 => m + b shouldBe i + 1\n          case 0.2 if i < 5  => m + b shouldBe i + 1\n          case 0.3 if i < 4  => m + b shouldBe i + 1\n          case 0.4 if i < 3  => m + b shouldBe i + 1\n          case 0.5 if i < 2  => m + b shouldBe i + 1\n          case _             => m + b shouldBe i\n        }\n      }\n    }\n  }\n\n  it should \"return the same pools if managed- and blackbox-pools are overlapping\" in {\n    val blackboxFraction = 1.0\n    val managedFraction = 1.0\n    val totalInvokerSize = 100\n    var result = mutable.Buffer[InvokerHealth]()\n    (1 to totalInvokerSize).foreach { i =>\n      result = result :+ InvokerHealth(InvokerInstanceId(i, userMemory = 256.MB), Healthy)\n    }\n\n    val m = Math.max(1, Math.ceil(totalInvokerSize.toDouble * managedFraction).toInt)\n    val b = Math.max(1, Math.floor(totalInvokerSize.toDouble * blackboxFraction).toInt)\n\n    m shouldBe totalInvokerSize\n    b shouldBe totalInvokerSize\n\n    result.take(m) shouldBe result.takeRight(b)\n  }\n\n  behavior of \"warm up\"\n\n  it should \"warm up all invokers when start\" in {\n    val mockEtcd = mock[EtcdClient]\n\n    val invokers: List[InvokerHealth] = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = testMemory, tags = Seq.empty[String]), Healthy),\n    )\n    expectGetInvokers(mockEtcd, invokers)\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n    val receiver = TestProbe()\n\n    val manager =\n      system.actorOf(ContainerManager\n        .props(factory(mockJobManager), mockMessaging(Some(receiver.ref)), testsid, mockEtcd, config, mockWatcher.ref))\n\n    (0 to 2).foreach(i => {\n      receiver.expectMsgPF() {\n        case msg: String if msg.contains(\"warmUp\") && msg.contains(s\"invoker$i\") => true\n        case msg                                                                 => false\n      }\n    })\n  }\n\n  it should \"warm up new invoker when new one is registered\" in {\n    val mockEtcd = mock[EtcdClient]\n    expectGetInvokers(mockEtcd, List.empty)\n\n    val mockJobManager = TestProbe()\n    val mockWatcher = TestProbe()\n    val receiver = TestProbe()\n\n    val manager =\n      system.actorOf(ContainerManager\n        .props(factory(mockJobManager), mockMessaging(Some(receiver.ref)), testsid, mockEtcd, config, mockWatcher.ref))\n\n    manager ! WatchEndpointInserted(\n      InvokerKeys.prefix,\n      InvokerKeys.health(InvokerInstanceId(0, userMemory = 128.MB)),\n      \"\",\n      true)\n    receiver.expectMsgPF() {\n      case msg: String if msg.contains(\"warmUp\") && msg.contains(s\"invoker0\") => true\n      case _                                                                  => false\n    }\n\n    // shouldn't warmup again\n    manager ! WatchEndpointInserted(\n      InvokerKeys.prefix,\n      InvokerKeys.health(InvokerInstanceId(0, userMemory = 128.MB)),\n      \"\",\n      true)\n    receiver.expectNoMessage()\n\n    // should warmup again since invoker0 is a new one\n    manager ! WatchEndpointRemoved(\n      InvokerKeys.prefix,\n      InvokerKeys.health(InvokerInstanceId(0, userMemory = 128.MB)),\n      \"\",\n      true)\n    manager ! WatchEndpointInserted(\n      InvokerKeys.prefix,\n      InvokerKeys.health(InvokerInstanceId(0, userMemory = 128.MB)),\n      \"\",\n      true)\n    receiver.expectMsgPF() {\n      case msg: String if msg.contains(\"warmUp\") && msg.contains(s\"invoker0\") => true\n      case _                                                                  => false\n    }\n  }\n\n  it should \"choose an invoker from candidates\" in {\n    val candidates = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 128 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 128 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 256 MB), Healthy),\n    )\n    val msg = ContainerCreationMessage(\n      TransactionId.testing,\n      testInvocationNamespace,\n      FullyQualifiedEntityName(EntityPath(\"ns1\"), EntityName(testAction)),\n      testRevision,\n      actionMetadata,\n      testsid,\n      schedulerHost,\n      rpcPort)\n\n    // no matter how many time we schedule the msg, it should always choose invoker2.\n    (1 to 10).foreach { _ =>\n      val newPairs = ContainerManager.chooseInvokerFromCandidates(candidates, msg)\n      newPairs.invokerId shouldBe Some(InvokerInstanceId(2, userMemory = 256 MB))\n    }\n  }\n\n  it should \"not choose an invoker when there is no candidate with enough memory\" in {\n    val candidates = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 128 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 128 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 128 MB), Healthy),\n    )\n    val msg = ContainerCreationMessage(\n      TransactionId.testing,\n      testInvocationNamespace,\n      FullyQualifiedEntityName(EntityPath(\"ns1\"), EntityName(testAction)),\n      testRevision,\n      actionMetadata,\n      testsid,\n      schedulerHost,\n      rpcPort)\n\n    // no matter how many time we schedule the msg, no invoker should be assigned.\n    (1 to 10).foreach { _ =>\n      val newPairs = ContainerManager.chooseInvokerFromCandidates(candidates, msg)\n      newPairs.invokerId shouldBe None\n    }\n  }\n\n  it should \"not choose an invoker when there is no candidate\" in {\n    val candidates = List()\n    val msg = ContainerCreationMessage(\n      TransactionId.testing,\n      testInvocationNamespace,\n      FullyQualifiedEntityName(EntityPath(\"ns1\"), EntityName(testAction)),\n      testRevision,\n      actionMetadata,\n      testsid,\n      schedulerHost,\n      rpcPort)\n\n    val newPairs = ContainerManager.chooseInvokerFromCandidates(candidates, msg)\n    newPairs.invokerId shouldBe None\n  }\n\n  it should \"update invoker memory\" in {\n    val invokers = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 1024 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 1024 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 1024 MB), Healthy),\n    )\n    val expected = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 1024 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 768 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 1024 MB), Healthy),\n    )\n    val requiredMemory = 256.MB.toMB\n    val invokerId = Some(InvokerInstanceId(1, userMemory = 1024 MB))\n\n    val updatedInvokers = ContainerManager.updateInvokerMemory(invokerId, requiredMemory, invokers)\n\n    updatedInvokers shouldBe expected\n  }\n\n  it should \"not update invoker memory when no invoker is assigned\" in {\n    val invokers = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 1024 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 1024 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 1024 MB), Healthy),\n    )\n    val requiredMemory = 256.MB.toMB\n\n    val updatedInvokers = ContainerManager.updateInvokerMemory(None, requiredMemory, invokers)\n\n    updatedInvokers shouldBe invokers\n  }\n\n  it should \"drop an invoker with less memory than MIN_MEMORY\" in {\n    val invokers = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 1024 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = 320 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 1024 MB), Healthy),\n    )\n    val expected = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = 1024 MB), Healthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 1024 MB), Healthy),\n    )\n    val requiredMemory = 256.MB.toMB\n    val invokerId = Some(InvokerInstanceId(1, userMemory = 320 MB))\n\n    val updatedInvokers = ContainerManager.updateInvokerMemory(invokerId, requiredMemory, invokers)\n\n    updatedInvokers shouldBe expected\n  }\n\n  it should \"filter warmed creations when there is no warmed container\" in {\n\n    val warmedContainers = Set.empty[String]\n    val inProgressWarmedContainers = TrieMap.empty[String, String]\n\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        FullyQualifiedEntityName(EntityPath(\"ns1\"), EntityName(testAction)),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        FullyQualifiedEntityName(EntityPath(\"ns3\"), EntityName(testAction)),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg3 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        FullyQualifiedEntityName(EntityPath(\"ns3\"), EntityName(testAction)),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    val msgs = List(msg1, msg2, msg3)\n\n    val (coldCreations, warmedCreations) =\n      ContainerManager.filterWarmedCreations(warmedContainers, inProgressWarmedContainers, invokers, msgs)\n\n    warmedCreations.isEmpty shouldBe true\n    coldCreations.size shouldBe 3\n  }\n\n  it should \"filter warmed creations when there are warmed containers\" in {\n    val warmedContainers = Set(\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        InvokerInstanceId(0, userMemory = 0.bytes),\n        ContainerId(\"fake\")),\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn.copy(name = EntityName(\"test-action-2\")),\n        testRevision,\n        InvokerInstanceId(1, userMemory = 0.bytes),\n        ContainerId(\"fake\")))\n    val inProgressWarmedContainers = TrieMap.empty[String, String]\n\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.copy(name = EntityName(\"test-action-2\")),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg3 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    val msgs = List(msg1, msg2, msg3)\n\n    val (coldCreations, warmedCreations) =\n      ContainerManager.filterWarmedCreations(warmedContainers, inProgressWarmedContainers, invokers, msgs)\n\n    warmedCreations.size shouldBe 2\n    coldCreations.size shouldBe 1\n\n    warmedCreations.map(_._1).contains(msg1) shouldBe true\n    warmedCreations.map(_._1).contains(msg2) shouldBe true\n    coldCreations.map(_._1).contains(msg3) shouldBe true\n  }\n\n  it should \"choose cold creation when warmed containers are in disabled invokers\" in {\n    val warmedContainers = Set(\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        InvokerInstanceId(0, userMemory = 0.bytes),\n        ContainerId(\"fake\")),\n      ContainerKeys.warmedContainers(\n        testInvocationNamespace,\n        testfqn.copy(name = EntityName(\"test-action-2\")),\n        testRevision,\n        InvokerInstanceId(1, userMemory = 0.bytes),\n        ContainerId(\"fake\")))\n    val inProgressWarmedContainers = TrieMap.empty[String, String]\n\n    val msg1 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg2 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn.copy(name = EntityName(\"test-action-2\")),\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n    val msg3 =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        testfqn,\n        testRevision,\n        actionMetadata,\n        testsid,\n        schedulerHost,\n        rpcPort)\n\n    val msgs = List(msg1, msg2, msg3)\n\n    // unhealthy invokers should not be chosen even if they have warmed containers\n    val invokers: List[InvokerHealth] = List(\n      InvokerHealth(InvokerInstanceId(0, userMemory = testMemory, tags = Seq.empty[String]), Unhealthy),\n      InvokerHealth(InvokerInstanceId(1, userMemory = testMemory, tags = Seq.empty[String]), Unhealthy),\n      InvokerHealth(InvokerInstanceId(2, userMemory = 1024.MB, tags = Seq.empty[String]), Healthy))\n\n    val (coldCreations, _) =\n      ContainerManager.filterWarmedCreations(warmedContainers, inProgressWarmedContainers, invokers, msgs)\n\n    coldCreations.size shouldBe 3\n    coldCreations.map(_._1).containsSlice(List(msg1, msg2, msg3)) shouldBe true\n  }\n}\n\n@RunWith(classOf[JUnitRunner])\nclass ContainerManager2Tests\n    extends AnyFlatSpecLike\n    with Matchers\n    with StreamLogging\n    with ExecHelpers\n    with MockFactory\n    with ScalaFutures\n    with WskActorSystem\n    with BeforeAndAfterEach\n    with DbUtils {\n\n  implicit val dispatcher = actorSystem.dispatcher\n  val etcdClient = EtcdClient(loadConfigOrThrow[EtcdConfig](ConfigKeys.etcd))\n  val testInvocationNamespace = \"test-invocation-namespace\"\n\n  override def afterAll(): Unit = {\n    etcdClient.close()\n    super.afterAll()\n  }\n\n  it should \"load invoker from specified clusterName only\" in {\n    val clusterName1 = loadConfigOrThrow[String](ConfigKeys.whiskClusterName)\n    val clusterName2 = \"clusterName2\"\n    val invokerResourceMessage =\n      InvokerResourceMessage(Healthy.asString, 1024, 0, 0, Seq.empty[String], Seq.empty[String])\n    etcdClient.put(s\"${clusterName1}/invokers/0\", invokerResourceMessage.serialize)\n    etcdClient.put(s\"${clusterName1}/invokers/1\", invokerResourceMessage.serialize)\n    etcdClient.put(s\"${clusterName1}/invokers/2\", invokerResourceMessage.serialize)\n    etcdClient.put(s\"${clusterName2}/invokers/3\", invokerResourceMessage.serialize)\n    etcdClient.put(s\"${clusterName2}/invokers/4\", invokerResourceMessage.serialize)\n    etcdClient.put(s\"${clusterName2}/invokers/5\", invokerResourceMessage.serialize)\n    // Make sure store above data in etcd\n    Thread.sleep(5.seconds.toMillis)\n    ContainerManager.getAvailableInvokers(etcdClient, 0.MB, testInvocationNamespace).map { invokers =>\n      invokers.length shouldBe 3\n      invokers.foreach { invokerHealth =>\n        List(0, 1, 2) should contain(invokerHealth.id.instance)\n      }\n    }\n    // Delete etcd data finally\n    List(\n      s\"${clusterName1}/invokers/0\",\n      s\"${clusterName1}/invokers/1\",\n      s\"${clusterName1}/invokers/2\",\n      s\"${clusterName2}/invokers/3\",\n      s\"${clusterName2}/invokers/4\",\n      s\"${clusterName2}/invokers/5\").foreach(etcdClient.del(_))\n  }\n\n  it should \"load invoker from specified invocation namespace only\" in {\n    val clusterName = loadConfigOrThrow[String](ConfigKeys.whiskClusterName)\n    val invokerResourceMessage =\n      InvokerResourceMessage(Healthy.asString, 1024, 0, 0, Seq.empty[String], Seq.empty[String])\n    val invokerResourceMessage2 =\n      InvokerResourceMessage(Healthy.asString, 1024, 0, 0, Seq.empty[String], Seq(testInvocationNamespace))\n    etcdClient.put(s\"${clusterName}/invokers/0\", invokerResourceMessage.serialize)\n    etcdClient.put(s\"${clusterName}/invokers/1\", invokerResourceMessage.serialize)\n    etcdClient.put(s\"${clusterName}/invokers/2\", invokerResourceMessage.serialize)\n    etcdClient.put(s\"${clusterName}/invokers/3\", invokerResourceMessage2.serialize)\n    etcdClient.put(s\"${clusterName}/invokers/4\", invokerResourceMessage2.serialize)\n    etcdClient.put(s\"${clusterName}/invokers/5\", invokerResourceMessage2.serialize)\n    // Make sure store above data in etcd\n    Thread.sleep(5.seconds.toMillis)\n    ContainerManager.getAvailableInvokers(etcdClient, 0.MB, testInvocationNamespace).map { invokers =>\n      invokers.length shouldBe 6\n      invokers.foreach { invokerHealth =>\n        List(0, 1, 2, 3, 4, 5) should contain(invokerHealth.id.instance)\n      }\n    }\n\n    // this new namespace should not use invoker3/4/5\n    ContainerManager.getAvailableInvokers(etcdClient, 0.MB, \"new-namespace\").map { invokers =>\n      invokers.length shouldBe 3\n      invokers.foreach { invokerHealth =>\n        List(0, 1, 2) should contain(invokerHealth.id.instance)\n      }\n    }\n    // Delete etcd data finally\n    List(\n      s\"${clusterName}/invokers/0\",\n      s\"${clusterName}/invokers/1\",\n      s\"${clusterName}/invokers/2\",\n      s\"${clusterName}/invokers/3\",\n      s\"${clusterName}/invokers/4\",\n      s\"${clusterName}/invokers/5\").foreach(etcdClient.del(_))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/container/test/CreationJobManagerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.container.test\n\nimport org.apache.pekko.actor.{ActorRef, ActorRefFactory, ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestActorRef, TestKit, TestProbe}\nimport com.ibm.etcd.client.{EtcdClient => Client}\nimport common.StreamLogging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.inProgressContainer\nimport org.apache.openwhisk.core.scheduler.container._\nimport org.apache.openwhisk.core.scheduler.message._\nimport org.apache.openwhisk.core.scheduler.queue.{MemoryQueueKey, MemoryQueueValue, QueuePool}\nimport org.apache.openwhisk.core.service.{RegisterData, UnregisterData}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\nimport java.util.concurrent.TimeUnit\nimport scala.concurrent.duration.{DurationInt, FiniteDuration}\nimport scala.concurrent.{ExecutionContextExecutor, Future}\n\n@RunWith(classOf[JUnitRunner])\nclass CreationJobManagerTests\n    extends TestKit(ActorSystem(\"CreationJobManager\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with StreamLogging {\n\n  val timeout = 20.seconds\n  val blackboxMultiple = 2\n  val blackboxTimeout = FiniteDuration(timeout.toSeconds * blackboxMultiple, TimeUnit.SECONDS)\n  implicit val ece: ExecutionContextExecutor = system.dispatcher\n  val config = new WhiskConfig(ExecManifest.requiredProperties)\n  val creationIdTest = CreationId.generate()\n  val isBlackboxInvocation = false\n\n  val testInvocationNamespace = \"test-invocation-namespace\"\n  val testNamespace = \"test-namespace\"\n  val testAction = \"test-action\"\n  val schedulerHost = \"127.17.0.1\"\n  val rpcPort = 13001\n  val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n  val execAction = ExecutableWhiskAction(EntityPath(testNamespace), EntityName(testAction), exec)\n  val execMetadata =\n    CodeExecMetaDataAsString(RuntimeManifest(execAction.exec.kind, ImageName(\"test\")), entryPoint = Some(\"test\"))\n  val revision = DocRevision(\"1-testRev\")\n  val actionMetadata =\n    WhiskActionMetaData(\n      execAction.namespace,\n      execAction.name,\n      execMetadata,\n      execAction.parameters,\n      execAction.limits,\n      execAction.version,\n      execAction.publish,\n      execAction.annotations)\n\n  override def afterAll(): Unit = {\n    client.close()\n    QueuePool.clear()\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  override def beforeEach(): Unit = {\n    QueuePool.clear()\n  }\n\n  def feedFactory(actorRefFactory: ActorRefFactory,\n                  description: String,\n                  topic: String,\n                  maxActiveAcksPerPoll: Int,\n                  handler: Array[Byte] => Future[Unit]): ActorRef = {\n    TestProbe().ref\n  }\n\n  def createRegisterMessage(action: FullyQualifiedEntityName,\n                            revision: DocRevision,\n                            sid: SchedulerInstanceId): RegisterCreationJob = {\n    val message =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        action,\n        revision,\n        actionMetadata,\n        sid,\n        schedulerHost,\n        rpcPort,\n        creationId = creationIdTest)\n    RegisterCreationJob(message)\n  }\n\n  val action = FullyQualifiedEntityName(EntityPath(\"test namespace\"), EntityName(\"actionName\"))\n  val sid = SchedulerInstanceId(\"0\")\n  val iid = InvokerInstanceId(0, userMemory = 1024.MB)\n  val testKey = inProgressContainer(testInvocationNamespace, action, revision, sid, creationIdTest)\n  val memory = 256.MB\n  val resources = Seq.empty[String]\n  val resourcesStrictPolicy = true\n\n  val registerMessage = createRegisterMessage(action, revision, sid)\n\n  val client: Client = {\n    val hostAndPorts = \"172.17.0.1:2379\"\n    Client.forEndpoints(hostAndPorts).withPlainText().build()\n  }\n\n  behavior of \"CreationJobManager\"\n\n  it should \"register creation job\" in {\n    val probe = TestProbe()\n\n    val manager = TestActorRef(new CreationJobManager(feedFactory, sid, probe.ref, timeout, blackboxMultiple))\n\n    manager ! registerMessage\n\n    probe.expectMsg(RegisterData(testKey, \"\", failoverEnabled = false))\n  }\n\n  it should \"skip duplicated creation job\" in {\n    val probe = TestProbe()\n\n    val manager = TestActorRef(new CreationJobManager(feedFactory, sid, probe.ref, timeout, blackboxMultiple))\n\n    manager ! registerMessage\n    manager ! registerMessage\n\n    probe.expectMsg(RegisterData(testKey, \"\", failoverEnabled = false))\n    probe.expectNoMessage()\n  }\n\n  def createFinishMessage(action: FullyQualifiedEntityName,\n                          revision: DocRevision,\n                          memory: ByteSize,\n                          invokerInstanceId: InvokerInstanceId,\n                          retryCount: Int = 0,\n                          error: Option[ContainerCreationError] = None): FinishCreationJob = {\n    val message =\n      ContainerCreationAckMessage(\n        TransactionId.testing,\n        creationIdTest,\n        testInvocationNamespace,\n        action,\n        revision,\n        actionMetadata,\n        invokerInstanceId,\n        schedulerHost,\n        rpcPort,\n        retryCount,\n        error)\n    FinishCreationJob(message)\n  }\n\n  def createRescheduling(finishMsg: FinishCreationJob): ReschedulingCreationJob =\n    ReschedulingCreationJob(\n      finishMsg.ack.transid,\n      finishMsg.ack.creationId,\n      finishMsg.ack.invocationNamespace,\n      finishMsg.ack.action,\n      finishMsg.ack.revision,\n      actionMetadata,\n      finishMsg.ack.schedulerHost,\n      finishMsg.ack.rpcPort,\n      finishMsg.ack.retryCount)\n\n  val normalFinish = createFinishMessage(action, revision, memory, iid, retryCount = 0)\n  val failedFinish =\n    createFinishMessage(action, revision, memory, iid, retryCount = 0, Some(ContainerCreationError.UnknownError))\n  val unrescheduleFinish =\n    createFinishMessage(action, revision, memory, iid, retryCount = 0, Some(ContainerCreationError.BlackBoxError))\n  val tooManyFinish =\n    createFinishMessage(action, revision, memory, iid, retryCount = 100, Some(ContainerCreationError.UnknownError))\n\n  it should \"delete a creation job normally and send a SuccessfulCreationJob to a queue\" in {\n    val containerManager = TestProbe()\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n    val jobManager = TestActorRef(\n      Props(new CreationJobManager(feedFactory, sid, dataManagementService.ref, timeout, blackboxMultiple)),\n      containerManager.ref)\n\n    QueuePool.put(\n      MemoryQueueKey(testInvocationNamespace, action.toDocId.asDocInfo(revision)),\n      MemoryQueueValue(probe.ref, true))\n    jobManager ! registerMessage\n\n    dataManagementService.expectMsg(RegisterData(testKey, \"\", failoverEnabled = false))\n\n    jobManager ! normalFinish\n\n    dataManagementService.expectMsg(UnregisterData(testKey))\n\n    containerManager.expectMsg(\n      SuccessfulCreationJob(\n        normalFinish.ack.creationId,\n        normalFinish.ack.invocationNamespace,\n        registerMessage.msg.action,\n        registerMessage.msg.revision))\n    probe.expectMsg(\n      SuccessfulCreationJob(\n        normalFinish.ack.creationId,\n        normalFinish.ack.invocationNamespace,\n        registerMessage.msg.action,\n        registerMessage.msg.revision))\n  }\n\n  it should \"only delete a creation job with failed msg after all retries are failed\" in {\n    val containerManager = TestProbe()\n    val dataManagementService = TestProbe()\n\n    val jobManager = TestActorRef(\n      Props(new CreationJobManager(feedFactory, sid, dataManagementService.ref, timeout, blackboxMultiple)),\n      containerManager.ref)\n\n    jobManager ! registerMessage\n\n    dataManagementService.expectMsg(RegisterData(testKey, \"\", failoverEnabled = false))\n\n    jobManager ! failedFinish\n\n    containerManager.expectMsg(createRescheduling(failedFinish))\n\n    jobManager ! failedFinish.copy(ack = failedFinish.ack.copy(retryCount = 5))\n    dataManagementService.expectMsg(UnregisterData(testKey))\n  }\n\n  it should \"delete a creation job with failed msg and send a FailedCreationJob to a queue\" in {\n    val containerManager = TestProbe()\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n    val jobManager = TestActorRef(\n      Props(new CreationJobManager(feedFactory, sid, dataManagementService.ref, timeout, blackboxMultiple)),\n      containerManager.ref)\n\n    QueuePool.put(\n      MemoryQueueKey(testInvocationNamespace, action.toDocId.asDocInfo(revision)),\n      MemoryQueueValue(probe.ref, true))\n\n    jobManager ! registerMessage\n\n    dataManagementService.expectMsg(RegisterData(testKey, \"\", failoverEnabled = false))\n\n    jobManager ! unrescheduleFinish\n\n    dataManagementService.expectMsg(UnregisterData(testKey))\n\n    containerManager.expectMsg(\n      FailedCreationJob(\n        registerMessage.msg.creationId,\n        registerMessage.msg.invocationNamespace,\n        registerMessage.msg.action,\n        registerMessage.msg.revision,\n        ContainerCreationError.BlackBoxError,\n        \"unknown reason\"))\n    probe.expectMsg(\n      FailedCreationJob(\n        registerMessage.msg.creationId,\n        registerMessage.msg.invocationNamespace,\n        registerMessage.msg.action,\n        registerMessage.msg.revision,\n        ContainerCreationError.BlackBoxError,\n        \"unknown reason\"))\n  }\n\n  it should \"delete a creation job that does not exist with failed msg\" in {\n    val containerManager = TestProbe()\n    val dataManagementService = TestProbe()\n\n    val jobManager = TestActorRef(\n      Props(new CreationJobManager(feedFactory, sid, dataManagementService.ref, timeout, blackboxMultiple)),\n      containerManager.ref)\n\n    jobManager ! failedFinish.copy(ack = failedFinish.ack.copy(retryCount = 5))\n\n    dataManagementService.expectMsg(UnregisterData(testKey))\n  }\n\n  it should \"delete a creation job with timeout\" in {\n    val containerManager = TestProbe()\n    val dataManagementService = TestProbe()\n\n    val jobManager = TestActorRef(\n      Props(new CreationJobManager(feedFactory, sid, dataManagementService.ref, timeout, blackboxMultiple)),\n      containerManager.ref)\n\n    jobManager ! registerMessage\n\n    dataManagementService.expectMsg(RegisterData(testKey, \"\", failoverEnabled = false))\n\n    Thread.sleep(timeout.toMillis) // sleep 5s to wait for the timeout handler to be executed\n    dataManagementService.expectMsg(UnregisterData(testKey))\n    containerManager.expectMsg(\n      FailedCreationJob(\n        registerMessage.msg.creationId,\n        registerMessage.msg.invocationNamespace,\n        registerMessage.msg.action,\n        registerMessage.msg.revision,\n        ContainerCreationError.TimeoutError,\n        s\"[${registerMessage.msg.action}] timeout waiting for the ack of ${registerMessage.msg.creationId} after $timeout\"))\n  }\n\n  it should \"increase the timeout if an action is a blackbox action\" in {\n    val containerManager = TestProbe()\n    val dataManagementService = TestProbe()\n\n    val jobManager = TestActorRef(\n      Props(new CreationJobManager(feedFactory, sid, dataManagementService.ref, timeout, blackboxMultiple)),\n      containerManager.ref)\n\n    val execMetadata =\n      BlackBoxExecMetaData(ImageName(\"test image\"), Some(\"main\"), native = false);\n\n    val actionMetaData = WhiskActionMetaData(\n      execAction.namespace,\n      execAction.name,\n      execMetadata,\n      execAction.parameters,\n      execAction.limits,\n      execAction.version,\n      execAction.publish,\n      execAction.annotations)\n\n    val message =\n      ContainerCreationMessage(\n        TransactionId.testing,\n        testInvocationNamespace,\n        action,\n        revision,\n        actionMetaData,\n        sid,\n        schedulerHost,\n        rpcPort,\n        creationId = creationIdTest)\n    val creationMsg = RegisterCreationJob(message)\n\n    jobManager ! creationMsg\n\n    dataManagementService.expectMsg(RegisterData(testKey, \"\", failoverEnabled = false))\n\n    // no message for timeout\n    dataManagementService.expectNoMessage(timeout)\n    Thread.sleep(timeout.toMillis * blackboxMultiple) // timeout is doubled for blackbox actions\n    dataManagementService.expectMsg(UnregisterData(testKey))\n    containerManager.expectMsg(\n      FailedCreationJob(\n        registerMessage.msg.creationId,\n        registerMessage.msg.invocationNamespace,\n        registerMessage.msg.action,\n        registerMessage.msg.revision,\n        ContainerCreationError.TimeoutError,\n        s\"[${registerMessage.msg.action}] timeout waiting for the ack of ${registerMessage.msg.creationId} after $blackboxTimeout\"))\n  }\n\n  it should \"delete a creation job with too many retry and send a FailedCreationJob to a queue\" in {\n    val containerManager = TestProbe()\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n    val jobManager = TestActorRef(\n      Props(new CreationJobManager(feedFactory, sid, dataManagementService.ref, timeout, blackboxMultiple)),\n      containerManager.ref)\n\n    QueuePool.put(\n      MemoryQueueKey(testInvocationNamespace, action.toDocId.asDocInfo(revision)),\n      MemoryQueueValue(probe.ref, true))\n\n    jobManager ! registerMessage\n\n    dataManagementService.expectMsg(RegisterData(testKey, \"\", failoverEnabled = false))\n\n    jobManager ! tooManyFinish\n\n    dataManagementService.expectMsg(UnregisterData(testKey))\n    containerManager.expectMsg(\n      FailedCreationJob(\n        registerMessage.msg.creationId,\n        registerMessage.msg.invocationNamespace,\n        registerMessage.msg.action,\n        registerMessage.msg.revision,\n        ContainerCreationError.UnknownError,\n        \"unknown reason\"))\n    probe.expectMsg(\n      FailedCreationJob(\n        registerMessage.msg.creationId,\n        registerMessage.msg.invocationNamespace,\n        registerMessage.msg.action,\n        registerMessage.msg.revision,\n        ContainerCreationError.UnknownError,\n        \"unknown reason\"))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/grpc/test/ActivationServiceImplTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.grpc.test\n\nimport org.apache.pekko.actor.{Actor, ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit}\nimport common.StreamLogging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.WarmUp.warmUpAction\nimport org.apache.openwhisk.core.connector.ActivationMessage\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.scheduler.grpc.ActivationServiceImpl\nimport org.apache.openwhisk.core.scheduler.queue.{\n  ActionMismatch,\n  MemoryQueueKey,\n  MemoryQueueValue,\n  NoActivationMessage,\n  NoMemoryQueue,\n  QueuePool\n}\nimport org.apache.openwhisk.grpc.{FetchRequest, FetchResponse, RescheduleRequest, RescheduleResponse}\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.core.scheduler.grpc.{ActivationResponse, GetActivation}\nimport org.scalatest.concurrent.ScalaFutures\nimport spray.json.JsonParser.ParsingException\n\nimport scala.concurrent.{Await, Future}\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass ActivationServiceImplTests\n    extends TestKit(ActorSystem(\"ActivationService\"))\n    with CommonVariable\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with ScalaFutures\n    with StreamLogging {\n\n  override def afterAll = {\n    QueuePool.clear()\n    TestKit.shutdownActorSystem(system)\n  }\n  override def beforeEach = QueuePool.clear()\n\n  private def await[T](awaitable: Future[T], timeout: FiniteDuration = 10.seconds) = Await.result(awaitable, timeout)\n\n  implicit val timeoutConfig = PatienceConfig(10.seconds)\n\n  behavior of \"ActivationService\"\n\n  implicit val ec = system.dispatcher\n\n  val messageTransId = TransactionId(TransactionId.testing.meta.id)\n  val uuid = UUID()\n\n  val testDoc = testFQN.toDocId.asDocInfo(testDocRevision)\n  val message = ActivationMessage(\n    messageTransId,\n    FullyQualifiedEntityName(testEntityPath, testEntityName),\n    DocRevision.empty,\n    Identity(\n      Subject(),\n      Namespace(EntityName(testNamespace), uuid),\n      BasicAuthenticationAuthKey(uuid, Secret()),\n      Set.empty),\n    ActivationId.generate(),\n    ControllerInstanceId(\"0\"),\n    blocking = false,\n    content = None)\n\n  it should \"delegate the FetchRequest to a MemoryQueue\" in {\n\n    val mock = system.actorOf(Props(new Actor() {\n      override def receive: Receive = {\n        case getActivation: GetActivation =>\n          testActor ! getActivation\n          sender() ! ActivationResponse(Right(message))\n      }\n    }))\n\n    QueuePool.put(MemoryQueueKey(testEntityPath.asString, testDoc), MemoryQueueValue(mock, true))\n    val activationServiceImpl = ActivationServiceImpl()\n\n    val tid = TransactionId(TransactionId.generateTid())\n    activationServiceImpl\n      .fetchActivation(\n        FetchRequest(\n          tid.serialize,\n          message.user.namespace.name.asString,\n          testFQN.serialize,\n          testDocRevision.serialize,\n          testContainerId,\n          false,\n          alive = true))\n      .futureValue shouldBe FetchResponse(ActivationResponse(Right(message)).serialize)\n\n    expectMsg(GetActivation(tid, testFQN, testContainerId, false, None))\n  }\n\n  it should \"return without any retry if there is no such queue\" in {\n    val activationServiceImpl = ActivationServiceImpl()\n\n    activationServiceImpl\n      .fetchActivation(\n        FetchRequest(\n          TransactionId(TransactionId.generateTid()).serialize,\n          message.user.namespace.name.asString,\n          testFQN.serialize,\n          testDocRevision.serialize,\n          testContainerId,\n          false,\n          alive = true))\n      .futureValue shouldBe FetchResponse(ActivationResponse(Left(NoMemoryQueue())).serialize)\n\n    expectNoMessage(200.millis)\n  }\n\n  it should \"return ActionMismatchError if get request for an old action\" in {\n\n    val activationServiceImpl = ActivationServiceImpl()\n\n    QueuePool.put(MemoryQueueKey(testEntityPath.asString, testDoc), MemoryQueueValue(testActor, true))\n\n    activationServiceImpl\n      .fetchActivation(\n        FetchRequest( // same doc id but with a different doc revision\n          TransactionId(TransactionId.generateTid()).serialize,\n          message.user.namespace.name.asString,\n          testFQN.serialize,\n          DocRevision(\"new-one\").serialize,\n          testContainerId,\n          false,\n          alive = true))\n      .futureValue shouldBe FetchResponse(ActivationResponse(Left(ActionMismatch())).serialize)\n\n    expectNoMessage(200.millis)\n  }\n\n  it should \"return NoActivationMessage if queue doesn't return response\" in {\n\n    val activationServiceImpl = ActivationServiceImpl()\n\n    QueuePool.put(MemoryQueueKey(testEntityPath.asString, testDoc), MemoryQueueValue(testActor, true))\n\n    val tid = TransactionId(TransactionId.generateTid())\n    activationServiceImpl\n      .fetchActivation(\n        FetchRequest(\n          tid.serialize,\n          message.user.namespace.name.asString,\n          testFQN.serialize,\n          testDocRevision.serialize,\n          testContainerId,\n          false,\n          alive = true))\n      .futureValue shouldBe FetchResponse(ActivationResponse(Left(NoActivationMessage())).serialize)\n\n    expectMsg(GetActivation(tid, testFQN, testContainerId, false, None))\n  }\n\n  it should \"return NoActivationMessage if it is a warm-up action\" in {\n\n    val activationServiceImpl = ActivationServiceImpl()\n\n    QueuePool.put(MemoryQueueKey(testEntityPath.asString, testDoc), MemoryQueueValue(testActor, true))\n\n    activationServiceImpl\n      .fetchActivation(\n        FetchRequest(\n          TransactionId(TransactionId.generateTid()).serialize,\n          message.user.namespace.name.asString,\n          warmUpAction.serialize,\n          testDocRevision.serialize,\n          testContainerId,\n          false,\n          alive = true))\n      .futureValue shouldBe FetchResponse(ActivationResponse(Left(NoActivationMessage())).serialize)\n\n    expectNoMessage(200.millis)\n  }\n\n  it should \"throw parsing error if fqn can't be parsed\" in {\n    val notParsableFqn = \"aaaaaaaaa\"\n\n    val activationServiceImpl = ActivationServiceImpl()\n\n    QueuePool.put(MemoryQueueKey(testEntityPath.asString, testDoc), MemoryQueueValue(testActor, true))\n\n    a[ParsingException] should be thrownBy await {\n      activationServiceImpl\n        .fetchActivation(\n          FetchRequest(\n            TransactionId(TransactionId.generateTid()).serialize,\n            message.user.namespace.name.asString,\n            notParsableFqn,\n            testDocRevision.serialize,\n            testContainerId,\n            false,\n            alive = true))\n    }\n  }\n\n  it should \"throw parsing error if rev can't be parsed\" in {\n    val notParsableRev = \"aaaaaaaaa\"\n\n    val activationServiceImpl = ActivationServiceImpl()\n\n    QueuePool.put(MemoryQueueKey(testEntityPath.asString, testDoc), MemoryQueueValue(testActor, true))\n\n    a[ParsingException] should be thrownBy await {\n      activationServiceImpl\n        .fetchActivation(\n          FetchRequest(\n            TransactionId(TransactionId.generateTid()).serialize,\n            message.user.namespace.name.asString,\n            testFQN.serialize,\n            notParsableRev,\n            testContainerId,\n            false,\n            alive = true))\n    }\n  }\n\n  it should \"reschedule msg if related queue exist\" in {\n    QueuePool.put(MemoryQueueKey(testEntityPath.asString, testDoc), MemoryQueueValue(testActor, true))\n    val activationServiceImpl = ActivationServiceImpl()\n\n    activationServiceImpl\n      .rescheduleActivation(\n        RescheduleRequest(\n          message.user.namespace.name.asString,\n          testFQN.serialize,\n          testDocRevision.serialize,\n          message.serialize))\n      .futureValue shouldBe RescheduleResponse(true)\n\n    expectMsg(message)\n  }\n\n  it should \"not reschedule msg if queue doesn't exist\" in {\n    val activationServiceImpl = ActivationServiceImpl()\n\n    activationServiceImpl\n      .rescheduleActivation(\n        RescheduleRequest(\n          message.user.namespace.name.asString,\n          testFQN.serialize,\n          testDocRevision.serialize,\n          message.serialize))\n      .futureValue shouldBe RescheduleResponse()\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/grpc/test/CommonVariable.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.grpc.test\n\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\n\ntrait CommonVariable {\n  val testInvocationNamespace = \"test-invocation-namespace\"\n  val testInvocationEntityPath = EntityPath(testInvocationNamespace)\n  val testNamespace = \"test-namespace\"\n  val testEntityPath = EntityPath(testNamespace)\n  val testAction = \"test-fqn\"\n  val testEntityName = EntityName(testAction)\n  val testDocRevision = DocRevision(\"1-test-revision\")\n  val testContainerId = \"fakeContainerId\"\n  val semVer = SemVer(0, 1, 1)\n  val testVersion = Some(semVer)\n  val testFQN = FullyQualifiedEntityName(testEntityPath, testEntityName, testVersion)\n  val testExec = CodeExecAsString(RuntimeManifest(\"nodejs:20\", ImageName(\"testImage\")), \"testCode\", None)\n  val testExecMetadata =\n    CodeExecMetaDataAsString(testExec.manifest, entryPoint = testExec.entryPoint)\n  val testActionMetaData =\n    WhiskActionMetaData(testEntityPath, testEntityName, testExecMetadata, version = semVer)\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/queue/test/ContainerCounterTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue.test\n\nimport java.{lang, util}\nimport java.util.concurrent.Executor\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.{TestKit, TestProbe}\nimport com.google.protobuf.ByteString\nimport com.ibm.etcd.api.Event.EventType\nimport com.ibm.etcd.api.{Event, KeyValue, LeaseKeepAliveResponse, ResponseHeader, TxnResponse}\nimport com.ibm.etcd.client.kv.KvClient.Watch\nimport com.ibm.etcd.client.kv.WatchUpdate\nimport com.ibm.etcd.client.{EtcdClient => Client}\nimport common.StreamLogging\nimport org.apache.openwhisk.core.entity.{\n  CreationId,\n  DocRevision,\n  EntityName,\n  EntityPath,\n  FullyQualifiedEntityName,\n  SchedulerInstanceId\n}\nimport org.apache.openwhisk.core.etcd.EtcdClient\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.inProgressContainer\nimport org.apache.openwhisk.core.scheduler.queue.NamespaceContainerCount\nimport org.apache.openwhisk.core.service.{DeleteEvent, PutEvent, UnwatchEndpoint, WatchEndpoint, WatcherService}\nimport org.apache.openwhisk.utils.retry\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass ContainerCounterTests\n    extends TestKit(ActorSystem(\"ContainerCounter\"))\n    with AnyFlatSpecLike\n    with Matchers\n    with MockFactory\n    with ScalaFutures\n    with StreamLogging {\n\n  private implicit val ec = system.dispatcher\n\n  private val namespace = \"testNamespace\"\n  private val namespace2 = \"testNamespace2\"\n  private val action = \"testAction\"\n  private val action2 = \"testAction2\"\n  private val schedulerId = SchedulerInstanceId(\"0\")\n  private val fqn = FullyQualifiedEntityName(EntityPath(namespace), EntityName(action))\n  private val revision = DocRevision(\"1-testRev1\")\n  private val fqn2 = FullyQualifiedEntityName(EntityPath(namespace), EntityName(action2))\n  private val revision2 = DocRevision(\"1-testRev2\")\n  private val fqn3 = FullyQualifiedEntityName(EntityPath(namespace2), EntityName(action2))\n  private val revision3 = DocRevision(\"1-testRev3\")\n  private val watcherName = s\"container-counter-$namespace\"\n  private val inProgressContainerPrefixKeyByNamespace =\n    ContainerKeys.inProgressContainerPrefixByNamespace(namespace)\n  private val existingContainerPrefixKeyByNamespace =\n    ContainerKeys.existingContainersPrefixByNamespace(namespace)\n\n  val client: Client = {\n    val hostAndPorts = \"172.17.0.1:2379\"\n    Client.forEndpoints(hostAndPorts).withPlainText().build()\n  }\n\n  it should \"be shared for a same namespace\" in {\n    val etcd = mock[EtcdClient]\n    val watcher = TestProbe()\n    val res = Future.sequence {\n      (0 to 99).map { _ =>\n        Future {\n          NamespaceContainerCount(namespace, etcd, watcher.ref)\n        }\n      }\n    }.futureValue\n\n    // only create one instance\n    res.toSet.size shouldBe 1\n    res.head.references.intValue shouldBe 100\n\n    // only register watch endpoint once\n    watcher.expectMsgAllOf(\n      WatchEndpoint(inProgressContainerPrefixKeyByNamespace, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(existingContainerPrefixKeyByNamespace, \"\", true, watcherName, Set(PutEvent, DeleteEvent)))\n    watcher.expectNoMessage()\n    NamespaceContainerCount.instances.size shouldBe 1\n    NamespaceContainerCount.instances.clear()\n  }\n\n  it should \"and only should be closed when all references are closed\" in {\n    val etcd = mock[EtcdClient]\n    val watcher = TestProbe()\n    val res = Future.sequence {\n      (0 to 99).map { _ =>\n        Future {\n          NamespaceContainerCount(namespace, etcd, watcher.ref)\n        }\n      }\n    }.futureValue\n\n    // only create one instance\n    res.toSet.size shouldBe 1\n    res.head.references.intValue shouldBe 100\n\n    // only register watch endpoint once\n    watcher.expectMsgAllOf(\n      WatchEndpoint(inProgressContainerPrefixKeyByNamespace, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(existingContainerPrefixKeyByNamespace, \"\", true, watcherName, Set(PutEvent, DeleteEvent)))\n    watcher.expectNoMessage()\n    NamespaceContainerCount.instances.size shouldBe 1\n\n    // close 50 times\n    Future.sequence {\n      (0 to 49).map { _ =>\n        Future(res.head.close())\n      }\n    }.futureValue\n    res.head.references.intValue shouldBe 50\n\n    // should not unregister watch endpoint\n    watcher.expectNoMessage()\n    NamespaceContainerCount.instances.size shouldBe 1\n\n    // close left 50 times\n    Future.sequence {\n      (0 to 49).map { _ =>\n        Future(res.head.close())\n      }\n    }.futureValue\n    res.head.references.intValue shouldBe 0\n\n    // only unregister watch endpoint once\n    watcher.expectMsgAllOf(\n      UnwatchEndpoint(inProgressContainerPrefixKeyByNamespace, true, watcherName),\n      UnwatchEndpoint(existingContainerPrefixKeyByNamespace, true, watcherName))\n    watcher.expectNoMessage()\n    NamespaceContainerCount.instances.size shouldBe 0\n  }\n\n  it should \"update the number of containers based on Watch event\" in {\n    val mockEtcdClient = new MockEtcdClient(client, true)\n    val watcher = system.actorOf(WatcherService.props(mockEtcdClient))\n\n    val ns = NamespaceContainerCount(namespace, mockEtcdClient, watcher)\n    Thread.sleep(1000)\n\n    ns.inProgressContainerNumByNamespace shouldBe 0\n    ns.existingContainerNumByNamespace shouldBe 0\n\n    val invoker = \"invoker0\"\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      inProgressContainer(namespace, fqn, revision, schedulerId, CreationId(\"testId\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      s\"${ContainerKeys.existingContainers(namespace, fqn, DocRevision.empty)}/${invoker}/test-container\",\n      \"test-value\")\n\n    Thread.sleep(1000)\n    ns.inProgressContainerNumByNamespace shouldBe 1\n    ns.existingContainerNumByNamespace shouldBe 1\n\n    // other action's containers under same namespace should have effect\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      inProgressContainer(namespace, fqn2, revision2, schedulerId, CreationId(\"testId2\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      s\"${ContainerKeys.existingContainers(namespace, fqn2, DocRevision.empty)}/${invoker}/test-container2\",\n      \"test-value\")\n\n    Thread.sleep(1000)\n    ns.inProgressContainerNumByNamespace shouldBe 2\n    ns.existingContainerNumByNamespace shouldBe 2\n\n    // other namespace's containers should have no influence\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      inProgressContainer(namespace2, fqn3, revision3, schedulerId, CreationId(\"testId3\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      s\"${ContainerKeys.existingContainers(namespace2, fqn3, DocRevision.empty)}/${invoker}/test-container3\",\n      \"test-value\")\n\n    Thread.sleep(1000)\n    ns.inProgressContainerNumByNamespace shouldBe 2\n    ns.existingContainerNumByNamespace shouldBe 2\n\n    // inProgress containers should have no effect on existing containers\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      inProgressContainer(namespace, fqn, revision, schedulerId, CreationId(\"testId\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      inProgressContainer(namespace, fqn2, revision2, schedulerId, CreationId(\"testId2\")),\n      \"test-value\")\n\n    Thread.sleep(1000)\n    ns.inProgressContainerNumByNamespace shouldBe 0\n    ns.existingContainerNumByNamespace shouldBe 2\n\n    // existing containers should have no effect on inProgress containers\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      s\"${ContainerKeys.existingContainers(namespace, fqn, DocRevision.empty)}/${invoker}/test-container\",\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      s\"${ContainerKeys.existingContainers(namespace, fqn2, DocRevision.empty)}/${invoker}/test-container2\",\n      \"test-value\")\n\n    Thread.sleep(1000)\n    ns.inProgressContainerNumByNamespace shouldBe 0\n    ns.existingContainerNumByNamespace shouldBe 0\n\n    NamespaceContainerCount.instances.clear()\n  }\n\n  it should \"update the number of containers correctly when multiple entries are inserted into etcd\" in {\n    val mockEtcdClient = new MockEtcdClient(client, true, failedCount = 1)\n    val watcher = system.actorOf(WatcherService.props(mockEtcdClient))\n\n    val ns = NamespaceContainerCount(namespace, mockEtcdClient, watcher)\n    retry(() => {\n      ns.inProgressContainerNumByNamespace shouldBe 0\n      ns.existingContainerNumByNamespace shouldBe 0\n    }, 10, Some(100.milliseconds))\n\n    val invoker = \"invoker0\"\n    (0 to 100).foreach(i => {\n      mockEtcdClient.publishEvents(\n        EventType.PUT,\n        inProgressContainer(namespace, fqn, revision, schedulerId, CreationId(s\"testId$i\")),\n        \"test-value\")\n    })\n    (0 to 100).foreach(i => {\n      mockEtcdClient.publishEvents(\n        EventType.PUT,\n        s\"${ContainerKeys.existingContainers(namespace, fqn, DocRevision.empty)}/${invoker}/test-container$i\",\n        \"test-value\")\n    })\n\n    retry(() => {\n      ns.inProgressContainerNumByNamespace shouldBe 101\n      ns.existingContainerNumByNamespace shouldBe 101\n    }, 50, Some(100.milliseconds))\n  }\n\n  class MockEtcdClient(client: Client, isLeader: Boolean, leaseNotFound: Boolean = false, failedCount: Int = 0)\n      extends EtcdClient(client)(ec) {\n    var count = 0\n    var storedValues = List.empty[(String, String, Long, Long)]\n    var dataMap = Map[String, String]()\n    var totalFailedCount = 0\n\n    override def putTxn[T](key: String, value: T, cmpVersion: Long, leaseId: Long): Future[TxnResponse] = {\n      if (isLeader) {\n        storedValues = (key, value.toString, cmpVersion, leaseId) :: storedValues\n      }\n      Future.successful(TxnResponse.newBuilder().setSucceeded(isLeader).build())\n    }\n\n    /*\n     * this method count the number of entries whose key starts with the given prefix\n     */\n    override def getCount(prefixKey: String): Future[Long] = {\n      if (totalFailedCount < failedCount) {\n        totalFailedCount += 1\n        Future.failed(new Exception(\"error\"))\n      } else {\n        Future.successful { dataMap.count(data => data._1.startsWith(prefixKey)) }\n      }\n    }\n\n    var watchCallbackMap = Map[String, WatchUpdate => Unit]()\n\n    override def keepAliveOnce(leaseId: Long): Future[LeaseKeepAliveResponse] =\n      Future.successful(LeaseKeepAliveResponse.newBuilder().setID(leaseId).build())\n\n    /*\n     * this method adds one callback for the given key in watchCallbackMap.\n     *\n     * Note: Currently it only supports prefix-based watch.\n     */\n    override def watchAllKeys(next: WatchUpdate => Unit, error: Throwable => Unit, completed: () => Unit): Watch = {\n\n      watchCallbackMap += \"\" -> next\n      new Watch {\n        override def close(): Unit = {}\n\n        override def addListener(listener: Runnable, executor: Executor): Unit = {}\n\n        override def cancel(mayInterruptIfRunning: Boolean): Boolean = true\n\n        override def isCancelled: Boolean = true\n\n        override def isDone: Boolean = true\n\n        override def get(): lang.Boolean = true\n\n        override def get(timeout: Long, unit: TimeUnit): lang.Boolean = true\n      }\n    }\n\n    /*\n     * This method stores the data in dataMap to simulate etcd.put()\n     * After then, it calls the registered watch callback for the given key\n     * So we don't need to call put() to simulate watch API.\n     * Expected order of calls is 1. watch(), 2.publishEvents(). Data will be stored in dataMap and\n     * callbacks in the callbackMap for the given prefix will be called by publishEvents()\n     *\n     * Note: watch callback is currently registered based on prefix only.\n     */\n    def publishEvents(eventType: EventType, key: String, value: String): Unit = {\n      val eType = eventType match {\n        case EventType.PUT =>\n          dataMap += key -> value\n          EventType.PUT\n\n        case EventType.DELETE =>\n          dataMap -= key\n          EventType.DELETE\n\n        case EventType.UNRECOGNIZED => Event.EventType.UNRECOGNIZED\n      }\n      val event = Event\n        .newBuilder()\n        .setType(eType)\n        .setPrevKv(\n          KeyValue\n            .newBuilder()\n            .setKey(ByteString.copyFromUtf8(key))\n            .setValue(ByteString.copyFromUtf8(value))\n            .build())\n        .setKv(\n          KeyValue\n            .newBuilder()\n            .setKey(ByteString.copyFromUtf8(key))\n            .setValue(ByteString.copyFromUtf8(value))\n            .build())\n        .build()\n\n      // find the callbacks which has the proper prefix for the given key\n      watchCallbackMap.filter(callback => key.startsWith(callback._1)).foreach { callback =>\n        callback._2(new mockWatchUpdate().addEvents(event))\n      }\n    }\n  }\n\n  class mockWatchUpdate extends WatchUpdate {\n    private var eventLists: util.List[Event] = new util.ArrayList[Event]()\n    override def getHeader: ResponseHeader = ???\n\n    def addEvents(event: Event): WatchUpdate = {\n      eventLists.add(event)\n      this\n    }\n\n    override def getEvents: util.List[Event] = eventLists\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/queue/test/ElasticSearchDurationCheckResultFormatTest.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue.test\n\nimport org.apache.openwhisk.core.scheduler.queue.{DurationCheckResult, ElasticSearchDurationCheckResultFormat}\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json._\n\n@RunWith(classOf[JUnitRunner])\nclass ElasticSearchDurationCheckResultFormatTest extends AnyFlatSpec with Matchers with ScalaFutures {\n  behavior of \"ElasticSearchDurationCheckResultFormatTest\"\n\n  val serde = new ElasticSearchDurationCheckResultFormat()\n\n  it should \"serialize the data correctly\" in {\n    val normalData = \"\"\"{\n          \"_shards\": {\n              \"failed\": 0,\n              \"skipped\": 0,\n              \"successful\": 5,\n              \"total\": 5\n          },\n          \"aggregations\": {\n              \"filterAggregation\": {\n                  \"averageAggregation\": {\n                      \"value\": 14\n                  },\n                  \"doc_count\": 3\n              }\n          },\n          \"hits\": {\n              \"hits\": [],\n              \"max_score\": 0,\n              \"total\": 3\n          },\n          \"timed_out\": false,\n          \"took\": 2\n      }\"\"\"\n\n    val bindingData = \"\"\"{\n          \"_shards\": {\n              \"failed\": 0,\n              \"skipped\": 0,\n              \"successful\": 5,\n              \"total\": 5\n          },\n          \"aggregations\": {\n              \"averageAggregation\": {\n                  \"value\": 12\n              }\n          },\n          \"hits\": {\n              \"hits\": [],\n              \"max_score\": 0,\n              \"total\": 2\n          },\n          \"timed_out\": false,\n          \"took\": 0\n      }\"\"\"\n\n    val expected1 = DurationCheckResult(Some(14), 3, 2)\n    val expected2 = DurationCheckResult(Some(12), 2, 0)\n    val result1 = serde.read(normalData.parseJson)\n    val result2 = serde.read(bindingData.parseJson)\n\n    result1 shouldBe expected1\n    result2 shouldBe expected2\n  }\n\n  // Since the write method is not being used, this test is meaningless but added just for the duality.\n  it should \"deserialize the data correctly\" in {\n    val data = DurationCheckResult(Some(14), 3, 2)\n    val expected =\n      \"\"\"[14, 3, 2]\n        |\n        |\"\"\".stripMargin\n    val result = serde.write(data)\n\n    result shouldBe expected.parseJson\n  }\n\n  it should \"throw an exception when data is not in the expected format\" in {\n    val malformedData = \"\"\"{\n          \"_shards\": {\n              \"failed\": 0,\n              \"skipped\": 0,\n              \"successful\": 5,\n              \"total\": 5\n          },\n          \"averageAggregation\": {\n              \"value\": 14\n          },\n          \"hits\": {\n              \"hits\": [],\n              \"max_score\": 0,\n              \"total\": 3\n          },\n          \"timed_out\": false,\n          \"took\": 2\n      }\"\"\"\n\n    assertThrows[DeserializationException] {\n      serde.read(malformedData.parseJson)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/queue/test/ElasticSearchDurationCheckerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue.test\n\nimport com.sksamuel.elastic4s.http.ElasticDsl._\nimport com.sksamuel.elastic4s.http.{ElasticClient, ElasticProperties, NoOpRequestConfigCallback}\nimport common._\nimport common.rest.WskRestOperations\nimport org.apache.http.auth.{AuthScope, UsernamePasswordCredentials}\nimport org.apache.http.impl.client.BasicCredentialsProvider\nimport org.apache.http.impl.nio.client.HttpAsyncClientBuilder\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.core.ConfigKeys\nimport org.apache.openwhisk.core.database.elasticsearch.ElasticSearchActivationStoreConfig\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.entity.test.ExecHelpers\nimport org.apache.openwhisk.core.scheduler.queue.{DurationCheckResult, ElasticSearchDurationChecker}\nimport org.elasticsearch.client.RestClientBuilder.HttpClientConfigCallback\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatest.{BeforeAndAfter, BeforeAndAfterAll}\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport pureconfig.generic.auto._\nimport pureconfig.loadConfigOrThrow\nimport java.time.Instant\nimport java.time.temporal.ChronoUnit\nimport scala.language.postfixOps\n\nimport scala.collection.mutable\nimport scala.concurrent.ExecutionContextExecutor\nimport scala.concurrent.duration._\n\n/**\n * This test will try to fetch the average duration from activation documents. This class guarantee the minimum compatibility.\n * In case there are any updates in the activation document, it will catch the difference between the expected and the real.\n */\n@RunWith(classOf[JUnitRunner])\nclass ElasticSearchDurationCheckerTests\n    extends AnyFlatSpec\n    with Matchers\n    with ScalaFutures\n    with WskTestHelpers\n    with StreamLogging\n    with ExecHelpers\n    with BeforeAndAfterAll\n    with BeforeAndAfter {\n\n  private val namespace = \"durationCheckNamespace\"\n  val wskadmin: RunCliCmd = new RunCliCmd {\n    override def baseCommand: mutable.Buffer[String] = WskAdmin.baseCommand\n  }\n  implicit val ec: ExecutionContextExecutor = actorSystem.dispatcher\n  implicit val timeoutConfig: PatienceConfig = PatienceConfig(5 seconds, 15 milliseconds)\n\n  private val auth = BasicAuthenticationAuthKey()\n  implicit val wskprops: WskProps = WskProps(authKey = auth.compact, namespace = namespace)\n  implicit val transid: TransactionId = TransactionId.testing\n\n  val wsk = new WskRestOperations\n  val elasticSearchConfig: ElasticSearchActivationStoreConfig =\n    loadConfigOrThrow[ElasticSearchActivationStoreConfig](ConfigKeys.elasticSearchActivationStore)\n\n  val testIndex: String = generateIndex(namespace)\n  val concurrency = 1\n  val actionMem: ByteSize = 256.MB\n  val defaultDurationCheckWindow = 5.seconds\n\n  private val httpClientCallback = new HttpClientConfigCallback {\n    override def customizeHttpClient(httpClientBuilder: HttpAsyncClientBuilder): HttpAsyncClientBuilder = {\n      val provider = new BasicCredentialsProvider\n      provider.setCredentials(\n        AuthScope.ANY,\n        new UsernamePasswordCredentials(elasticSearchConfig.username, elasticSearchConfig.password))\n      httpClientBuilder.setDefaultCredentialsProvider(provider)\n    }\n  }\n\n  private val client =\n    ElasticClient(\n      ElasticProperties(s\"${elasticSearchConfig.protocol}://${elasticSearchConfig.hosts}\"),\n      NoOpRequestConfigCallback,\n      httpClientCallback)\n\n  private val elasticSearchDurationChecker = new ElasticSearchDurationChecker(client, defaultDurationCheckWindow)\n\n  override def beforeAll(): Unit = {\n    val res = wskadmin.cli(Seq(\"user\", \"create\", namespace, \"-u\", auth.compact))\n    res.exitCode shouldBe 0\n\n    println(s\"namespace: $namespace, auth: ${auth.compact}\")\n    super.beforeAll()\n  }\n\n  override def afterAll(): Unit = {\n    client.execute {\n      deleteIndex(testIndex)\n    }\n    wskadmin.cli(Seq(\"user\", \"delete\", namespace))\n    logLines.foreach(println)\n    super.afterAll()\n  }\n\n  behavior of \"ElasticSearchDurationChecker\"\n\n  it should \"fetch the proper duration from ES\" in withAssetCleaner(wskprops) { (_, assetHelper) =>\n    val actionName = \"avgDuration\"\n    val dummyActionName = \"dummyAction\"\n\n    var totalDuration = 0L\n    val count = 3\n\n    assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n      action.create(actionName, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    assetHelper.withCleaner(wsk.action, dummyActionName) { (action, _) =>\n      action.create(dummyActionName, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    val actionMetaData =\n      WhiskActionMetaData(\n        EntityPath(namespace),\n        EntityName(actionName),\n        jsMetaData(Some(\"jsMain\"), binary = false),\n        limits = actionLimits(actionMem, concurrency))\n\n    val run1 = wsk.action.invoke(actionName, Map())\n    withActivation(wsk.activation, run1) { activation =>\n      activation.response.status shouldBe \"success\"\n    }\n    // wait for 1s\n    Thread.sleep(1000)\n\n    val start = Instant.now()\n    val run2 = wsk.action.invoke(dummyActionName, Map())\n    withActivation(wsk.activation, run2) { activation =>\n      activation.response.status shouldBe \"success\"\n    }\n\n    1 to count foreach { _ =>\n      val run = wsk.action.invoke(actionName, Map())\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n        totalDuration += activation.duration\n      }\n    }\n    val end = Instant.now()\n    val timeWindow = math.ceil(ChronoUnit.MILLIS.between(start, end) / 1000.0).seconds\n    val durationChecker = new ElasticSearchDurationChecker(client, timeWindow)\n\n    // it should aggregate the recent activations in 5 seconds\n    val durationCheckResult: DurationCheckResult =\n      durationChecker.checkAverageDuration(namespace, actionMetaData)(res => res).futureValue\n\n    /**\n     * Expected sample data\n      {\n          \"_shards\": {\n              \"failed\": 0,\n              \"skipped\": 0,\n              \"successful\": 5,\n              \"total\": 5\n          },\n          \"aggregations\": {\n              \"filterAggregation\": {\n                  \"averageAggregation\": {\n                      \"value\": 14\n                  },\n                  \"doc_count\": 3\n              }\n          },\n          \"hits\": {\n              \"hits\": [],\n              \"max_score\": 0,\n              \"total\": 3\n          },\n          \"timed_out\": false,\n          \"took\": 2\n      }\n     */\n    truncateDouble(durationCheckResult.averageDuration.getOrElse(0.0)) shouldBe truncateDouble(\n      totalDuration.toDouble / count.toDouble)\n    durationCheckResult.hitCount shouldBe count\n  }\n\n  it should \"fetch proper average duration for a package action\" in withAssetCleaner(wskprops) { (_, assetHelper) =>\n    val packageName = \"samplePackage\"\n    val actionName = \"packageAction\"\n    val fqn = s\"$namespace/$packageName/$actionName\"\n\n    val actionMetaData =\n      WhiskActionMetaData(\n        EntityPath(s\"$namespace/$packageName\"),\n        EntityName(actionName),\n        jsMetaData(Some(\"jsMain\"), binary = false),\n        limits = actionLimits(actionMem, concurrency))\n\n    var totalDuration = 0L\n    val count = 3\n\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName)\n    }\n\n    assetHelper.withCleaner(wsk.action, fqn) { (action, _) =>\n      action.create(fqn, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    1 to count foreach { _ =>\n      val run = wsk.action.invoke(fqn, Map())\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n      }\n    }\n    // wait for 1s\n    Thread.sleep(1000)\n\n    val start = Instant.now()\n    1 to count foreach { _ =>\n      val run = wsk.action.invoke(fqn, Map())\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n        totalDuration += activation.duration\n      }\n    }\n    val end = Instant.now()\n    val timeWindow = math.ceil(ChronoUnit.MILLIS.between(start, end) / 1000.0).seconds\n    val durationChecker = new ElasticSearchDurationChecker(client, timeWindow)\n    val durationCheckResult: DurationCheckResult =\n      durationChecker.checkAverageDuration(namespace, actionMetaData)(res => res).futureValue\n\n    /**\n     * Expected sample data\n      {\n          \"_shards\": {\n              \"failed\": 0,\n              \"skipped\": 0,\n              \"successful\": 5,\n              \"total\": 5\n          },\n          \"aggregations\": {\n              \"filterAggregation\": {\n                  \"averageAggregation\": {\n                      \"value\": 13\n                  },\n                  \"doc_count\": 3\n              }\n          },\n          \"hits\": {\n              \"hits\": [],\n              \"max_score\": 0,\n              \"total\": 6\n          },\n          \"timed_out\": false,\n          \"took\": 0\n      }\n     */\n    truncateDouble(durationCheckResult.averageDuration.getOrElse(0.0)) shouldBe truncateDouble(\n      totalDuration.toDouble / count.toDouble)\n    durationCheckResult.hitCount shouldBe count\n  }\n\n  it should \"fetch the duration for binding action\" in withAssetCleaner(wskprops) { (_, assetHelper) =>\n    val packageName = \"testPackage\"\n    val actionName = \"testAction\"\n    val originalFQN = s\"$namespace/$packageName/$actionName\"\n    val boundPackageName = \"boundPackage\"\n\n    val actionMetaData =\n      WhiskActionMetaData(\n        EntityPath(s\"$namespace/$boundPackageName\"),\n        EntityName(actionName),\n        jsMetaData(Some(\"jsMain\"), binary = false),\n        limits = actionLimits(actionMem, concurrency),\n        binding = Some(EntityPath(s\"$namespace/$packageName\")))\n\n    var totalDuration = 0L\n    val count = 3\n\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, shared = Some(true))\n    }\n\n    assetHelper.withCleaner(wsk.action, originalFQN) { (action, _) =>\n      action.create(originalFQN, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    assetHelper.withCleaner(wsk.pkg, boundPackageName) { (pkg, _) =>\n      pkg.bind(packageName, boundPackageName)\n    }\n\n    1 to count foreach { _ =>\n      val run = wsk.action.invoke(s\"$boundPackageName/$actionName\", Map())\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n      }\n    }\n    // wait for 1s\n    Thread.sleep(1000)\n\n    val start = Instant.now()\n    1 to count foreach { _ =>\n      val run = wsk.action.invoke(s\"$boundPackageName/$actionName\", Map())\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n        totalDuration += activation.duration\n      }\n    }\n    val end = Instant.now()\n    val timeWindow = math.ceil(ChronoUnit.MILLIS.between(start, end) / 1000.0).seconds\n    val durationChecker = new ElasticSearchDurationChecker(client, timeWindow)\n    val durationCheckResult: DurationCheckResult =\n      durationChecker.checkAverageDuration(namespace, actionMetaData)(res => res).futureValue\n\n    /**\n     * Expected sample data\n      {\n          \"_shards\": {\n              \"failed\": 0,\n              \"skipped\": 0,\n              \"successful\": 5,\n              \"total\": 5\n          },\n          \"aggregations\": {\n              \"averageAggregation\": {\n                  \"value\": 14\n              }\n          },\n          \"hits\": {\n              \"hits\": [],\n              \"max_score\": 0,\n              \"total\": 3\n          },\n          \"timed_out\": false,\n          \"took\": 0\n      }\n     */\n    truncateDouble(durationCheckResult.averageDuration.getOrElse(0.0)) shouldBe truncateDouble(\n      totalDuration.toDouble / count.toDouble)\n    durationCheckResult.hitCount shouldBe count\n  }\n\n  it should \"return nothing properly if there is no activation yet\" in withAssetCleaner(wskprops) { (_, _) =>\n    val actionName = \"noneAction\"\n\n    val actionMetaData =\n      WhiskActionMetaData(\n        EntityPath(s\"$namespace\"),\n        EntityName(actionName),\n        jsMetaData(Some(\"jsMain\"), binary = false),\n        limits = actionLimits(actionMem, concurrency))\n\n    val durationCheckResult: DurationCheckResult =\n      elasticSearchDurationChecker.checkAverageDuration(namespace, actionMetaData)(res => res).futureValue\n\n    durationCheckResult.averageDuration shouldBe None\n    durationCheckResult.hitCount shouldBe 0\n  }\n\n  it should \"return nothing properly if there is no activation for binding action yet\" in withAssetCleaner(wskprops) {\n    (_, _) =>\n      val packageName = \"testPackage2\"\n      val actionName = \"noneAction\"\n      val boundPackageName = \"boundPackage2\"\n\n      val actionMetaData =\n        WhiskActionMetaData(\n          EntityPath(s\"$namespace/$boundPackageName\"),\n          EntityName(actionName),\n          jsMetaData(Some(\"jsMain\"), false),\n          limits = actionLimits(actionMem, concurrency),\n          binding = Some(EntityPath(s\"${namespace}/${packageName}\")))\n\n      val durationCheckResult: DurationCheckResult =\n        elasticSearchDurationChecker.checkAverageDuration(namespace, actionMetaData)(res => res).futureValue\n\n      durationCheckResult.averageDuration shouldBe None\n      durationCheckResult.hitCount shouldBe 0\n  }\n\n  private def truncateDouble(number: Double, scale: Int = 2) = {\n    BigDecimal(number).setScale(scale, BigDecimal.RoundingMode.HALF_UP).toDouble\n  }\n\n  private def generateIndex(namespace: String): String = {\n    elasticSearchConfig.indexPattern.dropWhile(_ == '/') format namespace.toLowerCase\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/queue/test/MemoryQueueFlowTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue.test\n\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.pekko.actor.FSM.{CurrentState, StateTimeout, SubscribeTransitionCallBack, Transition}\nimport org.apache.pekko.testkit.{TestActor, TestFSMRef, TestProbe}\nimport com.sksamuel.elastic4s.http.{search => _}\nimport org.apache.openwhisk.common.GracefulShutdown\nimport org.apache.openwhisk.common.time.{Clock, SystemClock}\nimport org.apache.openwhisk.core.connector.ContainerCreationError.{NonExecutableActionError, WhiskError}\nimport org.apache.openwhisk.core.connector.ContainerCreationMessage\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.etcd.EtcdClient\nimport org.apache.openwhisk.core.scheduler.grpc.ActivationResponse\nimport org.apache.openwhisk.core.scheduler.message.{\n  ContainerCreation,\n  ContainerDeletion,\n  FailedCreationJob,\n  SuccessfulCreationJob\n}\nimport org.apache.openwhisk.core.scheduler.queue.MemoryQueue.checkToDropStaleActivation\nimport org.apache.openwhisk.core.scheduler.queue._\nimport org.apache.openwhisk.core.service._\nimport org.apache.openwhisk.http.Messages.{namespaceLimitUnderZero, tooManyConcurrentRequests}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.{JsObject, JsString}\n\nimport java.time.Instant\nimport scala.collection.immutable.Queue\nimport scala.collection.mutable\nimport scala.concurrent.Future\nimport scala.concurrent.duration.{DurationInt, FiniteDuration, MILLISECONDS}\nimport scala.language.postfixOps\n\nclass FakeClock extends Clock {\n  var instant: Instant = Instant.now()\n  def now() = instant\n  def set(now: Instant): Unit = {\n    instant = now\n  }\n  def plusSeconds(secondsToAdd: Long): Unit = {\n    instant = instant.plusSeconds(secondsToAdd)\n  }\n}\n\n@RunWith(classOf[JUnitRunner])\nclass MemoryQueueFlowTests\n    extends MemoryQueueTestsFixture\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterEach\n    with BeforeAndAfterAll {\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    ackedMessageCount = 0\n    storedMessageCount = 0\n  }\n\n  override def afterEach(): Unit = {\n    super.afterEach()\n    logLines.foreach(println)\n    stream.reset()\n  }\n\n  behavior of \"MemoryQueueFlow\"\n\n  it should \"normally be created and handle an activation and became idle an finally removed\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val decisionMaker = TestProbe()\n    val probe = TestProbe()\n    // no need to create more than 1 container for this test\n    decisionMaker.setAutoPilot((sender: ActorRef, msg) => {\n      msg match {\n        case msg: QueueSnapshot if !msg.initialized =>\n          logging.info(this, \"add an initial container\")\n          sender ! DecisionResults(AddInitialContainer, 1)\n          TestActor.KeepRunning\n\n        case _ =>\n          TestActor.KeepRunning\n      }\n    })\n    val container = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          decisionMaker.ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm ! message\n\n    // any id is fine because it would be overridden\n    var creationId = CreationId.generate()\n\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n\n    container.send(fsm, getActivation())\n    container.expectMsg(ActivationResponse(Right(message)))\n\n    // deleting the ID from creationIds set\n    fsm ! SuccessfulCreationJob(creationId, testInvocationNamespace, fqn, revision)\n\n    // deleting the container from containers set\n    container.send(fsm, getActivation(false))\n    container.expectMsg(ActivationResponse(Left(NoActivationMessage())))\n\n    fsm ! StateTimeout\n\n    probe.expectMsg(Transition(fsm, Running, Idle))\n\n    fsm ! StateTimeout\n\n    expectDataCleanUp(watcher, dataMgmtService)\n\n    probe.expectMsg(Transition(fsm, Idle, Removed))\n\n    // the queue is timed out again in the removed state\n    fsm ! StateTimeout\n\n    parent.expectMsg(queueRemovedMsg)\n    fsm ! QueueRemovedCompleted\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"became Idle and Running again if a message arrives\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val probe = TestProbe()\n    val testSchedulingDecisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testInvocationNamespace, fqn, schedulingConfig))\n\n    val messages = getActivationMessages(2)\n    val container = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          testSchedulingDecisionMaker,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm ! messages(0)\n\n    // any id is fine because it would be overridden\n    var creationId = CreationId.generate()\n\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n\n    container.send(fsm, getActivation())\n    container.expectMsg(ActivationResponse(Right(messages(0))))\n\n    // deleting the ID from creationIds set\n    fsm ! SuccessfulCreationJob(creationId, testInvocationNamespace, fqn, revision)\n\n    // deleting the container from containers set\n    container.send(fsm, getActivation(false))\n    container.expectMsg(ActivationResponse(Left(NoActivationMessage())))\n\n    fsm ! StateTimeout\n\n    probe.expectMsg(Transition(fsm, Running, Idle))\n\n    fsm ! messages(1)\n\n    probe.expectMsg(Transition(fsm, Idle, Running))\n\n    fsm ! StateTimeout\n    // since there is one message, it should not be Idle\n    probe.expectNoMessage()\n\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n\n    fsm.underlyingActor.queue.length shouldBe 1\n\n    container.send(fsm, getActivation(true))\n    container.expectMsg(ActivationResponse(Right(messages(1))))\n\n    // deleting the ID from creationIds set\n    fsm ! SuccessfulCreationJob(creationId, testInvocationNamespace, fqn, revision)\n\n    // deleting the container from containers set\n    container.send(fsm, getActivation(false))\n    container.expectMsg(ActivationResponse(Left(NoActivationMessage())))\n\n    fsm ! StateTimeout\n\n    probe.expectMsg(Transition(fsm, Running, Idle))\n\n    fsm ! StateTimeout\n\n    probe.expectMsg(Transition(fsm, Idle, Removed))\n\n    // the queue is timed out again in the removed state\n    fsm ! StateTimeout\n\n    fsm ! QueueRemovedCompleted\n\n    expectDataCleanUp(watcher, dataMgmtService)\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"go to the NamespaceThrottled state dropping messages when it can't create an initial container\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val decisionMaker = TestProbe()\n    val probe = TestProbe()\n\n    // this is the case where there is no capacity in a namespace and no container can be created.\n    decisionMaker.setAutoPilot((sender: ActorRef, msg) => {\n      msg match {\n        case QueueSnapshot(_, _, _, _, _, _, _, _, _, _, _, _, Running, _) =>\n          sender ! DecisionResults(EnableNamespaceThrottling(true), 0)\n          TestActor.KeepRunning\n\n        case _ =>\n          // do nothing\n          TestActor.KeepRunning\n      }\n    })\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          decisionMaker.ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm ! message\n\n    dataMgmtService.expectMsg(RegisterData(namespaceThrottlingKey, true.toString, failoverEnabled = false))\n    probe.expectMsg(Transition(fsm, Running, NamespaceThrottled))\n\n    awaitAssert({\n      ackedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(tooManyConcurrentRequests)))\n      storedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(tooManyConcurrentRequests)))\n\n      // all activations are dropped with an error\n      fsm.underlyingActor.queue.size shouldBe 0\n    }, 5.seconds)\n\n    fsm ! GracefulShutdown\n\n    probe.expectMsg(Transition(fsm, NamespaceThrottled, Removing))\n\n    // the queue is timed out in the Removing state\n    fsm ! StateTimeout\n\n    expectDataCleanUp(watcher, dataMgmtService)\n\n    probe.expectMsg(Transition(fsm, Removing, Removed))\n\n    // the queue is timed out again in the Removed state\n    fsm ! StateTimeout\n    fsm ! QueueRemovedCompleted\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"go to the NamespaceThrottled state without dropping messages and get back to the Running container\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val probe = TestProbe()\n    val testSchedulingDecisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testInvocationNamespace, fqn, schedulingConfig))\n\n    val getUserLimit = (_: String) => Future.successful(1)\n    val container = TestProbe()\n\n    val messages = getActivationMessages(2)\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          testSchedulingDecisionMaker,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    // send two messages to simulate the namespace-throttled case as it can't create more than 1 container\n    fsm ! messages(0)\n    fsm ! messages(1)\n\n    // any id is fine because it would be overridden\n    var creationId = CreationId.generate()\n\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n\n    // deleting the ID from creationIds set\n    fsm ! SuccessfulCreationJob(creationId, testInvocationNamespace, fqn, revision)\n\n    // one container is created\n    fsm.underlyingActor.namespaceContainerCount.inProgressContainerNumByNamespace = 1\n\n    // only one message is handled\n    container.send(fsm, getActivation(true, \"testContainerId1\"))\n    container.expectMsg(ActivationResponse(Right(messages(0))))\n\n    // namespace throttling is enabled\n    dataMgmtService.expectMsg(RegisterData(namespaceThrottlingKey, true.toString, failoverEnabled = false))\n\n    // since one message is not being handled but cannot create more container, it is namespace-throttled\n    probe.expectMsg(Transition(fsm, Running, NamespaceThrottled))\n\n    // deleting the container to secure the capacity\n    container.send(fsm, getActivation(false, \"testContainerId1\"))\n    container.expectMsg(ActivationResponse(Left(NoActivationMessage())))\n    fsm.underlyingActor.namespaceContainerCount.inProgressContainerNumByNamespace = 0\n\n    // namespace throttling is disabled\n    dataMgmtService.expectMsg(RegisterData(namespaceThrottlingKey, false.toString, failoverEnabled = false))\n\n    probe.expectMsg(Transition(fsm, NamespaceThrottled, Running))\n\n    // try container creation\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n\n    // a container is created\n    fsm ! SuccessfulCreationJob(creationId, testInvocationNamespace, fqn, revision)\n\n    // one message is handled\n    container.send(fsm, getActivation(true, \"testContainerId2\"))\n    container.expectMsg(ActivationResponse(Right(messages(1))))\n\n    // deleting the container from containers set\n    container.send(fsm, getActivation(false, \"testContainerId2\"))\n    container.expectMsg(ActivationResponse(Left(NoActivationMessage())))\n\n    fsm ! StateTimeout\n\n    // all subsequent procedures are same with the Running case\n    probe.expectMsg(Transition(fsm, Running, Idle))\n\n    fsm ! StateTimeout\n\n    probe.expectMsg(Transition(fsm, Idle, Removed))\n\n    // the queue is timed out again in the Removed state\n    fsm ! StateTimeout\n\n    parent.expectMsg(queueRemovedMsg)\n    fsm ! QueueRemovedCompleted\n\n    expectDataCleanUp(watcher, dataMgmtService)\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"go to the ActionThrottled state when there are too many stale activations including transition to NamespaceThrottling\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val probe = TestProbe()\n    val testSchedulingDecisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testInvocationNamespace, fqn, schedulingConfig))\n\n    // max retention size is 10 and throttling fraction is 0.8\n    // queue will be action throttled at 10 messages and disabled action throttling at 8 messages\n    val queueConfig = QueueConfig(5 seconds, 10 seconds, 10 seconds, 5 seconds, 10, 5000, 10000, 0.8, 10, false)\n\n    // limit is 1\n    val getUserLimit = (_: String) => Future.successful(1)\n    val container = TestProbe()\n\n    // generate 12 activations\n    val messages = getActivationMessages(12)\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          testSchedulingDecisionMaker,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    // send one messages to the queue to make it Running\n    fsm ! message\n\n    // any id is fine because it would be overridden\n    var creationId = CreationId.generate()\n\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n\n    // deleting the ID from creationIds set\n    fsm ! SuccessfulCreationJob(creationId, testInvocationNamespace, fqn, revision)\n\n    // one container is created\n    fsm.underlyingActor.namespaceContainerCount.existingContainerNumByNamespace += 1\n\n    // only one message is handled\n    container.send(fsm, getActivation(true, \"testContainerId1\"))\n    container.expectMsg(ActivationResponse(Right(message)))\n\n    // send 10 messages to fsm to enable action throttling\n    (0 to 9).foreach(fsm ! messages(_))\n\n    dataMgmtService.expectMsg(RegisterData(actionThrottlingKey, true.toString, failoverEnabled = false))\n    probe.expectMsg(Transition(fsm, Running, ActionThrottled))\n\n    // if messages arrive in the ActionThrottled state, they are immediately dropped.\n    fsm ! messages(10)\n\n    awaitAssert({\n      ackedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(tooManyConcurrentRequests)))\n      storedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(tooManyConcurrentRequests)))\n    }, 5.seconds)\n\n    fsm ! messages(11)\n\n    awaitAssert({\n      ackedMessageCount shouldBe 2\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(tooManyConcurrentRequests)))\n      storedMessageCount shouldBe 2\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(tooManyConcurrentRequests)))\n    }, 5.seconds)\n\n    // handle 3 messages to disable the action throttling\n    (0 to 2).foreach { index =>\n      // only one message is handled\n      container.send(fsm, getActivation(true, \"testContainerId1\"))\n      container.expectMsg(ActivationResponse(Right(messages(index))))\n    }\n\n    // action throttling is disabled\n    dataMgmtService.expectMsg(RegisterData(actionThrottlingKey, false.toString, failoverEnabled = false))\n    probe.expectMsg(Transition(fsm, ActionThrottled, Running))\n\n    // handle 8 messages to consume all messages\n    (3 to 9).foreach { index =>\n      // only one message is handled\n      container.send(fsm, getActivation(true, \"testContainerId1\"))\n      container.expectMsg(ActivationResponse(Right(messages(index))))\n    }\n\n    // sooner or later the namespace throttling is also enabled as it can't create more containers\n    dataMgmtService.expectMsg(RegisterData(namespaceThrottlingKey, true.toString, failoverEnabled = false))\n\n    probe.expectMsg(Transition(fsm, Running, NamespaceThrottled))\n\n    // deleting the container to secure the capacity\n    container.send(fsm, getActivation(false, \"testContainerId1\"))\n    container.expectMsg(ActivationResponse(Left(NoActivationMessage())))\n    fsm.underlyingActor.namespaceContainerCount.existingContainerNumByNamespace -= 1\n\n    // namespace throttling is disabled\n    dataMgmtService.expectMsg(10.seconds, RegisterData(namespaceThrottlingKey, false.toString, failoverEnabled = false))\n    probe.expectMsg(Transition(fsm, NamespaceThrottled, Running))\n\n    // normal termination process\n    fsm ! StateTimeout\n    probe.expectMsg(Transition(fsm, Running, Idle))\n\n    fsm ! StateTimeout\n    probe.expectMsg(Transition(fsm, Idle, Removed))\n\n    // the queue is timed out again in the Removed state\n    fsm ! StateTimeout\n    parent.expectMsg(queueRemovedMsg)\n\n    fsm ! QueueRemovedCompleted\n    expectDataCleanUp(watcher, dataMgmtService)\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"be Flushing when the limit is 0 and restarted back to Running state when the limit is increased\" in {\n    implicit val clock = new FakeClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val probe = TestProbe()\n    val testSchedulingDecisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testInvocationNamespace, fqn, schedulingConfig))\n\n    // generate 2 activations\n    val messages = getActivationMessages(3)\n\n    var limit = 0\n    val getUserLimit = (_: String) => Future.successful(limit)\n\n    val container = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          testSchedulingDecisionMaker,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    fsm ! messages(0)\n\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    clock.plusSeconds(FiniteDuration(retentionTimeout, MILLISECONDS).toSeconds)\n\n    probe.expectMsg(Transition(fsm, Running, Flushing))\n    // activation received in Flushing state won't be flushed immediately if Flushing state is caused by a whisk error\n    fsm ! messages(1)\n    fsm ! StateTimeout\n\n    awaitAssert({\n      ackedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(namespaceLimitUnderZero)))\n      storedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(namespaceLimitUnderZero)))\n      fsm.underlyingActor.queue.length shouldBe 1\n    }, 5.seconds)\n    // limit is increased by an operator\n    limit = 10\n\n    // any id is fine because it would be overridden\n    var creationId = CreationId.generate()\n\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n\n    // deleting the ID from creationIds set\n    fsm ! SuccessfulCreationJob(creationId, testInvocationNamespace, fqn, revision)\n\n    // Queue is now working\n    probe.expectMsg(Transition(fsm, Flushing, Running))\n\n    // one container is created\n    fsm.underlyingActor.namespaceContainerCount.existingContainerNumByNamespace += 1\n\n    // only one message is handled\n    container.send(fsm, getActivation(true, \"testContainerId1\"))\n    container.expectMsg(ActivationResponse(Right(messages(1))))\n\n    // deleting the container from containers set\n    container.send(fsm, getActivation(false, \"testContainerId1\"))\n    fsm.underlyingActor.namespaceContainerCount.existingContainerNumByNamespace -= 1\n\n    // normal termination process\n    fsm ! StateTimeout\n\n    probe.expectMsg(Transition(fsm, Running, Idle))\n\n    fsm ! StateTimeout\n\n    probe.expectMsg(Transition(fsm, Idle, Removed))\n\n    // the queue is timed out again in the Removed state\n    fsm ! StateTimeout\n    parent.expectMsg(queueRemovedMsg)\n    fsm ! QueueRemovedCompleted\n\n    expectDataCleanUp(watcher, dataMgmtService)\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"be Flushing when the limit is 0 and be terminated without recovering\" in {\n    implicit val clock = new FakeClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val probe = TestProbe()\n    val testSchedulingDecisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testInvocationNamespace, fqn, schedulingConfig))\n\n    // generate 2 activations\n    val messages = getActivationMessages(2)\n\n    val getUserLimit = (_: String) => Future.successful(0)\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          testSchedulingDecisionMaker,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    fsm ! messages(0)\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    probe.expectMsg(Transition(fsm, Running, Flushing))\n    fsm ! messages(1)\n\n    // activation received in Flushing state won't be flushed immediately if Flushing state is caused by a whisk error\n    clock.plusSeconds((queueConfig.maxRetentionMs) / 1000)\n    fsm ! DropOld\n\n    awaitAssert({\n      ackedMessageCount shouldBe 2\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(namespaceLimitUnderZero)))\n      storedMessageCount shouldBe 2\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(namespaceLimitUnderZero)))\n      fsm.underlyingActor.queue.length shouldBe 0\n    }, 5.seconds)\n\n    // In this case data clean up happens first.\n    fsm ! StateTimeout\n    expectDataCleanUp(watcher, dataMgmtService)\n    probe.expectMsg(Transition(fsm, Flushing, Removed))\n\n    // the queue is timed out again in the Removed state\n    fsm ! StateTimeout\n    parent.expectMsg(queueRemovedMsg)\n    fsm ! QueueRemovedCompleted\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"be the Flushing state when a whisk error happens\" in {\n    implicit val clock = new FakeClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val probe = TestProbe()\n    val decisionMaker = TestProbe()\n\n    // no need to create more than 1 container for this test\n    decisionMaker.setAutoPilot((sender: ActorRef, msg) => {\n      msg match {\n        case msg: QueueSnapshot if !msg.initialized =>\n          logging.info(this, \"add an initial container\")\n          sender ! DecisionResults(AddInitialContainer, 1)\n          TestActor.KeepRunning\n\n        case _ =>\n          TestActor.KeepRunning\n      }\n    })\n\n    // generate 2 activations\n    val messages = getActivationMessages(2)\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          decisionMaker.ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm ! messages(0)\n\n    // any id is fine because it would be overridden\n    var creationId = CreationId.generate()\n\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n    // Failed to create a container\n    fsm ! FailedCreationJob(creationId, testInvocationNamespace, fqn, revision, WhiskError, \"whisk error\")\n\n    probe.expectMsg(Transition(fsm, Running, Flushing))\n\n    fsm ! messages(1)\n\n    clock.plusSeconds(FiniteDuration(retentionTimeout, MILLISECONDS).toSeconds)\n    fsm ! StateTimeout\n\n    awaitAssert({\n      ackedMessageCount shouldBe 2\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(\"whisk error\")))\n      storedMessageCount shouldBe 2\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(\"whisk error\")))\n      fsm.underlyingActor.queue.length shouldBe 0\n    }, FiniteDuration(retentionTimeout, MILLISECONDS))\n\n    clock.plusSeconds(flushGrace.toSeconds * 2)\n    fsm ! StateTimeout\n\n    probe.expectMsg(Transition(fsm, Flushing, Removed))\n\n    // the queue is timed out again in the Removed state\n    fsm ! StateTimeout\n    parent.expectMsg(queueRemovedMsg)\n    fsm ! QueueRemovedCompleted\n\n    expectDataCleanUp(watcher, dataMgmtService)\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"be the Flushing state when a whisk error happens and be recovered when a container is created\" in {\n    implicit val clock = new FakeClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val testSchedulingDecisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testInvocationNamespace, fqn, schedulingConfig))\n    val probe = TestProbe()\n    val container = TestProbe()\n    // generate 2 activations\n    val messages = getActivationMessages(2)\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          testSchedulingDecisionMaker,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm ! messages(0)\n\n    // Failed to create a container\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        fsm ! FailedCreationJob(id, testInvocationNamespace, fqn, revision, WhiskError, \"whisk error\")\n    }\n\n    clock.plusSeconds(FiniteDuration(retentionTimeout, MILLISECONDS).toSeconds)\n    probe.expectMsg(Transition(fsm, Running, Flushing))\n    fsm ! messages(1)\n\n    // activation received in Flushing state won't be flushed immediately if Flushing state is caused by a whisk error\n    fsm ! StateTimeout\n\n    awaitAssert({\n      ackedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(\"whisk error\")))\n      storedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(\"whisk error\")))\n      fsm.underlyingActor.queue.length shouldBe 1\n    }, FiniteDuration(retentionTimeout, MILLISECONDS))\n\n    // Succeed to create a container\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        fsm ! SuccessfulCreationJob(id, testInvocationNamespace, fqn, revision)\n    }\n\n    probe.expectMsg(Transition(fsm, Flushing, Running))\n\n    container.send(fsm, getActivation())\n    container.expectMsg(ActivationResponse(Right(messages(1))))\n\n    // deleting the container from containers set\n    container.send(fsm, getActivation(false))\n    container.expectMsg(ActivationResponse(Left(NoActivationMessage())))\n\n    fsm.underlyingActor.creationIds = mutable.Set.empty[String]\n    fsm ! StateTimeout\n    probe.expectMsg(Transition(fsm, Running, Idle))\n\n    fsm ! StateTimeout\n\n    probe.expectMsg(Transition(fsm, Idle, Removed))\n\n    // the queue is timed out again in the Removed state\n    fsm ! StateTimeout\n    parent.expectMsg(queueRemovedMsg)\n    fsm ! QueueRemovedCompleted\n\n    expectDataCleanUp(watcher, dataMgmtService)\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"be the Flushing state when a developer error happens\" in {\n    implicit val clock = new FakeClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val testSchedulingDecisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testInvocationNamespace, fqn, schedulingConfig))\n    val probe = TestProbe()\n\n    // generate 2 activations\n    val messages = getActivationMessages(2)\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          testSchedulingDecisionMaker,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm ! messages(0)\n\n    // any id is fine because it would be overridden\n    var creationId = CreationId.generate()\n\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n    // Failed to create a container\n    fsm ! FailedCreationJob(\n      creationId,\n      testInvocationNamespace,\n      fqn,\n      revision,\n      NonExecutableActionError,\n      \"no executable action found\")\n\n    // drop all activations before transition to the Flushing state\n    within(10.seconds) {\n      ackedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(\n        JsObject(\"error\" -> JsString(\"no executable action found\")))\n      storedMessageCount shouldBe 1\n      lastAckedActivationResult.response.result shouldBe Some(\n        JsObject(\"error\" -> JsString(\"no executable action found\")))\n      fsm.underlyingActor.queue.length shouldBe 0\n    }\n\n    probe.expectMsg(Transition(fsm, Running, Flushing))\n\n    fsm ! messages(1)\n\n    // new messages immediately dropped in the Flushing state\n    within(10.seconds) {\n      ackedMessageCount shouldBe 2\n      lastAckedActivationResult.response.result shouldBe Some(\n        JsObject(\"error\" -> JsString(\"no executable action found\")))\n      storedMessageCount shouldBe 2\n      lastAckedActivationResult.response.result shouldBe Some(\n        JsObject(\"error\" -> JsString(\"no executable action found\")))\n      fsm.underlyingActor.queue.length shouldBe 0\n    }\n\n    // simulate timeout 2 times\n    clock.plusSeconds(flushGrace.toSeconds * 2)\n    fsm ! StateTimeout\n    fsm ! StateTimeout\n\n    expectDataCleanUp(watcher, dataMgmtService)\n    probe.expectMsg(Transition(fsm, Flushing, Removed))\n\n    // the queue is timed out again in the Removed state\n    fsm ! StateTimeout\n    parent.expectMsg(queueRemovedMsg)\n    fsm ! QueueRemovedCompleted\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"be gracefully terminated when it receives a GracefulShutDown message\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val watcher = TestProbe()\n    val dataMgmtService = TestProbe()\n    val containerManager = TestProbe()\n    val testSchedulingDecisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testInvocationNamespace, fqn, schedulingConfig))\n    val probe = TestProbe()\n\n    val messages = getActivationMessages(4)\n    val container = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataMgmtService.ref,\n          watcher.ref,\n          containerManager.ref,\n          testSchedulingDecisionMaker,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    probe watch fsm\n    registerCallback(probe, fsm)\n\n    fsm ! Start\n    expectInitialData(watcher, dataMgmtService)\n    fsm ! testInitialDataStorageResult\n\n    probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm ! messages(0)\n\n    // any id is fine because it would be overridden\n    var creationId = CreationId.generate()\n\n    containerManager.expectMsgPF() {\n      case ContainerCreation(List(ContainerCreationMessage(_, _, _, _, _, _, _, _, _, id)), _, _) =>\n        creationId = id\n    }\n\n    container.send(fsm, getActivation())\n    container.expectMsg(ActivationResponse(Right(messages(0))))\n\n    // deleting the ID from creationIds set\n    fsm ! SuccessfulCreationJob(creationId, testInvocationNamespace, fqn, revision)\n\n    fsm ! GracefulShutdown\n\n    // When it receives a graceful shutdown, it is supposed to delete etcd data first to create another queue in other schedulers.\n    // But still this queue should handle existing messages so it does not stop scheduling actors.\n    dataMgmtService.expectMsgAllOf(\n      UnregisterData(leaderKey),\n      UnregisterData(namespaceThrottlingKey),\n      UnregisterData(actionThrottlingKey))\n\n    probe.expectMsg(Transition(fsm, Running, Removing))\n\n    // a newly arrived message should be properly handled\n    fsm ! messages(1)\n    container.send(fsm, getActivation())\n    container.expectMsg(ActivationResponse(Right(messages(1))))\n\n    fsm ! messages(2)\n\n    // if there is a message, it should not terminate\n    fsm ! StateTimeout\n\n    container.send(fsm, getActivation())\n    container.expectMsg(ActivationResponse(Right(messages(2))))\n\n    fsm ! StateTimeout\n\n    // it doesn't need to check if all containers are timeout as same version of a new queue will be created in another scheduler.\n\n    watcher.expectMsgAllOf(\n      UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n      UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n      UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n    probe.expectMsg(Transition(fsm, Removing, Removed))\n\n    // the queue is timed out again in the Removed state\n    fsm ! StateTimeout\n    parent.expectMsg(queueRemovedMsg)\n    fsm ! QueueRemovedCompleted\n\n    probe.expectTerminated(fsm, 10.seconds)\n  }\n\n  it should \"be deprecated when a new queue supersedes it.\" in {\n    // GracefulShuttingDown is not applicable\n    val allStates = List(Running, Idle, Flushing, ActionThrottled, NamespaceThrottled, Removing, Removed)\n\n    allStates.foreach { state =>\n      implicit val clock = SystemClock\n      val mockEtcdClient = mock[EtcdClient]\n      val parent = TestProbe()\n      val watcher = TestProbe()\n      val dataMgmtService = TestProbe()\n      val containerManager = TestProbe()\n      val decisionMaker = TestProbe()\n      decisionMaker.ignoreMsg { case _: QueueSnapshot => true }\n      val probe = TestProbe()\n\n      println(s\"start with $state\")\n\n      expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n      val fsm =\n        TestFSMRef(\n          new MemoryQueue(\n            mockEtcdClient,\n            durationChecker,\n            fqn,\n            mockMessaging(),\n            schedulingConfig,\n            testInvocationNamespace,\n            revision,\n            endpoints,\n            actionMetadata,\n            dataMgmtService.ref,\n            watcher.ref,\n            containerManager.ref,\n            decisionMaker.ref,\n            schedulerId,\n            ack,\n            store,\n            getUserLimit,\n            checkToDropStaleActivation,\n            queueConfig),\n          parent.ref)\n\n      probe watch fsm\n      registerCallback(probe, fsm)\n\n      fsm ! Start\n      expectInitialData(watcher, dataMgmtService)\n      fsm ! testInitialDataStorageResult\n\n      probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n      fsm ! message\n\n      fsm.setState(state)\n\n      probe.expectMsg(Transition(fsm, Running, state))\n\n      // queue endpoint is removed for some reason\n      fsm ! WatchEndpointRemoved(\"watchKey\", `leaderKey`, \"watchValue\", false)\n\n      // try to restore it\n      dataMgmtService.expectMsg(RegisterInitialData(leaderKey, \"watchValue\", failoverEnabled = false, Some(fsm)))\n\n      // another queue is already running\n      fsm ! InitialDataStorageResults(`leaderKey`, Left(AlreadyExist()))\n      parent.expectMsg(queueRemovedMsg)\n      parent.expectMsg(message)\n\n      // clean up actors only because etcd data is being used by a new queue\n      watcher.expectMsgAllOf(\n        UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n        UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n        UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n      // move to the Deprecated state\n      probe.expectMsg(Transition(fsm, state, Removed))\n\n      // the queue is timed out again in the Removed state\n      fsm ! StateTimeout\n      parent.expectMsg(queueRemovedMsg)\n      fsm ! QueueRemovedCompleted\n\n      probe.expectTerminated(fsm, 10.seconds)\n    }\n  }\n\n  it should \"be deprecated and stops even if the queue manager could not respond.\" in {\n    // GracefulShuttingDown is not applicable\n    val allStates = List(Running, Idle, Flushing, ActionThrottled, NamespaceThrottled, Removing, Removed)\n\n    allStates.foreach { state =>\n      implicit val clock = SystemClock\n      val mockEtcdClient = mock[EtcdClient]\n      val parent = TestProbe()\n      val watcher = TestProbe()\n      val dataMgmtService = TestProbe()\n      val containerManager = TestProbe()\n      val decisionMaker = TestProbe()\n      decisionMaker.ignoreMsg { case _: QueueSnapshot => true }\n      val probe = TestProbe()\n\n      println(s\"start with $state\")\n\n      expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n      val fsm =\n        TestFSMRef(\n          new MemoryQueue(\n            mockEtcdClient,\n            durationChecker,\n            fqn,\n            mockMessaging(),\n            schedulingConfig,\n            testInvocationNamespace,\n            revision,\n            endpoints,\n            actionMetadata,\n            dataMgmtService.ref,\n            watcher.ref,\n            containerManager.ref,\n            decisionMaker.ref,\n            schedulerId,\n            ack,\n            store,\n            getUserLimit,\n            checkToDropStaleActivation,\n            queueConfig),\n          parent.ref)\n\n      probe watch fsm\n      registerCallback(probe, fsm)\n\n      fsm ! Start\n      expectInitialData(watcher, dataMgmtService)\n      fsm ! testInitialDataStorageResult\n\n      probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n      fsm ! message\n\n      fsm.setState(state)\n\n      probe.expectMsg(Transition(fsm, Running, state))\n\n      // queue endpoint is removed for some reason\n      fsm ! WatchEndpointRemoved(\"watchKey\", `leaderKey`, \"watchValue\", false)\n\n      // try to restore it\n      dataMgmtService.expectMsg(RegisterInitialData(leaderKey, \"watchValue\", failoverEnabled = false, Some(fsm)))\n\n      // another queue is already running\n      fsm ! InitialDataStorageResults(`leaderKey`, Left(AlreadyExist()))\n\n      parent.expectMsg(queueRemovedMsg)\n      parent.expectMsg(message)\n\n      // clean up actors only because etcd data is being used by a new queue\n      watcher.expectMsgAllOf(\n        UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n        UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n        UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n      // move to the Deprecated state\n      probe.expectMsg(Transition(fsm, state, Removed))\n\n      // queue manager could not respond to the memory queue.\n      fsm ! StateTimeout\n\n      // the queue is supposed to send queueRemovedMsg once again and stops itself.\n      parent.expectMsg(queueRemovedMsg)\n      fsm ! QueueRemovedCompleted\n      probe.expectTerminated(fsm, 10.seconds)\n    }\n  }\n\n  // this is to guarantee StopScheduling is handled in all states\n  it should \"handle StopScheduling in any states.\" in {\n    val testContainerId = \"fakeContainerId\"\n    val allStates =\n      List(Running, Idle, ActionThrottled, NamespaceThrottled, Flushing, Removing, Removed)\n\n    allStates.foreach { state =>\n      implicit val clock = SystemClock\n      val mockEtcdClient = mock[EtcdClient]\n      val parent = TestProbe()\n      val watcher = TestProbe()\n      val dataMgmtService = TestProbe()\n      val containerManager = TestProbe()\n      val decisionMaker = TestProbe()\n      decisionMaker.ignoreMsg { case _: QueueSnapshot => true }\n      val probe = TestProbe()\n      val container = TestProbe()\n      val schedulingActors = TestProbe()\n\n      println(s\"start with $state\")\n\n      expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n      val fsm =\n        TestFSMRef(\n          new MemoryQueue(\n            mockEtcdClient,\n            durationChecker,\n            fqn,\n            mockMessaging(),\n            schedulingConfig,\n            testInvocationNamespace,\n            revision,\n            endpoints,\n            actionMetadata,\n            dataMgmtService.ref,\n            watcher.ref,\n            containerManager.ref,\n            decisionMaker.ref,\n            schedulerId,\n            ack,\n            store,\n            getUserLimit,\n            checkToDropStaleActivation,\n            queueConfig),\n          parent.ref)\n\n      probe watch fsm\n      registerCallback(probe, fsm)\n\n      fsm ! Start\n      expectInitialData(watcher, dataMgmtService)\n      fsm ! testInitialDataStorageResult\n\n      probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n      state match {\n        case NamespaceThrottled | ActionThrottled =>\n          fsm ! message\n          fsm.setState(state, ThrottledData(schedulingActors.ref, schedulingActors.ref))\n\n        case Flushing =>\n          fsm ! message\n          fsm.setState(state, FlushingData(schedulingActors.ref, schedulingActors.ref, WhiskError, \"whisk error\"))\n\n        case Removing =>\n          fsm.underlyingActor.containers = mutable.Set(testContainerId)\n          fsm ! message\n          fsm.setState(state, RemovingData(schedulingActors.ref, schedulingActors.ref, outdated = true))\n\n        case Idle =>\n          fsm ! StateTimeout\n\n        case Removed =>\n          fsm ! StateTimeout\n          probe.expectMsg(Transition(fsm, Running, Idle))\n\n          fsm ! StateTimeout\n          watcher.expectMsgAllOf(\n            UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n          probe.expectMsg(Transition(fsm, Idle, Removed))\n\n        case _ =>\n          fsm ! message\n          fsm.setState(state)\n      }\n\n      if (state != Removed) {\n        probe.expectMsg(Transition(fsm, Running, state))\n      }\n\n      fsm ! StopSchedulingAsOutdated\n\n      state match {\n        case Removing =>\n          // still exist old container for old queue, fetch the queue by old container\n          container.send(fsm, getActivation())\n          container.expectMsg(ActivationResponse(Right(message)))\n          // has no old containers for old queue, so send the message to queueManager\n          fsm.underlyingActor.containers = mutable.Set.empty[String]\n          fsm.underlyingActor.queue =\n            Queue.apply(TimeSeriesActivationEntry(Instant.ofEpochMilli(Instant.now.toEpochMilli + 1000), message))\n          fsm ! StopSchedulingAsOutdated\n          parent.expectMsg(message)\n\n          // queue should be terminated after gracefulShutdownTimeout\n          fsm ! StateTimeout\n\n          // clean up actors only because etcd data is being used by a new queue\n          watcher.expectMsgAllOf(\n            UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n          containerManager.expectMsg(ContainerDeletion(testInvocationNamespace, fqn, revision, actionMetadata))\n\n          probe.expectMsg(Transition(fsm, Removing, Removed))\n          probe.expectTerminated(fsm, 10.seconds)\n\n        // queue will be removed soon, do nothing\n        case Removed =>\n          fsm ! QueueRemovedCompleted\n          probe.expectTerminated(fsm, 10.seconds)\n\n        case Idle =>\n          watcher.expectMsgAllOf(\n            UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n          // queue is stale and will be removed\n          parent.expectMsg(staleQueueRemovedMsg)\n          containerManager.expectMsg(ContainerDeletion(testInvocationNamespace, fqn, revision, actionMetadata))\n\n          probe.expectMsg(Transition(fsm, state, Removed))\n\n          fsm ! QueueRemovedCompleted\n          probe.expectTerminated(fsm, 10.seconds)\n\n        case Flushing =>\n          // queue is stale and will be removed\n          parent.expectMsg(staleQueueRemovedMsg)\n          probe.expectMsg(Transition(fsm, state, Removed))\n\n          fsm ! QueueRemovedCompleted\n          fsm ! StateTimeout\n\n          watcher.expectMsgAllOf(\n            UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n          probe.expectTerminated(fsm, 10.seconds)\n\n        case _ =>\n          parent.expectMsg(staleQueueRemovedMsg)\n          parent.expectMsg(message)\n          // queue is stale and will be removed\n          probe.expectMsg(Transition(fsm, state, Removing))\n\n          fsm ! QueueRemovedCompleted\n\n          watcher.expectMsgAllOf(\n            UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n          containerManager.expectMsg(ContainerDeletion(testInvocationNamespace, fqn, revision, actionMetadata))\n\n          // move to the Deprecated state\n          probe.expectMsg(Transition(fsm, Removing, Removed))\n          probe.expectTerminated(fsm, 10.seconds)\n      }\n    }\n  }\n\n  // this is to guarantee GracefulShutdown is handled in all states\n  it should \"handle GracefulShutdown in any states.\" in {\n    val allStates =\n      List(Running, Idle, ActionThrottled, NamespaceThrottled, Flushing, Removing, Removed)\n\n    allStates.foreach { state =>\n      implicit val clock = SystemClock\n      val mockEtcdClient = mock[EtcdClient]\n      val parent = TestProbe()\n      val watcher = TestProbe()\n      val dataMgmtService = TestProbe()\n      val containerManager = TestProbe()\n      val decisionMaker = TestProbe()\n      decisionMaker.ignoreMsg { case _: QueueSnapshot => true }\n      val probe = TestProbe()\n      val container = TestProbe()\n      val schedulingActors = TestProbe()\n\n      println(s\"start with $state\")\n\n      expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n      val fsm =\n        TestFSMRef(\n          new MemoryQueue(\n            mockEtcdClient,\n            durationChecker,\n            fqn,\n            mockMessaging(),\n            schedulingConfig,\n            testInvocationNamespace,\n            revision,\n            endpoints,\n            actionMetadata,\n            dataMgmtService.ref,\n            watcher.ref,\n            containerManager.ref,\n            decisionMaker.ref,\n            schedulerId,\n            ack,\n            store,\n            getUserLimit,\n            checkToDropStaleActivation,\n            queueConfig),\n          parent.ref)\n\n      probe watch fsm\n      registerCallback(probe, fsm)\n\n      fsm ! Start\n      expectInitialData(watcher, dataMgmtService)\n\n      probe.expectMsg(Transition(fsm, Uninitialized, Running))\n\n      fsm ! testInitialDataStorageResult\n\n      state match {\n        case NamespaceThrottled | ActionThrottled =>\n          fsm ! message\n          fsm.setState(state, ThrottledData(schedulingActors.ref, schedulingActors.ref))\n\n        case Flushing =>\n          fsm ! message\n          fsm.setState(state, FlushingData(schedulingActors.ref, schedulingActors.ref, WhiskError, \"whisk error\"))\n\n        case Removing =>\n          fsm ! message\n          fsm.setState(state, RemovingData(schedulingActors.ref, schedulingActors.ref, outdated = true))\n\n        case Idle =>\n          fsm ! StateTimeout\n\n        case Removed =>\n          fsm ! StateTimeout\n          probe.expectMsg(Transition(fsm, Running, Idle))\n\n          fsm ! StateTimeout\n          watcher.expectMsgAllOf(\n            UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n          probe.expectMsg(Transition(fsm, Idle, Removed))\n\n        case _ =>\n          fsm ! message\n          fsm.setState(state)\n      }\n\n      if (state != Removed) {\n        probe.expectMsg(Transition(fsm, Running, state))\n      }\n\n      fsm ! GracefulShutdown\n\n      state match {\n        // queue will be gracefully shutdown.\n        case Removing =>\n          // queue should not be terminated as there is an activation\n          fsm ! StateTimeout\n\n          container.send(fsm, getActivation())\n          container.expectMsg(ActivationResponse(Right(message)))\n\n          // queue should not be terminated as there is an activation\n          fsm ! StateTimeout\n\n          // clean up actors only because etcd data is being used by a new queue\n          watcher.expectMsgAllOf(\n            UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n          // move to the Deprecated state\n          probe.expectMsg(Transition(fsm, Removing, Removed))\n          probe.expectTerminated(fsm, 10.seconds)\n\n        // queue will be removed soon, do nothing\n        case Removed =>\n          fsm ! QueueRemovedCompleted\n          probe.expectTerminated(fsm, 10.seconds)\n\n        case Idle | Flushing =>\n          watcher.expectMsgAllOf(\n            UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n          probe.expectMsg(Transition(fsm, state, Removed))\n\n          // the queue is timed out againd in the Removed state\n          fsm ! StateTimeout\n\n          // queue is stale and will be removed\n          parent.expectMsg(queueRemovedMsg)\n          fsm ! QueueRemovedCompleted\n          probe.expectTerminated(fsm, 10.seconds)\n\n        case _ =>\n          // queue is stale and will be removed\n\n          probe.expectMsg(Transition(fsm, state, Removing))\n\n          // queue should not be terminated as there is an activation\n          fsm ! StateTimeout\n\n          container.send(fsm, getActivation())\n          container.expectMsg(ActivationResponse(Right(message)))\n\n          fsm ! StateTimeout\n\n          watcher.expectMsgAllOf(\n            UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n            UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n\n          probe.expectMsg(Transition(fsm, Removing, Removed))\n\n          // the queue is timed out againd in the Removed state\n          fsm ! StateTimeout\n          parent.expectMsg(queueRemovedMsg)\n          fsm ! QueueRemovedCompleted\n          probe.expectTerminated(fsm, 10.seconds)\n      }\n    }\n  }\n\n  def registerCallback(probe: TestProbe, c: ActorRef) = {\n    c ! SubscribeTransitionCallBack(probe.ref)\n    probe.expectMsg(CurrentState(c, Uninitialized))\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/queue/test/MemoryQueueTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue.test\n\nimport java.time.Instant\nimport java.util.concurrent.Executor\nimport java.{lang, util}\nimport org.apache.pekko.actor.ActorRef\nimport org.apache.pekko.actor.FSM.{CurrentState, StateTimeout, SubscribeTransitionCallBack, Transition}\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.testkit.{ImplicitSender, TestActor, TestFSMRef, TestKit, TestProbe}\nimport org.apache.pekko.util.Timeout\nimport com.google.protobuf.ByteString\nimport com.ibm.etcd.api.Event.EventType\nimport com.ibm.etcd.api._\nimport com.ibm.etcd.client.kv.KvClient.Watch\nimport com.ibm.etcd.client.kv.WatchUpdate\nimport com.ibm.etcd.client.{EtcdClient => Client}\nimport com.sksamuel.elastic4s.http.ElasticClient\nimport common.StreamLogging\nimport org.apache.openwhisk.common.time.SystemClock\nimport org.apache.openwhisk.common.{GracefulShutdown, TransactionId}\nimport org.apache.openwhisk.core.ack.ActiveAck\nimport org.apache.openwhisk.core.connector._\nimport org.apache.openwhisk.core.containerpool.ContainerId\nimport org.apache.openwhisk.core.database.NoDocumentException\nimport org.apache.openwhisk.core.entity.ExecManifest.ImageName\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.size._\nimport org.apache.openwhisk.core.etcd.EtcdKV.ContainerKeys.{existingContainers, inProgressContainer}\nimport org.apache.openwhisk.core.etcd._\nimport org.apache.openwhisk.core.scheduler.grpc.{GetActivation, ActivationResponse => GetActivationResponse}\nimport org.apache.openwhisk.core.scheduler.message.{ContainerCreation, FailedCreationJob, SuccessfulCreationJob}\nimport org.apache.openwhisk.core.scheduler.queue.MemoryQueue.checkToDropStaleActivation\nimport org.apache.openwhisk.core.scheduler.queue._\nimport org.apache.openwhisk.core.service._\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.BeforeAndAfter\nimport org.scalatest.BeforeAndAfterEach\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json.{JsObject, JsString}\n\nimport scala.collection.immutable.Queue\nimport scala.collection.mutable\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.language.{higherKinds, postfixOps}\n\n@RunWith(classOf[JUnitRunner])\nclass MemoryQueueTests\n    extends MemoryQueueTestsFixture\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with BeforeAndAfter\n    with StreamLogging {\n\n  override def beforeEach(): Unit = {\n    super.beforeEach()\n    NamespaceContainerCount.instances.clear()\n    ackedMessageCount = 0\n    storedMessageCount = 0\n  }\n  override def afterAll(): Unit = {\n    logLines.foreach(println)\n    client.close()\n    NamespaceContainerCount.instances.clear()\n    TestKit.shutdownActorSystem(system)\n  }\n\n  implicit val askTimeout: Timeout = Timeout(5 seconds)\n\n  action.revision(revision)\n  val memory = action.limits.memory.megabytes.MB\n\n  val testContainerId = \"fakeContainerId\"\n\n  val testQueueCreationMessage = CreateQueue(testInvocationNamespace, fqn, revision, actionMetadata)\n\n  val client: Client = {\n    val hostAndPorts = \"172.17.0.1:2379\"\n    Client.forEndpoints(hostAndPorts).withPlainText().build()\n  }\n\n  def registerCallback(c: ActorRef) = {\n    c ! SubscribeTransitionCallBack(testActor)\n    expectMsg(CurrentState(c, Uninitialized))\n  }\n\n  val mockWatch = new Watch {\n    override def close(): Unit = {}\n\n    override def addListener(listener: Runnable, executor: Executor): Unit = {}\n\n    override def cancel(mayInterruptIfRunning: Boolean): Boolean = true\n\n    override def isCancelled: Boolean = true\n\n    override def isDone: Boolean = true\n\n    override def get(): lang.Boolean = true\n\n    override def get(timeout: Long, unit: TimeUnit): lang.Boolean = true\n  }\n\n  class mockWatchUpdate extends WatchUpdate {\n    private var eventLists: util.List[Event] = new util.ArrayList[Event]()\n    override def getHeader: ResponseHeader = ???\n\n    def addEvents(event: Event): WatchUpdate = {\n      eventLists.add(event)\n      this\n    }\n\n    override def getEvents: util.List[Event] = eventLists\n  }\n\n  behavior of \"MemoryQueue\"\n\n  it should \"send StateTimeout message when state timeout\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val prove = TestProbe()\n    val watcher = TestProbe()\n    val parent = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val queueConfigWithShortTimeout = queueConfig.copy(\n      idleGrace = 10.milliseconds,\n      stopGrace = 10.milliseconds,\n      gracefulShutdownTimeout = 10.milliseconds)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          prove.ref,\n          watcher.ref,\n          TestProbe().ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfigWithShortTimeout),\n        parent.ref,\n        \"MemoryQueue\")\n\n    registerCallback(fsm)\n    fsm ! Start\n    expectMsg(Transition(fsm, Uninitialized, Running))\n\n    // Test stateTimeout for when(Running, stateTimeout = queueConfig.idleGrace)\n    fsm.isStateTimerActive shouldBe true\n    Thread.sleep(queueConfigWithShortTimeout.idleGrace.toMillis)\n\n    expectMsg(Transition(fsm, Running, Idle))\n\n    // Test stateTimeout for when(Idle, stateTimeout = queueConfig.stopGrace)\n    fsm.isStateTimerActive shouldBe true\n    Thread.sleep(queueConfigWithShortTimeout.stopGrace.toMillis)\n    expectMsg(Transition(fsm, Idle, Removed))\n\n    // Test stateTimeout for when(Removed, stateTimeout = queueConfig.gracefulShutdownTimeout)\n    fsm.isStateTimerActive shouldBe true\n    Thread.sleep(queueConfigWithShortTimeout.gracefulShutdownTimeout.toMillis)\n    parent.expectMsg(queueRemovedMsg)\n  }\n\n  it should \"start startTimerWithFixedDelay(name=StopQueue) on Transition _ => Flushing\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val prove = TestProbe()\n    val watcher = TestProbe()\n    val parent = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val queueConfigWithShortTimeout =\n      queueConfig.copy(\n        idleGrace = 10.seconds,\n        stopGrace = 10.milliseconds,\n        gracefulShutdownTimeout = 10.milliseconds,\n        flushGrace = 10.milliseconds)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          prove.ref,\n          watcher.ref,\n          TestProbe().ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfigWithShortTimeout),\n        parent.ref,\n        \"MemoryQueue\")\n\n    registerCallback(fsm)\n    fsm ! Start\n    expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm ! FailedCreationJob(\n      testCreationId,\n      message.user.namespace.name.asString,\n      message.action,\n      message.revision,\n      ContainerCreationError.NoAvailableInvokersError,\n      \"no available invokers\")\n\n    // Test case _ -> Flushing => startTimerWithFixedDelay(\"StopQueue\", StateTimeout, queueConfig.flushGrace)\n    // state Running -> Flushing\n    expectMsg(Transition(fsm, Running, Flushing))\n    fsm.isTimerActive(\"StopQueue\") shouldBe true\n\n    // wait for flushGrace time, StopQueue timer should send StateTimeout\n    Thread.sleep(queueConfigWithShortTimeout.flushGrace.toMillis)\n    expectMsg(Transition(fsm, Flushing, Removed))\n  }\n\n  it should \"register the endpoint when initializing\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val prove = TestProbe()\n    val watcher = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          prove.ref,\n          watcher.ref,\n          TestProbe().ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n\n    registerCallback(fsm)\n\n    fsm ! Start\n    watcher.expectMsgAllOf(\n      WatchEndpoint(inProgressContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(existingContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        inProgressContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        existingContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)))\n    prove.expectMsg(RegisterInitialData(namespaceThrottlingKey, false.toString, false))\n    prove.expectMsg(RegisterData(actionThrottlingKey, false.toString, false))\n    fsm ! testInitialDataStorageResult\n\n    expectMsg(Transition(fsm, Uninitialized, Running))\n    fsm.stop()\n  }\n\n  it should \"go to Flushing state if any error happens when the queue is depreacted\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val prove = TestProbe()\n    val watcher = TestProbe()\n    val containerManager = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          prove.ref,\n          watcher.ref,\n          containerManager.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n\n    registerCallback(fsm)\n\n    fsm ! Start\n    watcher.expectMsgAllOf(\n      WatchEndpoint(inProgressContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(existingContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        inProgressContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        existingContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)))\n    prove.expectMsg(RegisterInitialData(namespaceThrottlingKey, false.toString, false))\n    prove.expectMsg(RegisterData(actionThrottlingKey, false.toString, false))\n\n    expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm ! failedInitialDataStorageResult\n    expectMsg(Transition(fsm, Running, Removed))\n\n    fsm.stop()\n  }\n\n  it should \"go to the Running state without storing any data if it receives VersionUpdated\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val prove = TestProbe()\n    val watcher = TestProbe()\n    val dataManagementService = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataManagementService.ref,\n          watcher.ref,\n          prove.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n\n    watcher.expectMsgAllOf(\n      WatchEndpoint(inProgressContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(existingContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        inProgressContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        existingContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)))\n\n    registerCallback(fsm)\n\n    fsm ! VersionUpdated\n    dataManagementService.expectNoMessage()\n    expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm.stop()\n  }\n\n  it should \"remove the queue when timeout occurs\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val parent = TestProbe()\n    val dataManagementService = TestProbe()\n    val schedulerActor = TestProbe().ref\n    val droppingActor = TestProbe().ref\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataManagementService.ref,\n          TestProbe().ref,\n          TestProbe().ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref,\n        \"MemoryQueue\")\n\n    registerCallback(fsm)\n\n    val probe = TestProbe()\n    val probe2 = TestProbe()\n    probe watch schedulerActor\n    probe watch droppingActor\n    probe2 watch fsm\n\n    // do not remove itself when there are still existing containers\n    fsm.underlyingActor.containers = mutable.Set(\"1\")\n    fsm.setState(Running, RunningData(schedulerActor, droppingActor))\n    expectMsg(Transition(fsm, Uninitialized, Running))\n    fsm ! StateTimeout\n    probe.expectNoMessage()\n    parent.expectNoMessage()\n    probe2.expectNoMessage()\n    dataManagementService.expectNoMessage()\n\n    // change the existing containers count to 0, the StateTimeout should work\n    fsm.underlyingActor.containers = mutable.Set.empty[String]\n    fsm ! StateTimeout\n    probe.expectTerminated(schedulerActor)\n    probe.expectTerminated(droppingActor)\n    parent.expectNoMessage()\n    expectMsg(Transition(fsm, Running, Idle))\n\n    fsm ! StateTimeout\n    expectMsg(Transition(fsm, Idle, Removed))\n    dataManagementService.expectMsg(UnregisterData(leaderKey))\n    dataManagementService.expectMsg(UnregisterData(namespaceThrottlingKey))\n    dataManagementService.expectMsg(UnregisterData(actionThrottlingKey))\n\n    // queue is timed out again in the Removed state.\n    fsm ! StateTimeout\n    parent.expectMsg(QueueRemoved(testInvocationNamespace, fqn.toDocId.asDocInfo(revision), Some(leaderKey)))\n    fsm ! QueueRemovedCompleted\n    probe2.expectTerminated(fsm)\n\n    fsm.stop()\n  }\n\n  it should \"back to Running state when got new ActivationMessage when in Idle State\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val prove = TestProbe()\n    val watcher = TestProbe()\n    val dataManagementService = TestProbe()\n    val tid = TransactionId(TransactionId.generateTid())\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataManagementService.ref,\n          watcher.ref,\n          prove.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n\n    watcher.expectMsgAllOf(\n      WatchEndpoint(inProgressContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(existingContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        inProgressContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        existingContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)))\n\n    registerCallback(fsm)\n    val queueRef = fsm.underlyingActor\n\n    fsm ! Start\n    expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm.underlyingActor.creationIds = mutable.Set.empty[String]\n    fsm ! StateTimeout\n    expectMsg(Transition(fsm, Running, Idle))\n\n    queueRef.queue.length shouldBe 0\n    fsm ! message\n    queueRef.queue.length shouldBe 1\n    expectMsg(Transition(fsm, Idle, Running))\n\n    (fsm ? GetActivation(tid, fqn, testContainerId, false, None))\n      .mapTo[GetActivationResponse]\n      .futureValue shouldBe GetActivationResponse(Right(message))\n\n    queueRef.queue.length shouldBe 0\n    fsm.underlyingActor.containers = mutable.Set.empty[String]\n    fsm.underlyingActor.creationIds = mutable.Set.empty[String]\n    fsm ! StateTimeout\n    expectMsg(Transition(fsm, Running, Idle))\n    (fsm ? GetActivation(tid, fqn, testContainerId, false, None))\n      .mapTo[GetActivationResponse]\n      .futureValue shouldBe GetActivationResponse(Left(NoActivationMessage()))\n\n    fsm.stop()\n  }\n\n  it should \"back to Running state when got new ActivationMessage when in Removed State\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val prove = TestProbe()\n    val watcher = TestProbe()\n    val dataManagementService = TestProbe()\n    val parent = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataManagementService.ref,\n          watcher.ref,\n          prove.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref)\n\n    watcher.expectMsgAllOf(\n      WatchEndpoint(inProgressContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(existingContainerKey, \"\", true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        inProgressContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        existingContainerPrefixKeyByNamespace,\n        \"\",\n        true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)))\n\n    registerCallback(fsm)\n    val queueRef = fsm.underlyingActor\n\n    fsm ! Start\n    expectMsg(Transition(fsm, Uninitialized, Running))\n\n    fsm.underlyingActor.creationIds = mutable.Set.empty[String]\n    fsm ! StateTimeout\n    expectMsg(Transition(fsm, Running, Idle))\n\n    fsm ! StateTimeout\n    expectMsg(Transition(fsm, Idle, Removed))\n    queueRef.queue.length shouldBe 0\n    parent.expectMsg(queueRemovedMsg)\n\n    fsm ! message\n\n    // queue is timed out again in the Removed state.\n    parent.expectMsg(message)\n\n    fsm ! StateTimeout\n\n    expectNoMessage()\n\n    fsm.stop()\n  }\n\n  it should \"store the received ActivationMessage in the queue\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = new MockEtcdClient(client, isLeader = true)\n    val prove = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          prove.ref,\n          prove.ref,\n          prove.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n    val queueRef = fsm.underlyingActor\n\n    fsm.setState(Running, RunningData(prove.ref, prove.ref))\n    queueRef.queue.length shouldBe 0\n\n    fsm ! message\n\n    queueRef.queue.length shouldBe 1\n\n    fsm.stop()\n\n  }\n\n  it should \"send a ActivationMessage in response to GetActivation\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val probe = TestProbe()\n    val tid = TransactionId(TransactionId.generateTid())\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          probe.ref,\n          probe.ref,\n          probe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n    fsm.setState(Running, RunningData(TestProbe().ref, TestProbe().ref))\n\n    fsm ! message\n\n    (fsm ? GetActivation(tid, fqn, testContainerId, false, None))\n      .mapTo[GetActivationResponse]\n      .futureValue shouldBe GetActivationResponse(Right(message))\n\n    fsm.stop()\n  }\n\n  it should \"send NoActivationMessage in case there is no message in the queue\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val probe = TestProbe()\n    val tid = TransactionId(TransactionId.generateTid())\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          probe.ref,\n          probe.ref,\n          probe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n\n    fsm.setState(Running, RunningData(probe.ref, probe.ref))\n\n    fsm ! GetActivation(tid, fqn, testContainerId, false, None)\n    // will poll for 1 seconds to return NoActivationMessage, so set the max wait time to 2 seconds here\n    expectMsg(2.seconds, GetActivationResponse(Left(NoActivationMessage())))\n\n    fsm.stop()\n  }\n\n  it should \"poll for the ActivationMessage in case there is no message in the queue\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val probe = TestProbe()\n    val tid = TransactionId(TransactionId.generateTid())\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          probe.ref,\n          probe.ref,\n          probe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n\n    fsm.setState(Running, RunningData(probe.ref, probe.ref))\n\n    fsm ! GetActivation(tid, fqn, testContainerId, false, None)\n    fsm ! GetActivation(tid, fqn, testContainerId, false, None)\n    fsm ! GetActivation(tid, fqn, testContainerId, false, None)\n    fsm ! GetActivation(tid, fqn, testContainerId, false, None)\n    fsm ! message\n    fsm ! message\n    fsm ! message\n\n    // will get three activation response, and one NoActivationMessage\n    expectMsg(GetActivationResponse(Right(message)))\n    expectMsg(GetActivationResponse(Right(message)))\n    expectMsg(GetActivationResponse(Right(message)))\n    expectMsg(2.seconds, GetActivationResponse(Left(NoActivationMessage())))\n    fsm.stop()\n  }\n\n  it should \"not send msg to a deleted container\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val probe = TestProbe()\n    val tid = TransactionId(TransactionId.generateTid())\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          probe.ref,\n          probe.ref,\n          probe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n\n    fsm.setState(Running, RunningData(probe.ref, probe.ref))\n\n    val sender1 = TestProbe()\n    val sender2 = TestProbe()\n    fsm.tell(GetActivation(tid, fqn, \"1\", false, None), sender1.ref)\n    fsm.tell(GetActivation(tid, fqn, \"2\", false, None), sender2.ref)\n    fsm.tell(GetActivation(tid, fqn, \"2\", false, None, false), sender2.ref)\n    fsm ! message\n\n    // sender 1 will get a message while sender 2 will get a NoActivationMessage\n    sender1.expectMsg(GetActivationResponse(Right(message)))\n    sender2.expectMsg(GetActivationResponse(Left(NoActivationMessage())))\n    sender2.expectMsg(GetActivationResponse(Left(NoActivationMessage())))\n\n    fsm.tell(GetActivation(tid, fqn, \"1\", false, None), sender1.ref)\n    fsm.tell(GetActivation(tid, fqn, \"2\", false, None), sender2.ref)\n    fsm ! WatchEndpointRemoved(existingContainerKey, \"2\", \"\", true) // remove container2 using watch event\n    fsm ! message\n\n    // sender 1 will get a message while sender 2 will get a NoActivationMessage\n    sender1.expectMsg(GetActivationResponse(Right(message)))\n    sender2.expectMsg(GetActivationResponse(Left(NoActivationMessage())))\n  }\n\n  it should \"send response to request according to the order of container id and warmed flag\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val probe = TestProbe()\n    val tid = TransactionId(TransactionId.generateTid())\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          probe.ref,\n          probe.ref,\n          probe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig))\n\n    fsm.setState(Running, RunningData(probe.ref, probe.ref))\n\n    val sender1 = TestProbe()\n    val sender2 = TestProbe()\n    val sender3 = TestProbe()\n    val sender4 = TestProbe()\n    fsm.tell(GetActivation(tid, fqn, \"1\", false, None), sender1.ref)\n    fsm.tell(GetActivation(tid, fqn, \"2\", false, None), sender2.ref)\n    fsm.tell(GetActivation(tid, fqn, \"3\", false, None), sender3.ref)\n    fsm.tell(GetActivation(tid, fqn, \"4\", false, None), sender4.ref)\n    fsm ! message\n    fsm ! message\n    fsm ! message\n\n    // sender 2-4 will get a message while sender 1 will get a NoActivationMessage\n    sender4.expectMsg(GetActivationResponse(Right(message)))\n    sender3.expectMsg(GetActivationResponse(Right(message)))\n    sender2.expectMsg(GetActivationResponse(Right(message)))\n    sender1.expectMsg(2.seconds, GetActivationResponse(Left(NoActivationMessage())))\n\n    // container \"1\" is warmed one\n    fsm.tell(GetActivation(tid, fqn, \"1\", true, None), sender1.ref)\n    fsm.tell(GetActivation(tid, fqn, \"2\", false, None), sender2.ref)\n    fsm.tell(GetActivation(tid, fqn, \"3\", false, None), sender3.ref)\n    fsm.tell(GetActivation(tid, fqn, \"4\", false, None), sender4.ref)\n    fsm ! message\n    fsm ! message\n    fsm ! message\n\n    // sender 1, 3, 4 will get a message while sender 2 will get a NoActivationMessage\n    sender1.expectMsg(GetActivationResponse(Right(message)))\n    sender3.expectMsg(GetActivationResponse(Right(message)))\n    sender4.expectMsg(GetActivationResponse(Right(message)))\n    sender2.expectMsg(2.seconds, GetActivationResponse(Left(NoActivationMessage())))\n    fsm.stop()\n  }\n\n  it should \"send a container creation request to ContainerManager at initialization time\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val containerManger = TestProbe()\n    val probe = TestProbe()\n    val decisionMaker = TestProbe()\n\n    // This pilot must conform to SchedulingDecisionMaker\n    // Since a queue tries to add the initial container at startup, this pilot just makes a decision accordingly.\n    decisionMaker.setAutoPilot((sender: ActorRef, msg) => {\n      msg match {\n        case msg: QueueSnapshot =>\n          sender ! DecisionResults(AddContainer, 1)\n      }\n      TestActor.KeepRunning\n    })\n\n    val schedulerHost = endpoints.host\n    val rpcPort = endpoints.rpcPort\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          probe.ref,\n          probe.ref,\n          containerManger.ref,\n          decisionMaker.ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        probe.ref,\n        \"MemoryQueue\")\n\n    fsm ! Start\n    containerManger.expectMsgPF(10 seconds) {\n      case ContainerCreation(\n          List(\n            ContainerCreationMessage(\n              _,\n              `testInvocationNamespace`,\n              `fqn`,\n              _,\n              `actionMetadata`,\n              `schedulerId`,\n              `schedulerHost`,\n              `rpcPort`,\n              _,\n              _)),\n          `memory`,\n          `testInvocationNamespace`) =>\n        true\n    }\n\n    fsm.stop()\n  }\n\n  it should \"complete error activation while received FailedCreationJob and the error is not a whisk error(unrecoverable)\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val testProbe = TestProbe()\n    val parent = TestProbe()\n    val expectedCount = 3\n\n    val probe = TestProbe()\n\n    val newAck = new ActiveAck {\n      override def apply(tid: TransactionId,\n                         activationResult: WhiskActivation,\n                         blockingInvoke: Boolean,\n                         controllerInstance: ControllerInstanceId,\n                         userId: UUID,\n                         acknowledgement: AcknowledgementMessage): Future[Any] = {\n        probe.ref ! activationResult.response\n        Future.successful({})\n      }\n    }\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          testProbe.ref,\n          testProbe.ref,\n          testProbe.ref,\n          TestProbe().ref,\n          schedulerId,\n          newAck,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref,\n        \"MemoryQueue\")\n\n    fsm ! SubscribeTransitionCallBack(parent.ref)\n    parent.expectMsg(CurrentState(fsm, Uninitialized))\n    parent watch fsm\n\n    fsm ! Start\n\n    parent.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    (1 to expectedCount).foreach(_ => fsm ! message)\n    fsm ! FailedCreationJob(\n      testCreationId,\n      message.user.namespace.name.asString,\n      message.action,\n      message.revision,\n      ContainerCreationError.NonExecutableActionError,\n      \"nonExecutbleAction error\")\n    parent.expectMsg(Transition(fsm, Running, Flushing))\n    (1 to expectedCount).foreach(_ => probe.expectMsg(ActivationResponse.developerError(\"nonExecutbleAction error\")))\n\n    // flush msg immediately\n    fsm ! message\n    probe.expectMsg(ActivationResponse.developerError(\"nonExecutbleAction error\"))\n\n    parent.expectMsgAllOf(2 * queueConfig.flushGrace + 5.seconds, queueRemovedMsg, Transition(fsm, Flushing, Removed))\n\n    fsm ! StateTimeout\n    parent.expectMsg(queueRemovedMsg)\n    fsm ! QueueRemovedCompleted\n    parent.expectTerminated(fsm)\n\n    fsm.stop()\n  }\n\n  it should \"complete error activation after timeout while received FailedCreationJob and the error is a whisk error(recoverable)\" in {\n    implicit val clock = new FakeClock\n    val mockEtcdClient = mock[EtcdClient]\n    val testProbe = TestProbe()\n    val decisionMaker = TestProbe()\n    decisionMaker.ignoreMsg { case _: QueueSnapshot => true }\n    val parent = TestProbe()\n    val expectedCount = 3\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val queueConfig = QueueConfig(5 seconds, 10 seconds, 10 seconds, 5 seconds, 10, 10000, 20000, 0.9, 10, false)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          testProbe.ref,\n          testProbe.ref,\n          testProbe.ref,\n          decisionMaker.ref,\n          schedulerId,\n          ack,\n          store,\n          (s: String) => { Future.successful(10000) }, // avoid exceed user limit\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref,\n        \"MemoryQueue\")\n\n    fsm ! SubscribeTransitionCallBack(parent.ref)\n    parent.expectMsg(CurrentState(fsm, Uninitialized))\n\n    fsm ! Start\n\n    parent.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    (1 to expectedCount).foreach(_ => fsm ! message)\n    fsm ! FailedCreationJob(\n      testCreationId,\n      message.user.namespace.name.asString,\n      message.action,\n      message.revision,\n      ContainerCreationError.NoAvailableInvokersError,\n      \"no available invokers\")\n\n    parent.expectMsg(Transition(fsm, Running, Flushing))\n    parent.expectNoMessage(5.seconds)\n\n    // Add 3 more messages.\n    clock.plusSeconds(5)\n    (1 to expectedCount).foreach(_ => fsm ! message)\n    parent.expectNoMessage(5.seconds)\n\n    // After 10 seconds(action retention timeout), the first 3 messages are timed out.\n    // It does not get removed as there are still 3 messages in the queue.\n    clock.plusSeconds(5)\n    fsm ! DropOld\n\n    awaitAssert({\n      ackedMessageCount shouldBe 3\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(\"no available invokers\")))\n      storedMessageCount shouldBe 3\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(\"no available invokers\")))\n    }, 5.seconds)\n\n    // should goto Running\n    fsm ! SuccessfulCreationJob(testCreationId, message.user.namespace.name.asString, message.action, message.revision)\n\n    parent.expectMsg(Transition(fsm, Flushing, Running))\n\n    // should goto Flushing again as there is no container running.\n    fsm ! FailedCreationJob(\n      testCreationId,\n      message.user.namespace.name.asString,\n      message.action,\n      message.revision,\n      ContainerCreationError.ResourceNotEnoughError,\n      \"resource not enough\")\n    parent.expectMsg(Transition(fsm, Running, Flushing))\n\n    // wait for the flush grace, and then all existing activations will be flushed\n    clock.plusSeconds((queueConfig.maxBlackboxRetentionMs + queueConfig.flushGrace.toMillis) / 1000)\n    fsm ! DropOld\n\n    // The error message is updated from the recent error message of the FailedCreationJob.\n    awaitAssert({\n      ackedMessageCount shouldBe 6\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(\"resource not enough\")))\n      storedMessageCount shouldBe 6\n      lastAckedActivationResult.response.result shouldBe Some(JsObject(\"error\" -> JsString(\"resource not enough\")))\n    }, 5.seconds)\n\n    // should goto Removed\n    parent.expectMsgAnyOf(queueRemovedMsg, Transition(fsm, Flushing, Removed))\n\n    fsm ! QueueRemovedCompleted\n\n    fsm.stop()\n  }\n\n  it should \"send old version activation to queueManager when update action if doesn't exist old version container\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val probe = TestProbe()\n    val queueManager = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          probe.ref,\n          probe.ref,\n          probe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        queueManager.ref)\n\n    val now = Instant.now\n    fsm.underlyingActor.queue =\n      Queue.apply(TimeSeriesActivationEntry(Instant.ofEpochMilli(now.toEpochMilli + 1000), message))\n    fsm.underlyingActor.containers = mutable.Set.empty[String]\n    fsm.setState(Running, RunningData(probe.ref, probe.ref))\n    fsm ! StopSchedulingAsOutdated // update action\n    queueManager.expectMsg(staleQueueRemovedMsg)\n    queueManager.expectMsg(message)\n    fsm.stop()\n  }\n\n  it should \"fetch old version activation by old container when update action\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val probe = TestProbe()\n    val queueManager = TestProbe()\n    val tid = TransactionId(TransactionId.generateTid())\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          probe.ref,\n          probe.ref,\n          probe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        queueManager.ref)\n\n    val now = Instant.now\n    fsm.underlyingActor.queue =\n      Queue.apply(TimeSeriesActivationEntry(Instant.ofEpochMilli(now.toEpochMilli + 1000), message))\n    fsm.underlyingActor.containers = mutable.Set(testContainerId)\n    fsm.setState(Running, RunningData(probe.ref, probe.ref))\n    fsm ! StopSchedulingAsOutdated // update action\n    queueManager.expectMsg(staleQueueRemovedMsg)\n    (fsm ? GetActivation(tid, fqn, testContainerId, false, None))\n      .mapTo[GetActivationResponse]\n      .futureValue shouldBe GetActivationResponse(Right(message))\n    fsm.stop()\n  }\n\n  it should \"complete error activation after blackbox timeout when the action is a blackbox action and received FailedCreationJob with a whisk error(recoverable)\" in {\n    implicit val clock = new FakeClock\n    val mockEtcdClient = mock[EtcdClient]\n    val testProbe = TestProbe()\n    val decisionMaker = TestProbe()\n    decisionMaker.ignoreMsg { case _: QueueSnapshot => true }\n    val parent = TestProbe()\n    val expectedCount = 3\n\n    val probe = TestProbe()\n    val newAck = new ActiveAck {\n      override def apply(tid: TransactionId,\n                         activationResult: WhiskActivation,\n                         blockingInvoke: Boolean,\n                         controllerInstance: ControllerInstanceId,\n                         userId: UUID,\n                         acknowledgement: AcknowledgementMessage): Future[Any] = {\n        probe.ref ! activationResult.response\n        Future.successful({})\n      }\n    }\n\n    val execMetadata = BlackBoxExecMetaData(ImageName(\"test\"), None, native = false)\n\n    val blackboxActionMetadata =\n      WhiskActionMetaData(\n        action.namespace,\n        action.name,\n        execMetadata,\n        action.parameters,\n        action.limits,\n        action.version,\n        action.publish,\n        action.annotations)\n        .revision[WhiskActionMetaData](action.rev)\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val queueConfig = QueueConfig(5 seconds, 10 seconds, 10 seconds, 5 seconds, 10, 10000, 20000, 0.9, 10, false)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          blackboxActionMetadata,\n          testProbe.ref,\n          testProbe.ref,\n          testProbe.ref,\n          decisionMaker.ref,\n          schedulerId,\n          newAck,\n          store,\n          (s: String) => { Future.successful(10000) }, // avoid exceed user limit\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref,\n        \"MemoryQueue\")\n\n    fsm ! SubscribeTransitionCallBack(parent.ref)\n    parent.expectMsg(CurrentState(fsm, Uninitialized))\n    parent watch fsm\n\n    fsm ! Start\n\n    parent.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    (1 to expectedCount).foreach(_ => fsm ! message)\n    fsm ! FailedCreationJob(\n      testCreationId,\n      message.user.namespace.name.asString,\n      message.action,\n      message.revision,\n      ContainerCreationError.NoAvailableInvokersError,\n      \"no available invokers\")\n\n    parent.expectMsg(Transition(fsm, Running, Flushing))\n    probe.expectNoMessage()\n\n    // should wait for sometime before flush message\n    fsm ! message\n\n    // wait for the flush grace, and then some existing activations will be flushed\n    clock.plusSeconds((queueConfig.maxBlackboxRetentionMs + queueConfig.flushGrace.toMillis) / 1000)\n    fsm ! DropOld\n    (1 to expectedCount).foreach(_ => probe.expectMsg(ActivationResponse.whiskError(\"no available invokers\")))\n\n    val duration = FiniteDuration(queueConfig.maxBlackboxRetentionMs, MILLISECONDS) + queueConfig.flushGrace\n    probe.expectMsg(duration, ActivationResponse.whiskError(\"no available invokers\"))\n    parent.expectMsgAllOf(duration, queueRemovedMsg, Transition(fsm, Flushing, Removed))\n    fsm ! QueueRemovedCompleted\n    parent.expectTerminated(fsm)\n\n    fsm.stop()\n  }\n\n  it should \"stop scheduling if the namespace does not exist\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val getZeroLimit = (_: String) => { Future.failed(NoDocumentException(\"namespace does not exist\")) }\n    val testProbe = TestProbe()\n    val parent = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm =\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          testProbe.ref,\n          testProbe.ref,\n          testProbe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getZeroLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        parent.ref,\n        \"MemoryQueue\")\n    val probe = TestProbe()\n    probe watch fsm\n\n    fsm ! SubscribeTransitionCallBack(parent.ref)\n    fsm ! Start\n\n    parent.expectMsg(10 seconds, CurrentState(fsm, Uninitialized))\n    parent.expectMsg(10 seconds, Transition(fsm, Uninitialized, Running))\n\n    fsm ! StopSchedulingAsOutdated\n    parent expectMsgAllOf (10 seconds, Transition(fsm, Running, Removing), QueueRemoved(\n      testInvocationNamespace,\n      fqn.toDocId.asDocInfo(revision),\n      None))\n\n    fsm ! QueueRemovedCompleted\n    parent.expectMsg(10 seconds, Transition(fsm, Removing, Removed))\n\n    probe.expectTerminated(fsm, 10 seconds)\n  }\n\n  it should \"throttle the namespace when the limit is already reached\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n    val parent = TestProbe()\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm = TestFSMRef(\n      {\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataManagementService.ref,\n          probe.ref,\n          probe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig)\n      },\n      probe.ref,\n      \"MemoryQueue\")\n\n    fsm ! SubscribeTransitionCallBack(parent.ref)\n    parent.expectMsg(CurrentState(fsm, Uninitialized))\n\n    fsm ! Start\n    dataManagementService.expectMsg(RegisterInitialData(namespaceThrottlingKey, false.toString, false))\n    dataManagementService.expectMsg(RegisterData(actionThrottlingKey, false.toString, false))\n\n    parent.expectMsg(10 seconds, Transition(fsm, Uninitialized, Running))\n\n    fsm ! EnableNamespaceThrottling(dropMsg = true)\n    parent.expectMsg(10 seconds, Transition(fsm, Running, NamespaceThrottled))\n    dataManagementService.expectMsg(RegisterData(namespaceThrottlingKey, true.toString, false))\n\n    fsm.stop()\n  }\n\n  it should \"disable namespace throttling when the capacity become available\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n    val decisionMaker = TestProbe()\n\n    // This test pilot mimic the decision maker who disable the namespace throttling when there is enough capacity.\n    decisionMaker.setAutoPilot((sender: ActorRef, msg) => {\n      msg match {\n        case QueueSnapshot(_, _, _, _, _, _, _, _, _, _, _, _, NamespaceThrottled, _) =>\n          sender ! DisableNamespaceThrottling\n\n        case _ =>\n        //do nothing\n      }\n      TestActor.KeepRunning\n    })\n\n    // it always induces the throttling\n    val getUserLimit = (_: String) => { Future.successful(4) }\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm = TestFSMRef({\n      new MemoryQueue(\n        mockEtcdClient,\n        durationChecker,\n        fqn,\n        mockMessaging(),\n        schedulingConfig,\n        testInvocationNamespace,\n        revision,\n        endpoints,\n        actionMetadata,\n        dataManagementService.ref,\n        probe.ref,\n        probe.ref,\n        decisionMaker.ref,\n        schedulerId,\n        ack,\n        store,\n        getUserLimit,\n        checkToDropStaleActivation,\n        queueConfig)\n    })\n\n    registerCallback(fsm)\n\n    fsm ! Start\n\n    expectMsg(10 seconds, Transition(fsm, Uninitialized, Running))\n\n    fsm.setState(NamespaceThrottled, ThrottledData(probe.ref, probe.ref))\n    expectMsg(10 seconds, Transition(fsm, Running, NamespaceThrottled))\n    expectMsg(10 seconds, Transition(fsm, NamespaceThrottled, Running))\n\n    dataManagementService.expectMsg(RegisterInitialData(namespaceThrottlingKey, false.toString, false))\n\n    fsm.stop()\n  }\n\n  it should \"throttle the action when the number of messages reaches the limit\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n\n    // it always induces the throttling\n    val getZeroLimit = (_: String) => { Future.successful(2) }\n\n    val queueConfig = QueueConfig(5 seconds, 10 seconds, 10 seconds, 5 seconds, 1, 5000, 10000, 0.9, 10, false)\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm = TestFSMRef {\n      new MemoryQueue(\n        mockEtcdClient,\n        durationChecker,\n        fqn,\n        mockMessaging(),\n        schedulingConfig,\n        testInvocationNamespace,\n        revision,\n        endpoints,\n        actionMetadata,\n        dataManagementService.ref,\n        probe.ref,\n        probe.ref,\n        TestProbe().ref,\n        schedulerId,\n        ack,\n        store,\n        getZeroLimit,\n        checkToDropStaleActivation,\n        queueConfig)\n    }\n\n    registerCallback(fsm)\n\n    fsm.setState(Running, RunningData(probe.ref, probe.ref))\n    expectMsg(10 seconds, Transition(fsm, Uninitialized, Running))\n    fsm ! message\n\n    dataManagementService.expectMsg(RegisterData(actionThrottlingKey, true.toString, false))\n\n    expectMsg(10 seconds, Transition(fsm, Running, ActionThrottled))\n\n    fsm.stop()\n  }\n\n  it should \"disable action throttling when the number of messages is under throttling fraction\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n    val parent = TestProbe()\n\n    val queueConfig = QueueConfig(5 seconds, 10 seconds, 10 seconds, 5 seconds, 10, 5000, 10000, 0.9, 10, false)\n    val msgRetentionSize = queueConfig.maxRetentionSize\n\n    val tid = TransactionId(TransactionId.generateTid())\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm = TestFSMRef {\n      new MemoryQueue(\n        mockEtcdClient,\n        durationChecker,\n        fqn,\n        mockMessaging(),\n        schedulingConfig,\n        testInvocationNamespace,\n        revision,\n        endpoints,\n        actionMetadata,\n        dataManagementService.ref,\n        probe.ref,\n        probe.ref,\n        TestProbe().ref,\n        schedulerId,\n        ack,\n        store,\n        getUserLimit,\n        checkToDropStaleActivation,\n        queueConfig)\n    }\n\n    fsm ! SubscribeTransitionCallBack(parent.ref)\n    parent.expectMsg(CurrentState(fsm, Uninitialized))\n\n    fsm.setState(Running, RunningData(probe.ref, probe.ref))\n    parent.expectMsg(Transition(fsm, Uninitialized, Running))\n\n    (1 to msgRetentionSize).foreach { _ =>\n      fsm ! message\n    }\n\n    parent.expectMsg(Transition(fsm, Running, ActionThrottled))\n    dataManagementService.expectMsg(RegisterData(actionThrottlingKey, true.toString, false))\n\n    fsm ! GetActivation(tid, fqn, testContainerId, false, None)\n\n    //receive one activation message\n    parent.expectMsg(Transition(fsm, ActionThrottled, Running))\n    dataManagementService.expectMsg(RegisterData(actionThrottlingKey, false.toString, false))\n\n    fsm.stop()\n  }\n\n  it should \"update the number of containers based on Watch event\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = new MockEtcdClient(client, true)\n    val probe = TestProbe()\n    val watcher = system.actorOf(WatcherService.props(mockEtcdClient))\n    val testSchedulingDecisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testInvocationNamespace, fqn, schedulingConfig))\n\n    val mockFunction = (_: String) => {\n      Future.successful(4)\n    }\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm = TestFSMRef {\n      new MemoryQueue(\n        mockEtcdClient,\n        durationChecker,\n        fqn,\n        mockMessaging(),\n        schedulingConfig,\n        testInvocationNamespace,\n        revision,\n        endpoints,\n        actionMetadata,\n        probe.ref,\n        watcher,\n        probe.ref,\n        testSchedulingDecisionMaker,\n        schedulerId,\n        ack,\n        store,\n        mockFunction,\n        checkToDropStaleActivation,\n        queueConfig)\n    }\n\n    fsm.setState(Uninitialized)\n    fsm ! Start\n\n    val memoryQueue = fsm.underlyingActor\n    val newFqn = fqn.copy(version = Some(SemVer(0, 0, 2)))\n    val newRevision = DocRevision(\"2-testRev\")\n\n    memoryQueue.containers.size shouldBe 0\n    memoryQueue.creationIds.count(_.startsWith(\"testId\")) shouldBe 0\n    memoryQueue.namespaceContainerCount.existingContainerNumByNamespace shouldBe 0\n    memoryQueue.namespaceContainerCount.inProgressContainerNumByNamespace shouldBe 0\n\n    val testInvoker = InvokerInstanceId(0, userMemory = 1024.MB)\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      inProgressContainer(testInvocationNamespace, fqn, revision, schedulerId, CreationId(\"testId1\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      existingContainers(\n        testInvocationNamespace,\n        fqn,\n        revision,\n        Some(testInvoker),\n        Some(ContainerId(\"test-containerId1\"))),\n      \"test-value\")\n\n    // container with other version should not be counted\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      inProgressContainer(testInvocationNamespace, newFqn, newRevision, schedulerId, CreationId(\"testId2\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      existingContainers(\n        testInvocationNamespace,\n        newFqn,\n        newRevision,\n        Some(testInvoker),\n        Some(ContainerId(\"test-containerId2\"))),\n      \"test-value\")\n\n    awaitAssert({\n      memoryQueue.containers.size shouldBe 1 // ['test-containerId1']\n      memoryQueue.creationIds.count(_.startsWith(\"testId\")) shouldBe 1 // ['testId1']\n      memoryQueue.namespaceContainerCount.existingContainerNumByNamespace shouldBe 2\n      memoryQueue.namespaceContainerCount.inProgressContainerNumByNamespace shouldBe 2\n    }, 5.seconds)\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      inProgressContainer(testInvocationNamespace, fqn, revision, schedulerId, CreationId(\"testId3\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      existingContainers(\n        testInvocationNamespace,\n        fqn,\n        revision,\n        Some(testInvoker),\n        Some(ContainerId(\"test-containerId3\"))),\n      \"test-value\")\n\n    // container with other version should not be counted\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      inProgressContainer(testInvocationNamespace, newFqn, newRevision, schedulerId, CreationId(\"testId4\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.PUT,\n      existingContainers(\n        testInvocationNamespace,\n        newFqn,\n        newRevision,\n        Some(testInvoker),\n        Some(ContainerId(\"test-containerId4\"))),\n      \"test-value\")\n\n    awaitAssert({\n      memoryQueue.containers.size shouldBe 2 // ['test-containerId1', 'test-containerId3']\n      memoryQueue.creationIds.count(_.startsWith(\"testId\")) shouldBe 2 // ['testId1', 'testId3']\n      memoryQueue.namespaceContainerCount.existingContainerNumByNamespace shouldBe 4\n      memoryQueue.namespaceContainerCount.inProgressContainerNumByNamespace shouldBe 4\n    }, 5.seconds)\n\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      inProgressContainer(testInvocationNamespace, fqn, revision, schedulerId, CreationId(\"testId1\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      inProgressContainer(testInvocationNamespace, fqn, revision, schedulerId, CreationId(\"testId3\")),\n      \"test-value\")\n\n    // container with other version should not be counted\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      inProgressContainer(testInvocationNamespace, newFqn, newRevision, schedulerId, CreationId(\"testId2\")),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      inProgressContainer(testInvocationNamespace, newFqn, newRevision, schedulerId, CreationId(\"testId4\")),\n      \"test-value\")\n\n    awaitAssert({\n      memoryQueue.containers.size shouldBe 2 // ['test-containerId1', 'test-containerId3']\n      memoryQueue.creationIds.count(_.startsWith(\"testId\")) shouldBe 0\n      memoryQueue.namespaceContainerCount.inProgressContainerNumByNamespace shouldBe 0\n      memoryQueue.namespaceContainerCount.existingContainerNumByNamespace shouldBe 4\n    }, 5.seconds)\n\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      existingContainers(\n        testInvocationNamespace,\n        fqn,\n        revision,\n        Some(testInvoker),\n        Some(ContainerId(\"test-containerId1\"))),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      existingContainers(\n        testInvocationNamespace,\n        fqn,\n        revision,\n        Some(testInvoker),\n        Some(ContainerId(\"test-containerId3\"))),\n      \"test-value\")\n\n    // container with other version should not be counted\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      existingContainers(\n        testInvocationNamespace,\n        newFqn,\n        newRevision,\n        Some(testInvoker),\n        Some(ContainerId(\"test-containerId2\"))),\n      \"test-value\")\n\n    mockEtcdClient.publishEvents(\n      EventType.DELETE,\n      existingContainers(\n        testInvocationNamespace,\n        newFqn,\n        newRevision,\n        Some(testInvoker),\n        Some(ContainerId(\"test-containerId4\"))),\n      \"test-value\")\n\n    awaitAssert({\n      memoryQueue.containers.size shouldBe 0\n      memoryQueue.creationIds.count(_.startsWith(\"testId\")) shouldBe 0\n      memoryQueue.namespaceContainerCount.inProgressContainerNumByNamespace shouldBe 0\n      memoryQueue.namespaceContainerCount.existingContainerNumByNamespace shouldBe 0\n    }, 5.seconds)\n  }\n\n  private def getData(states: List[MemoryQueueState]) = {\n    val schedulingActors = List.fill(states.size)(TestProbe())\n    val droppingActors = List.fill(states.size)(TestProbe())\n    val data =\n      (schedulingActors zip droppingActors)\n        .map {\n          case (schedulingActor, droppingActor) =>\n            RunningData(schedulingActor.ref, droppingActor.ref)\n        }\n    (schedulingActors, droppingActors, data)\n  }\n  it should \"clean up throttling data when it stops gracefully\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n    val actorProbe = TestProbe()\n    val states = List(Running, ActionThrottled, NamespaceThrottled, Flushing)\n    val (schedulingActors, droppingActors, data) = getData(states)\n\n    val fsmList = (1 to states.size).map { index =>\n      expectDurationChecking(mockEsClient, testInvocationNamespace)\n      TestFSMRef(\n        new MemoryQueue(\n          mockEtcdClient,\n          durationChecker,\n          fqn,\n          mockMessaging(),\n          schedulingConfig,\n          testInvocationNamespace,\n          revision,\n          endpoints,\n          actionMetadata,\n          dataManagementService.ref,\n          probe.ref,\n          probe.ref,\n          TestProbe().ref,\n          schedulerId,\n          ack,\n          store,\n          getUserLimit,\n          checkToDropStaleActivation,\n          queueConfig),\n        probe.ref,\n        s\"MemoryQueue$index\")\n    }.toList\n\n    schedulingActors foreach (actorProbe watch _.ref)\n    droppingActors foreach (actorProbe watch _.ref)\n\n    fsmList zip states zip data foreach {\n      case ((fsm, state), datum) =>\n        fsm.setState(state, datum)\n    }\n\n    fsmList zip data foreach {\n      case (fsm, RunningData(schedulingActor, droppingActor)) =>\n        fsm ! GracefulShutdown\n\n        inAnyOrder {\n          dataManagementService.expectMsg(UnregisterData(leaderKey))\n          dataManagementService.expectMsg(UnregisterData(namespaceThrottlingKey))\n          dataManagementService.expectMsg(UnregisterData(actionThrottlingKey))\n        }\n    }\n\n    fsmList foreach { _.stop() }\n  }\n\n  behavior of \"drop function\"\n\n  val completeErrorActivation = (msg: ActivationMessage, reason: String, isWhiskError: Boolean) => {\n    Future.successful({})\n  }\n\n  it should \"drop the old activation from the queue\" in {\n    var queue = Queue.empty[TimeSeriesActivationEntry]\n\n    val clock = new FakeClock\n    val now = clock.now()\n    val records = List(\n      TimeSeriesActivationEntry(Instant.ofEpochMilli(now.toEpochMilli + 1000), message),\n      TimeSeriesActivationEntry(Instant.ofEpochMilli(now.toEpochMilli + 2000), message),\n      TimeSeriesActivationEntry(Instant.ofEpochMilli(now.toEpochMilli + 3000), message),\n      TimeSeriesActivationEntry(Instant.ofEpochMilli(now.toEpochMilli + 10000), message),\n      TimeSeriesActivationEntry(Instant.ofEpochMilli(now.toEpochMilli + 20000), message),\n      TimeSeriesActivationEntry(Instant.ofEpochMilli(now.toEpochMilli + 30000), message),\n    )\n\n    records.foreach(record => queue = queue.enqueue(record))\n    clock.plusSeconds(5)\n\n    queue = MemoryQueue.dropOld(\n      clock,\n      queue,\n      java.time.Duration.ofMillis(1000),\n      \"activation processing is not initiated for 1000 ms\",\n      completeErrorActivation)\n\n    queue.size shouldBe 3\n  }\n\n  it should \"not raise any exception with empty queue\" in {\n    var queue = Queue.empty[TimeSeriesActivationEntry]\n\n    noException should be thrownBy {\n      queue = MemoryQueue.dropOld(\n        SystemClock,\n        queue,\n        java.time.Duration.ofMillis(1000),\n        \"activation processing is not initiated for 1000 ms\",\n        completeErrorActivation)\n    }\n  }\n\n  behavior of \"duration checker\"\n\n  it should \"check the duration once\" in {\n    implicit val clock = SystemClock\n    val mockEtcdClient = mock[EtcdClient]\n\n    val dataManagementService = TestProbe()\n    val probe = TestProbe()\n\n    val mockEsClient = mock[ElasticClient]\n    val durationChecker = new ElasticSearchDurationChecker(mockEsClient, durationCheckWindow)\n\n    expectDurationChecking(mockEsClient, testInvocationNamespace)\n\n    val fsm = TestFSMRef(\n      new MemoryQueue(\n        mockEtcdClient,\n        durationChecker,\n        fqn,\n        mockMessaging(),\n        schedulingConfig,\n        testInvocationNamespace,\n        revision,\n        endpoints,\n        actionMetadata,\n        dataManagementService.ref,\n        probe.ref,\n        probe.ref,\n        TestProbe().ref,\n        schedulerId,\n        ack,\n        store,\n        getUserLimit,\n        checkToDropStaleActivation,\n        queueConfig),\n      probe.ref)\n\n    fsm ! Start\n\n    Thread.sleep(1000)\n  }\n\n  class MockWatcher extends Watch {\n    var isClosed = false\n\n    override def close(): Unit = {\n      isClosed = true\n    }\n\n    override def addListener(listener: Runnable, executor: Executor): Unit = {}\n\n    override def cancel(mayInterruptIfRunning: Boolean): Boolean = true\n\n    override def isCancelled: Boolean = true\n\n    override def isDone: Boolean = true\n\n    override def get(): lang.Boolean = true\n\n    override def get(timeout: Long, unit: TimeUnit): lang.Boolean = true\n  }\n\n  class MockEtcdClient(client: Client, isLeader: Boolean, leaseNotFound: Boolean = false, failedCount: Int = 1)\n      extends EtcdClient(client)(ece) {\n    var count = 0\n    var storedValues = List.empty[(String, String, Long, Long)]\n    var dataMap = Map[String, String]()\n\n    override def putTxn[T](key: String, value: T, cmpVersion: Long, leaseId: Long): Future[TxnResponse] = {\n      if (isLeader) {\n        storedValues = (key, value.toString, cmpVersion, leaseId) :: storedValues\n      }\n      Future.successful(TxnResponse.newBuilder().setSucceeded(isLeader).build())\n    }\n\n    /*\n     * this method count the number of entries whose key starts with the given prefix\n     */\n    override def getCount(prefixKey: String): Future[Long] = {\n      Future.successful { dataMap.count(data => data._1.startsWith(prefixKey)) }\n    }\n\n    var watchCallbackMap = Map[String, WatchUpdate => Unit]()\n\n    override def keepAliveOnce(leaseId: Long): Future[LeaseKeepAliveResponse] =\n      Future.successful(LeaseKeepAliveResponse.newBuilder().setID(leaseId).build())\n\n    /*\n     * this method adds one callback for the given key in watchCallbackMap.\n     *\n     * Note: Currently it only supports prefix-based watch.\n     */\n    override def watchAllKeys(next: WatchUpdate => Unit, error: Throwable => Unit, completed: () => Unit): Watch = {\n\n      watchCallbackMap += \"\" -> next\n      new Watch {\n        override def close(): Unit = {}\n\n        override def addListener(listener: Runnable, executor: Executor): Unit = {}\n\n        override def cancel(mayInterruptIfRunning: Boolean): Boolean = true\n\n        override def isCancelled: Boolean = true\n\n        override def isDone: Boolean = true\n\n        override def get(): lang.Boolean = true\n\n        override def get(timeout: Long, unit: TimeUnit): lang.Boolean = true\n      }\n    }\n\n    /*\n     * This method stores the data in dataMap to simulate etcd.put()\n     * After then, it calls the registered watch callback for the given key\n     * So we don't need to call put() to simulate watch API.\n     * Expected order of calls is 1. watch(), 2.publishEvents(). Data will be stored in dataMap and\n     * callbacks in the callbackMap for the given prefix will be called by publishEvents()\n     *\n     * Note: watch callback is currently registered based on prefix only.\n     */\n    def publishEvents(eventType: EventType, key: String, value: String): Unit = {\n      val eType = eventType match {\n        case EventType.PUT =>\n          dataMap += key -> value\n          EventType.PUT\n\n        case EventType.DELETE =>\n          dataMap -= key\n          EventType.DELETE\n\n        case EventType.UNRECOGNIZED => Event.EventType.UNRECOGNIZED\n      }\n      val event = Event\n        .newBuilder()\n        .setType(eType)\n        .setPrevKv(\n          KeyValue\n            .newBuilder()\n            .setKey(ByteString.copyFromUtf8(key))\n            .setValue(ByteString.copyFromUtf8(value))\n            .build())\n        .setKv(\n          KeyValue\n            .newBuilder()\n            .setKey(ByteString.copyFromUtf8(key))\n            .setValue(ByteString.copyFromUtf8(value))\n            .build())\n        .build()\n\n      // find the callbacks which has the proper prefix for the given key\n      watchCallbackMap.filter(callback => key.startsWith(callback._1)).foreach { callback =>\n        callback._2(new mockWatchUpdate().addEvents(event))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/queue/test/MemoryQueueTestsFixture.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue.test\n\nimport java.time.Instant\n\nimport org.apache.pekko.actor.{ActorRef, ActorSystem}\nimport org.apache.pekko.testkit.{ImplicitSender, TestKit, TestProbe}\nimport com.sksamuel.elastic4s.http\nimport com.sksamuel.elastic4s.http.ElasticDsl.{avgAgg, boolQuery, matchQuery, rangeQuery, search}\nimport com.sksamuel.elastic4s.http._\nimport com.sksamuel.elastic4s.http.search.{SearchHits, SearchResponse}\nimport com.sksamuel.elastic4s.searches.SearchRequest\nimport common.StreamLogging\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.common.WhiskInstants.InstantImplicits\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.ack.ActiveAck\nimport org.apache.openwhisk.core.connector.{\n  AcknowledgementMessage,\n  ActivationMessage,\n  Message,\n  MessageProducer,\n  ResultMetadata\n}\nimport org.apache.openwhisk.core.database.UserContext\nimport org.apache.openwhisk.core.database.elasticsearch.ElasticSearchActivationStore.generateIndex\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity.{WhiskActivation, _}\nimport org.apache.openwhisk.core.etcd.EtcdKV.{ContainerKeys, QueueKeys, ThrottlingKeys}\nimport org.apache.openwhisk.core.scheduler.{SchedulerEndpoints, SchedulingConfig}\nimport org.apache.openwhisk.core.scheduler.grpc.GetActivation\nimport org.apache.openwhisk.core.scheduler.queue.ElasticSearchDurationChecker.{getFromDate, AverageAggregationName}\nimport org.apache.openwhisk.core.scheduler.queue._\nimport org.apache.openwhisk.core.service.{\n  AlreadyExist,\n  DeleteEvent,\n  Done,\n  InitialDataStorageResults,\n  PutEvent,\n  RegisterData,\n  RegisterInitialData,\n  UnregisterData,\n  UnwatchEndpoint,\n  WatchEndpoint\n}\nimport org.scalamock.scalatest.MockFactory\n\nimport scala.concurrent.duration.{DurationInt}\nimport scala.concurrent.{ExecutionContextExecutor, Future}\nimport scala.language.{higherKinds, postfixOps}\n\nclass MemoryQueueTestsFixture\n    extends TestKit(ActorSystem(\"MemoryQueue\"))\n    with MockFactory\n    with ImplicitSender\n    with StreamLogging {\n\n  implicit val ece: ExecutionContextExecutor = system.dispatcher\n\n  // common variables\n  def requiredProperties =\n    Map(\n      WhiskConfig.actionInvokePerMinuteLimit -> null,\n      WhiskConfig.actionInvokeConcurrentLimit -> null,\n      WhiskConfig.triggerFirePerMinuteLimit -> null)\n  val config = new WhiskConfig(requiredProperties)\n\n  // action variables\n  val testInvocationNamespace = \"test-invocation-namespace\"\n  val testNamespace = \"test-namespace\"\n  val testAction = \"test-action\"\n\n  val schedulingConfig = SchedulingConfig(100.milliseconds, 100.milliseconds, 10.seconds, false, 1.5)\n\n  val fqn = FullyQualifiedEntityName(EntityPath(testNamespace), EntityName(testAction), Some(SemVer(0, 0, 1)))\n  val revision = DocRevision(\"1-testRev\")\n  val exec = CodeExecAsString(RuntimeManifest(\"actionKind\", ImageName(\"testImage\")), \"testCode\", None)\n  val action = ExecutableWhiskAction(EntityPath(testNamespace), EntityName(testAction), exec)\n  val execMetadata =\n    CodeExecMetaDataAsString(RuntimeManifest(action.exec.kind, ImageName(\"test\")), entryPoint = Some(\"test\"))\n  val actionMetadata =\n    WhiskActionMetaData(\n      action.namespace,\n      action.name,\n      execMetadata,\n      action.parameters,\n      action.limits,\n      action.version,\n      action.publish,\n      action.annotations)\n      .revision[WhiskActionMetaData](action.rev)\n  val dummyWhiskActivation = WhiskActivation(\n    EntityPath(\"testnamespace\"),\n    EntityName(\"activation\"),\n    Subject(),\n    ActivationId.generate(),\n    start = Instant.now.inMills,\n    end = Instant.now.inMills)\n\n  val messageTransId = TransactionId(TransactionId.testing.meta.id)\n  val uuid = UUID()\n  val message = ActivationMessage(\n    messageTransId,\n    action.fullyQualifiedName(true),\n    action.rev,\n    Identity(\n      Subject(),\n      Namespace(EntityName(testInvocationNamespace), uuid),\n      BasicAuthenticationAuthKey(uuid, Secret()),\n      Set.empty),\n    ActivationId.generate(),\n    ControllerInstanceId(\"0\"),\n    blocking = false,\n    content = None)\n\n  // scheduler variables\n  val schedulerId = SchedulerInstanceId(\"0\")\n  val endpoints = SchedulerEndpoints(\"127.0.0.1\", 2552, 8080)\n\n  // elasticsearch\n  val durationCheckWindow = 1.days\n  val mockEsClient = mock[ElasticClient]\n  val durationChecker = new ElasticSearchDurationChecker(mockEsClient, durationCheckWindow)\n\n  // ETCD variables\n  val leaderKey = QueueKeys.queue(testInvocationNamespace, fqn, leader = true)\n  val leaderValue: String = endpoints.serialize\n\n  val inProgressContainerKey =\n    ContainerKeys.containerPrefix(ContainerKeys.inProgressPrefix, testInvocationNamespace, fqn, Some(revision))\n  val existingContainerKey =\n    ContainerKeys.containerPrefix(ContainerKeys.namespacePrefix, testInvocationNamespace, fqn, Some(revision))\n  val inProgressContainerPrefixKeyByNamespace =\n    ContainerKeys.inProgressContainerPrefixByNamespace(testInvocationNamespace)\n  val existingContainerPrefixKeyByNamespace =\n    ContainerKeys.existingContainersPrefixByNamespace(testInvocationNamespace)\n  val namespaceThrottlingKey = ThrottlingKeys.namespace(EntityName(testInvocationNamespace))\n  val actionThrottlingKey = ThrottlingKeys.action(testInvocationNamespace, fqn.copy(version = None))\n\n  // queue variables\n  val queueConfig = QueueConfig(5 seconds, 10 seconds, 5 seconds, 5 seconds, 10, 10000, 20000, 0.9, 10, false)\n  val idleGrace = queueConfig.idleGrace\n  val stopGrace = queueConfig.stopGrace\n  val flushGrace = queueConfig.flushGrace\n  val retentionTimeout = queueConfig.maxRetentionMs\n  val blackboxTimeout = queueConfig.maxBlackboxRetentionMs\n  val gracefulShutdownTimeout = queueConfig.gracefulShutdownTimeout\n  val testRetentionSize = queueConfig.maxRetentionSize\n  val testThrottlingFraction = queueConfig.throttlingFraction\n  val queueRemovedMsg = QueueRemoved(testInvocationNamespace, fqn.toDocId.asDocInfo(revision), Some(leaderKey))\n  val staleQueueRemovedMsg = QueueRemoved(testInvocationNamespace, fqn.toDocId.asDocInfo(revision), None)\n  val queueReactivatedMsg = QueueReactivated(testInvocationNamespace, fqn, fqn.toDocId.asDocInfo(revision))\n\n  // DataManagementService\n  val testInitialDataStorageResult = InitialDataStorageResults(leaderKey, Right(Done()))\n  val failedInitialDataStorageResult = InitialDataStorageResults(leaderKey, Left(AlreadyExist()))\n\n  // Watcher\n  val watcherName = s\"memory-queue-$fqn-$revision\"\n  val watcherNameForNamespace = s\"container-counter-$testInvocationNamespace\"\n  val testCreationId = CreationId.generate()\n\n  // ack\n  var ackedMessageCount = 0\n  var lastAckedActivationResult = dummyWhiskActivation\n  val ack = new ActiveAck {\n    override def apply(tid: TransactionId,\n                       activationResult: WhiskActivation,\n                       blockingInvoke: Boolean,\n                       controllerInstance: ControllerInstanceId,\n                       userId: UUID,\n                       acknowledgement: AcknowledgementMessage): Future[Any] = {\n      ackedMessageCount += 1\n      lastAckedActivationResult = activationResult\n      Future.successful({})\n    }\n  }\n\n  // store activation\n  var storedMessageCount = 0\n  var lastStoredActivationResult = dummyWhiskActivation\n  val store: (TransactionId, WhiskActivation, UserContext) => Future[Any] =\n    (tid: TransactionId, activationResult: WhiskActivation, contest: UserContext) => {\n      storedMessageCount += 1\n      lastStoredActivationResult = activationResult\n      Future.successful(())\n    }\n\n  def mockMessaging(receiver: Option[ActorRef] = None): MessageProducer = {\n    val producer = receiver.map(fakeProducer(_)).getOrElse(stub[MessageProducer])\n    producer\n  }\n\n  private def fakeProducer(receiver: ActorRef) = new MessageProducer {\n\n    /** Count of messages sent. */\n    override def sentCount(): Long = 0\n\n    /** Sends msg to topic. This is an asynchronous operation. */\n    override def send(topic: String, msg: Message, retry: Int): Future[ResultMetadata] = {\n      receiver ! s\"$topic-${msg}\"\n\n      Future.successful(ResultMetadata(topic, 0, -1))\n    }\n\n    /** Closes producer. */\n    override def close(): Unit = {}\n  }\n\n  def getUserLimit(invocationNamespace: String): Future[Int] = {\n    Future.successful(2)\n  }\n\n  def expectDurationChecking(mockEsClient: ElasticClient, namespace: String) = {\n    val index = generateIndex(namespace)\n\n    val searchRequest = (search(index) query {\n      boolQuery must {\n        List(\n          matchQuery(\"path.keyword\", fqn.copy(version = None).toString),\n          rangeQuery(\"@timestamp\").gte(getFromDate(durationCheckWindow)))\n      }\n    } aggregations\n      avgAgg(AverageAggregationName, \"duration\")).size(0)\n\n    (mockEsClient\n      .execute[SearchRequest, SearchResponse, Future](_: SearchRequest)(\n        _: Functor[Future],\n        _: http.Executor[Future],\n        _: Handler[SearchRequest, SearchResponse],\n        _: Manifest[SearchResponse]))\n      .expects(searchRequest, *, *, *, *)\n      .returns(\n        Future.successful(RequestSuccess(\n          200,\n          None,\n          Map.empty,\n          SearchResponse(1, false, false, Map.empty, Shards(0, 0, 0), None, Map.empty, SearchHits(0, 0, Array.empty)))))\n      .once()\n  }\n\n  def expectInitialData(watcher: TestProbe, dataMgmtService: TestProbe) = {\n    watcher.expectMsgAllOf(\n      WatchEndpoint(leaderKey, endpoints.serialize, isPrefix = false, watcherName, Set(DeleteEvent)),\n      WatchEndpoint(inProgressContainerKey, \"\", isPrefix = true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(existingContainerKey, \"\", isPrefix = true, watcherName, Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        inProgressContainerPrefixKeyByNamespace,\n        \"\",\n        isPrefix = true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)),\n      WatchEndpoint(\n        existingContainerPrefixKeyByNamespace,\n        \"\",\n        isPrefix = true,\n        watcherNameForNamespace,\n        Set(PutEvent, DeleteEvent)))\n\n    dataMgmtService.expectMsg(RegisterInitialData(namespaceThrottlingKey, false.toString, failoverEnabled = false))\n    dataMgmtService.expectMsg(RegisterData(actionThrottlingKey, false.toString, failoverEnabled = false))\n  }\n\n  def expectDataCleanUp(watcher: TestProbe, dataMgmtService: TestProbe) = {\n    dataMgmtService.expectMsgAllOf(\n      UnregisterData(leaderKey),\n      UnregisterData(namespaceThrottlingKey),\n      UnregisterData(actionThrottlingKey))\n\n    watcher.expectMsgAllOf(\n      UnwatchEndpoint(inProgressContainerKey, isPrefix = true, watcherName),\n      UnwatchEndpoint(existingContainerKey, isPrefix = true, watcherName),\n      UnwatchEndpoint(leaderKey, isPrefix = false, watcherName))\n  }\n\n  def getActivationMessages(count: Int): List[ActivationMessage] = {\n    val result = 1 to count map { _ =>\n      ActivationMessage(\n        messageTransId,\n        action.fullyQualifiedName(true),\n        action.rev,\n        Identity(\n          Subject(),\n          Namespace(EntityName(testInvocationNamespace), uuid),\n          BasicAuthenticationAuthKey(uuid, Secret()),\n          Set.empty),\n        ActivationId.generate(),\n        ControllerInstanceId(\"0\"),\n        blocking = false,\n        content = None)\n    }\n    result.toList\n  }\n\n  def getActivation(alive: Boolean = true, containerId: String = \"testContainerId\") =\n    GetActivation(TransactionId(\"tid\"), fqn, containerId, warmed = false, None, alive)\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/queue/test/QueueManagerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue.test\n\nimport java.time.{Clock, Instant}\nimport java.util.concurrent.atomic.AtomicInteger\nimport org.apache.pekko.actor.{Actor, ActorIdentity, ActorRef, ActorRefFactory, ActorSystem, Identify, Props}\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.testkit.{ImplicitSender, TestActor, TestActorRef, TestKit, TestProbe}\nimport org.apache.pekko.util.Timeout\nimport com.ibm.etcd.api.{KeyValue, RangeResponse}\nimport common.{LoggedFunction, StreamLogging}\nimport org.apache.openwhisk.common.{GracefulShutdown, TransactionId}\nimport org.apache.openwhisk.core.WarmUp.warmUpAction\nimport org.apache.openwhisk.core.ack.ActiveAck\nimport org.apache.openwhisk.core.connector.test.TestConnector\nimport org.apache.openwhisk.core.connector.{AcknowledgementMessage, ActivationMessage, GetState, StatusData}\nimport org.apache.openwhisk.core.database.{ArtifactStore, DocumentRevisionMismatchException, UserContext}\nimport org.apache.openwhisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.etcd.EtcdKV.QueueKeys\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdFollower, EtcdLeader}\nimport org.apache.openwhisk.core.etcd.EtcdType._\nimport org.apache.openwhisk.core.scheduler.grpc.test.CommonVariable\nimport org.apache.openwhisk.core.scheduler.grpc.{ActivationResponse, GetActivation}\nimport org.apache.openwhisk.core.scheduler.queue._\nimport org.apache.openwhisk.core.scheduler.{SchedulerEndpoints, SchedulerStates}\nimport org.apache.openwhisk.core.service._\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration._\nimport scala.language.postfixOps\n\n@RunWith(classOf[JUnitRunner])\nclass QueueManagerTests\n    extends TestKit(ActorSystem(\"QueueManager\"))\n    with CommonVariable\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with BeforeAndAfterEach\n    with StreamLogging {\n\n  override def afterAll: Unit = {\n    QueuePool.clear()\n    TestKit.shutdownActorSystem(system)\n  }\n  override def beforeEach = QueuePool.clear()\n  implicit val askTimeout = Timeout(5 seconds)\n  implicit val ec = system.dispatcher\n\n  val entityStore = WhiskEntityStore.datastore()\n\n  val schedulerId = SchedulerInstanceId(\"0\")\n  val testQueueCreationMessage =\n    CreateQueue(testInvocationNamespace, testFQN, testDocRevision, testActionMetaData)\n\n  val schedulerEndpoint = SchedulerEndpoints(\"127.0.0.1\", 8080, 2552)\n  val mockConsumer = new TestConnector(s\"scheduler${schedulerId.asString}\", 4, true)\n\n  val messageTransId = TransactionId(TransactionId.testing.meta.id)\n  val uuid = UUID()\n\n  val action = ExecutableWhiskAction(testEntityPath, testEntityName, testExec)\n  val testLeaderKey = QueueKeys.queue(testInvocationNamespace, action.fullyQualifiedName(false), true)\n\n  val activationMessage = ActivationMessage(\n    messageTransId,\n    action.fullyQualifiedName(true),\n    testDocRevision,\n    Identity(\n      Subject(),\n      Namespace(EntityName(testInvocationNamespace), uuid),\n      BasicAuthenticationAuthKey(uuid, Secret()),\n      Set.empty),\n    ActivationId.generate(),\n    ControllerInstanceId(\"0\"),\n    blocking = false,\n    content = None)\n  val statusData =\n    StatusData(testInvocationNamespace, testFQN.asString, List.empty[ActivationId], \"Running\", \"RunningData\")\n\n  // update start time for activation to ensure it's not stale\n  def newActivation(start: Instant = Instant.now()): ActivationMessage = {\n    activationMessage.copy(transid = TransactionId(messageTransId.meta.copy(start = start)))\n  }\n\n  val activationResponse = ActivationResponse(Right(activationMessage))\n\n  val ack = new ActiveAck {\n    override def apply(tid: TransactionId,\n                       activationResult: WhiskActivation,\n                       blockingInvoke: Boolean,\n                       controllerInstance: ControllerInstanceId,\n                       userId: UUID,\n                       acknowledgement: AcknowledgementMessage): Future[Any] = {\n      Future.successful({})\n\n    }\n  }\n\n  val store: (TransactionId, WhiskActivation, UserContext) => Future[Any] =\n    (tid: TransactionId, activationResult: WhiskActivation, contest: UserContext) => Future.successful(())\n\n  val testLeaseId = 60\n\n  val childFactory =\n    (system: ActorRefFactory, _: String, _: FullyQualifiedEntityName, _: DocRevision, _: WhiskActionMetaData) =>\n      system.actorOf(Props(new Actor() {\n        override def receive: Receive = {\n          case GetActivation(_, _, _, _, _, _) =>\n            sender ! ActivationResponse(Right(newActivation()))\n          case GetState =>\n            sender ! statusData\n        }\n      }))\n\n  def convertToMetaData(action: WhiskAction): WhiskActionMetaData = {\n    val exec = CodeExecMetaDataAsString(RuntimeManifest(action.exec.kind, ImageName(\"test\")), entryPoint = Some(\"test\"))\n    WhiskActionMetaData(\n      action.namespace,\n      action.name,\n      exec,\n      action.parameters,\n      action.limits,\n      action.version,\n      action.publish,\n      action.annotations)\n      .revision[WhiskActionMetaData](action.rev)\n  }\n\n  /**get WhiskActionMetaData*/\n  def getWhiskActionMetaData(meta: Future[WhiskActionMetaData]) = LoggedFunction {\n    (_: ArtifactStore[WhiskEntity], _: DocId, _: DocRevision, _: Boolean, _: Boolean) =>\n      meta\n  }\n\n  val get = getWhiskActionMetaData(Future(convertToMetaData(action.toWhiskAction.revision(testDocRevision))))\n  val failedGet = getWhiskActionMetaData(Future.failed(new Exception(\"error\")))\n\n  val watchEndpoint =\n    WatchEndpoint(QueueKeys.queuePrefix, \"\", isPrefix = true, \"queue-manager\", Set(PutEvent, DeleteEvent))\n\n  behavior of \"QueueManager\"\n\n  it should \"get the remote actor ref and send the message\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    val testQueueManagerActorName = \"QueueManagerActorSelectionTest\"\n    val watcher = TestProbe()\n\n    system.actorOf(\n      QueueManager\n        .props(\n          entityStore,\n          get,\n          mockEtcdClient,\n          schedulerEndpoint,\n          schedulerId,\n          dataManagementService.ref,\n          watcher.ref,\n          ack,\n          store,\n          childFactory,\n          mockConsumer),\n      testQueueManagerActorName)\n\n    watcher.expectMsg(watchEndpoint)\n    val testQueueManagerPath = s\"pekko://QueueManager/user/${testQueueManagerActorName}\"\n\n    val selected = system.actorSelection(testQueueManagerPath)\n\n    val ActorIdentity(_, Some(ref)) = (selected ? Identify(testQueueManagerPath)).mapTo[ActorIdentity].futureValue\n\n    (ref ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n  }\n\n  it should \"create a queue in response to a queue creation request\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    val watcher = TestProbe()\n\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n  }\n\n  it should \"response queue creation request when failed to do election\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = getTestDataManagementService(false)\n    val watcher = TestProbe()\n\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n  }\n\n  it should \"not create a queue if there is already a queue for the given fqn\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    val watcher = TestProbe()\n\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n    dataManagementService.expectMsg(ElectLeader(testLeaderKey, schedulerEndpoint.serialize, queueManager))\n\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n  }\n\n  it should \"only do leader election for one time if there are more than one create queue requests incoming\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    dataManagementService.ignoreMsg {\n      case _: UpdateDataOnChange => true\n    }\n    val watcher = TestProbe()\n\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n\n    val probe = TestProbe()\n    queueManager.tell(testQueueCreationMessage, probe.ref)\n    queueManager.tell(testQueueCreationMessage, probe.ref)\n    queueManager.tell(testQueueCreationMessage, probe.ref)\n    queueManager.tell(testQueueCreationMessage, probe.ref)\n\n    // dataManagementService should only do one election\n    dataManagementService.expectMsg(ElectLeader(testLeaderKey, schedulerEndpoint.serialize, queueManager))\n    dataManagementService.expectNoMessage()\n\n    // all four requests should get responses\n    probe.expectMsg(CreateQueueResponse(testInvocationNamespace, testFQN, true))\n    probe.expectMsg(CreateQueueResponse(testInvocationNamespace, testFQN, true))\n    probe.expectMsg(CreateQueueResponse(testInvocationNamespace, testFQN, true))\n    probe.expectMsg(CreateQueueResponse(testInvocationNamespace, testFQN, true))\n  }\n\n  private def getTestDataManagementService(success: Boolean = true) = {\n    val dataManagementService = TestProbe()\n    dataManagementService.setAutoPilot((sender: ActorRef, msg: Any) =>\n      msg match {\n        case ElectLeader(key, value, _, _) =>\n          if (success) {\n            sender ! ElectionResult(Right(EtcdLeader(key, value, 10)))\n          } else {\n            sender ! ElectionResult(Left(EtcdFollower(key, value)))\n          }\n          TestActor.KeepRunning\n\n        case _ =>\n          TestActor.KeepRunning\n    })\n    dataManagementService\n  }\n\n  it should \"forward msg to remote queue when queue exist on remote\" in {\n    stream.reset()\n    val leaderKey = QueueKeys.queue(\n      activationMessage.user.namespace.name.asString,\n      activationMessage.action.copy(version = None),\n      true)\n    val mockEtcdClient = mock[EtcdClient]\n    (mockEtcdClient\n      .get(_: String))\n      .expects(*)\n      .returning(\n        Future.successful(\n          RangeResponse\n            .newBuilder()\n            .addKvs(KeyValue.newBuilder().setKey(leaderKey).setValue(schedulerEndpoint.serialize).build())\n            .build()))\n      .once()\n    val dataManagementService = getTestDataManagementService()\n    val watcher = TestProbe()\n\n    val probe = TestProbe()\n\n    val childFactory =\n      (_: ActorRefFactory, _: String, _: FullyQualifiedEntityName, _: DocRevision, _: WhiskActionMetaData) => probe.ref\n\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n    watcher.expectMsg(watchEndpoint)\n\n    // got a message but no queue created on this scheduler\n    // it should try to got leader key from etcd and forward this msg to remote queue, here is `schedulerEndpoints`\n    queueManager ! newActivation()\n    stream.toString should include(s\"send activation to remote queue, key: $leaderKey\")\n    stream.toString should include(s\"add a new actor selection to a map with key: $leaderKey\")\n    stream.reset()\n\n    // got msg again, and it should get remote queue from memory instead of etcd\n    val msg2 = newActivation().copy(activationId = ActivationId.generate())\n    queueManager ! msg2\n    stream.toString shouldNot include(s\"send activation to remote queue, key: $leaderKey\")\n  }\n\n  it should \"create a new MemoryQueue when the revision matches with the one in a datastore\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    val watcher = TestProbe()\n\n    val probe = TestProbe()\n\n    val childFactory =\n      (_: ActorRefFactory, _: String, _: FullyQualifiedEntityName, _: DocRevision, _: WhiskActionMetaData) => probe.ref\n\n    val newRevision = DocRevision(\"2-test-revision\")\n    val newFqn = FullyQualifiedEntityName(EntityPath(testNamespace), EntityName(testAction), Some(SemVer(0, 0, 2)))\n    val newGet = getWhiskActionMetaData(\n      Future(convertToMetaData(action.copy(version = SemVer(0, 0, 2)).toWhiskAction.revision(newRevision))))\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            newGet,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    //current queue's revision is `1-test-revision`\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    probe.expectMsg(Start)\n\n    //the activationMessage's revision(2-test-revision) is newer than current queue's revision(1-test-revision)\n    val activationMessage = ActivationMessage(\n      messageTransId,\n      newFqn,\n      newRevision,\n      Identity(\n        Subject(),\n        Namespace(EntityName(testInvocationNamespace), uuid),\n        BasicAuthenticationAuthKey(uuid, Secret()),\n        Set.empty),\n      ActivationId.generate(),\n      ControllerInstanceId(\"0\"),\n      blocking = false,\n      content = None)\n\n    queueManager ! activationMessage\n    val msgs = (0 to 10).map(i => {\n      activationMessage.copy(activationId = ActivationId.generate())\n    })\n    msgs.foreach(msg => queueManager ! msg) // even send multiple requests, we should only create new queue for once\n    probe.expectMsg(StopSchedulingAsOutdated)\n    probe.expectMsg(VersionUpdated)\n    probe.expectMsg(activationMessage)\n    msgs.foreach(msg => probe.expectMsg(msg))\n  }\n\n  it should \"create a new MemoryQueue correctly when the action is updated again during updating the queue\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    val watcher = TestProbe()\n\n    val probe = TestProbe()\n    val queueWatcher = TestProbe()\n\n    val childFactory =\n      (_: ActorRefFactory,\n       _: String,\n       fqn: FullyQualifiedEntityName,\n       revision: DocRevision,\n       metadata: WhiskActionMetaData) => {\n        queueWatcher.ref ! (fqn, revision)\n        probe.ref\n      }\n\n    val newRevision = DocRevision(\"2-test-revision\")\n    val newFqn = FullyQualifiedEntityName(EntityPath(testNamespace), EntityName(testAction), Some(SemVer(0, 0, 2)))\n    val finalFqn = newFqn.copy(version = Some(SemVer(0, 0, 3)))\n    val finalRevision = DocRevision(\"3-test-revision\")\n    // simulate the case that action is updated again while fetch it from database\n    def newGet(store: ArtifactStore[WhiskEntity],\n               docId: DocId,\n               docRevision: DocRevision,\n               fromCache: Boolean,\n               ignoreMissingAttachment: Boolean) = {\n      if (docRevision == DocRevision.empty) {\n        Future(convertToMetaData(action.copy(version = SemVer(0, 0, 3)).toWhiskAction.revision(finalRevision)))\n      } else\n        Future.failed(DocumentRevisionMismatchException(\"mismatch\"))\n    }\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            newGet,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    //current queue's revision is `1-test-revision`\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    queueWatcher.expectMsg((testFQN, testDocRevision))\n    probe.expectMsg(Start)\n\n    //the activationMessage's revision(2-test-revision) is newer than current queue's revision(1-test-revision)\n    val activationMessage = ActivationMessage(\n      messageTransId,\n      newFqn,\n      newRevision,\n      Identity(\n        Subject(),\n        Namespace(EntityName(testInvocationNamespace), uuid),\n        BasicAuthenticationAuthKey(uuid, Secret()),\n        Set.empty),\n      ActivationId.generate(),\n      ControllerInstanceId(\"0\"),\n      blocking = false,\n      content = None)\n\n    queueManager ! activationMessage\n    probe.expectMsg(StopSchedulingAsOutdated)\n    queueWatcher.expectMsg((finalFqn, finalRevision))\n    probe.expectMsg(VersionUpdated)\n    probe.expectMsg(activationMessage.copy(action = finalFqn, revision = finalRevision))\n  }\n\n  it should \"recreate the queue if it's removed by mistake while leader key is not removed from etcd\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    (mockEtcdClient\n      .get(_: String))\n      .expects(*)\n      .returning(Future.successful {\n        RangeResponse\n          .newBuilder()\n          .addKvs(KeyValue.newBuilder().setKey(\"test\").setValue(schedulerEndpoint.serialize).build())\n          .build()\n      })\n      .anyNumberOfTimes()\n    val dataManagementService = getTestDataManagementService()\n    val watcher = TestProbe()\n\n    val probe = TestProbe()\n\n    val childFactory =\n      (_: ActorRefFactory, _: String, _: FullyQualifiedEntityName, _: DocRevision, _: WhiskActionMetaData) => probe.ref\n\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    //current queue's revision is `1-test-revision`\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    probe.expectMsg(Start)\n\n    // simulate queue superseded, the queue will be removed but leader key won't be deleted\n    queueManager ! QueueRemoved(\n      testInvocationNamespace,\n      testFQN.toDocId.asDocInfo(testDocRevision),\n      Some(testLeaderKey))\n\n    queueManager.!(activationMessage)(queueManager)\n    val msg2 = activationMessage.copy(activationId = ActivationId.generate())\n    queueManager.!(msg2)(queueManager) // even send two requests, we should only recreate one queue\n    probe.expectMsg(Start)\n    probe.expectMsg(activationMessage)\n    probe.expectMsg(msg2)\n  }\n\n  it should \"not skip outdated activation when the revision is older than the one in a datastore\" in {\n    stream.reset()\n    val mockEtcdClient = mock[EtcdClient]\n    (mockEtcdClient\n      .get(_: String))\n      .expects(*)\n      .returning(\n        Future.successful(\n          RangeResponse\n            .newBuilder()\n            .addKvs(KeyValue.newBuilder().setKey(\"test\").setValue(schedulerEndpoint.serialize).build())\n            .build()))\n    val dataManagementService = getTestDataManagementService()\n    val watcher = TestProbe()\n\n    val probe = TestProbe()\n\n    val childFactory =\n      (_: ActorRefFactory, _: String, _: FullyQualifiedEntityName, _: DocRevision, _: WhiskActionMetaData) => probe.ref\n\n    val newRevision = DocRevision(\"2-test-revision\")\n    val get = getWhiskActionMetaData(Future(convertToMetaData(action.toWhiskAction.revision(newRevision))))\n\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    //current queue's revision is `2-test-revision`\n    val testQueueCreationMessage =\n      CreateQueue(testInvocationNamespace, testFQN, revision = newRevision, testActionMetaData)\n\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    //the activationMessage's revision(1-test-revision) is older than current queue's revision(2-test-revision)\n    queueManager ! newActivation()\n\n    stream.toString should include(s\"it will be replaced with the latest revision and invoked\")\n  }\n\n  it should \"retry to fetch queue data if etcd does not respond\" in {\n    val mockEtcdClient = stub[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    dataManagementService.ignoreMsg {\n      case _: UpdateDataOnChange => true\n    }\n    val watcher = TestProbe()\n\n    (mockEtcdClient.get _) when (*) returns (Future.failed(new Exception(\"failed to get for some reason\")))\n\n    val queueManager =\n      TestActorRef(\n        new QueueManager(\n          entityStore,\n          get,\n          mockEtcdClient,\n          schedulerEndpoint,\n          schedulerId,\n          dataManagementService.ref,\n          watcher.ref,\n          ack,\n          store,\n          childFactory,\n          mockConsumer,\n          QueueManagerConfig(maxRetriesToGetQueue = 2, maxSchedulingTime = 10 seconds)))\n\n    queueManager ! newActivation()\n    Thread.sleep(100)\n    (mockEtcdClient.get _) verify (*) repeated (3)\n  }\n\n  it should \"retry to fetch queue data if there is no data in the response\" in {\n    val mockEtcdClient = stub[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    dataManagementService.ignoreMsg {\n      case _: UpdateDataOnChange => true\n    }\n    val watcher = TestProbe()\n\n    val emptyResult = Future.successful(RangeResponse.newBuilder().build())\n    (mockEtcdClient.get _) when (*) returns (emptyResult)\n\n    val queueManager =\n      TestActorRef(\n        new QueueManager(\n          entityStore,\n          get,\n          mockEtcdClient,\n          schedulerEndpoint,\n          schedulerId,\n          dataManagementService.ref,\n          watcher.ref,\n          ack,\n          store,\n          childFactory,\n          mockConsumer,\n          QueueManagerConfig(maxRetriesToGetQueue = 2, maxSchedulingTime = 10 seconds)))\n\n    queueManager ! newActivation()\n    Thread.sleep(100)\n    (mockEtcdClient.get _) verify (*) repeated (3)\n  }\n\n  it should \"save queue endpoint in memory\" in {\n    stream.reset()\n\n    val mockEtcdClient = stub[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    dataManagementService.ignoreMsg {\n      case _: UpdateDataOnChange => true\n    }\n    val watcher = TestProbe()\n\n    val emptyResult = Future.successful(RangeResponse.newBuilder().build())\n    (mockEtcdClient.get _) when (*) returns (emptyResult)\n\n    val queueManager =\n      TestActorRef(\n        new QueueManager(\n          entityStore,\n          get,\n          mockEtcdClient,\n          schedulerEndpoint,\n          schedulerId,\n          dataManagementService.ref,\n          watcher.ref,\n          ack,\n          store,\n          childFactory,\n          mockConsumer,\n          QueueManagerConfig(maxRetriesToGetQueue = 2, maxSchedulingTime = 10 seconds)))\n\n    queueManager ! WatchEndpointInserted(\"queue\", \"queue/test-action/leader\", schedulerEndpoint.serialize, true)\n    stream.toString should include(s\"Endpoint inserted, key: queue/test-action/leader, endpoints: ${schedulerEndpoint}\")\n    stream.reset()\n\n    queueManager ! WatchEndpointInserted(\"queue\", \"queue/test-action/leader\", \"host with wrong format\", true)\n    stream.toString should include(s\"Unexpected error\")\n    stream.toString should include(s\"when put leaderKey: queue/test-action/leader\")\n    stream.reset()\n\n    queueManager ! WatchEndpointRemoved(\"queue\", \"queue/test-action/leader\", schedulerEndpoint.serialize, true)\n    stream.toString should include(s\"Endpoint removed for key: queue/test-action/leader\")\n  }\n\n  it should \"able to query queue status\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val watcher = TestProbe()\n    val dataManagementService = getTestDataManagementService()\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    (queueManager ? QueueSize).mapTo[Int].futureValue shouldBe 1\n\n    (queueManager ? GetState).mapTo[Future[List[StatusData]]].flatten.futureValue shouldBe List(statusData)\n  }\n\n  it should \"drop the activation message that has not been scheduled for a long time\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val watcher = TestProbe()\n    val probe = TestProbe()\n    val dataManagementService = getTestDataManagementService()\n\n    val ack = new ActiveAck {\n      override def apply(tid: TransactionId,\n                         activationResult: WhiskActivation,\n                         blockingInvoke: Boolean,\n                         controllerInstance: ControllerInstanceId,\n                         userId: UUID,\n                         acknowledgement: AcknowledgementMessage): Future[Any] = {\n        Future.successful(probe.ref ! acknowledgement.isSystemError)\n      }\n    }\n\n    val oldNow = Instant.now(Clock.systemUTC()).minusMillis(11000)\n    val oldActivationMessage = newActivation(oldNow)\n\n    val queueManager =\n      TestActorRef(\n        new QueueManager(\n          entityStore,\n          failedGet,\n          mockEtcdClient,\n          schedulerEndpoint,\n          schedulerId,\n          dataManagementService.ref,\n          watcher.ref,\n          ack,\n          store,\n          childFactory,\n          mockConsumer,\n          QueueManagerConfig(maxRetriesToGetQueue = 2, maxSchedulingTime = 10 seconds)))\n\n    // send old activation message\n    queueManager ! oldActivationMessage\n\n    // response should be whisk internal error\n    probe.expectMsg(Some(true))\n\n    stream.toString should include(s\"[${activationMessage.activationId}] the activation message has not been scheduled\")\n  }\n\n  it should \"not drop the unscheduled activation message that has been processed within the scheduling time limit.\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    (mockEtcdClient\n      .get(_: String))\n      .expects(*)\n      .returning(\n        Future.successful(\n          RangeResponse\n            .newBuilder()\n            .addKvs(KeyValue.newBuilder().setKey(\"test\").setValue(schedulerEndpoint.serialize).build())\n            .build()))\n    val watcher = TestProbe()\n    val probe = TestProbe()\n    val dataManagementService = getTestDataManagementService()\n\n    val ack = new ActiveAck {\n      override def apply(tid: TransactionId,\n                         activationResult: WhiskActivation,\n                         blockingInvoke: Boolean,\n                         controllerInstance: ControllerInstanceId,\n                         userId: UUID,\n                         acknowledgement: AcknowledgementMessage): Future[Any] = {\n        Future.successful(probe.ref ! activationResult.activationId)\n      }\n    }\n\n    val oldNow = Instant.now(Clock.systemUTC()).minusMillis(9000)\n    val oldActivationMessage = newActivation(oldNow)\n\n    val queueManager =\n      TestActorRef(\n        new QueueManager(\n          entityStore,\n          failedGet,\n          mockEtcdClient,\n          schedulerEndpoint,\n          schedulerId,\n          dataManagementService.ref,\n          watcher.ref,\n          ack,\n          store,\n          childFactory,\n          mockConsumer,\n          QueueManagerConfig(maxRetriesToGetQueue = 2, maxSchedulingTime = 10 seconds)))\n\n    // send old activation message\n    queueManager ! oldActivationMessage\n\n    // ack is no expected\n    probe.expectNoMessage(500.milliseconds)\n  }\n\n  it should \"complete the error activation when the version of action is changed but fetch is failed\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val watcher = TestProbe()\n\n    val probe = TestProbe()\n    val consumer = TestProbe()\n    val dataManagementService = getTestDataManagementService()\n    val ack = new ActiveAck {\n      override def apply(tid: TransactionId,\n                         activationResult: WhiskActivation,\n                         blockingInvoke: Boolean,\n                         controllerInstance: ControllerInstanceId,\n                         userId: UUID,\n                         acknowledgement: AcknowledgementMessage): Future[Any] = {\n        Future.successful(probe.ref ! activationResult.activationId)\n      }\n    }\n    val store: (TransactionId, WhiskActivation, UserContext) => Future[Any] =\n      (_: TransactionId, activation: WhiskActivation, _: UserContext) =>\n        Future.successful(probe.ref ! activation.activationId)\n\n    val newFqn = FullyQualifiedEntityName(EntityPath(testNamespace), EntityName(testAction), Some(SemVer(0, 0, 2)))\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            failedGet,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    queueManager.tell(\n      UpdateMemoryQueue(testFQN.toDocId.asDocInfo(testDocRevision), newFqn, newActivation()),\n      consumer.ref)\n\n    probe.expectMsg(activationMessage.activationId)\n    probe.expectMsg(activationMessage.activationId)\n  }\n\n  it should \"remove the queue and consumer if it receives a QueueRemoved message\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val watcher = TestProbe()\n    val dataManagementService = getTestDataManagementService()\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    queueManager ! QueueRemoved(\n      testInvocationNamespace,\n      testFQN.toDocId.asDocInfo(testDocRevision),\n      Some(testLeaderKey))\n\n    (queueManager ? QueueSize).mapTo[Int].futureValue shouldBe 0\n  }\n\n  it should \"put the queue back to pool if it receives a QueueReactive message\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val watcher = TestProbe()\n    val dataManagementService = getTestDataManagementService()\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    (queueManager ? QueueSize).mapTo[Int].futureValue shouldBe 1\n\n    queueManager ! QueueRemoved(\n      testInvocationNamespace,\n      testFQN.toDocId.asDocInfo(testDocRevision),\n      Some(testLeaderKey))\n\n    (queueManager ? QueueSize).mapTo[Int].futureValue shouldBe 0\n\n    queueManager ! QueueReactivated(testInvocationNamespace, testFQN, testFQN.toDocId.asDocInfo(testDocRevision))\n\n    (queueManager ? QueueSize).mapTo[Int].futureValue shouldBe 1\n  }\n\n  it should \"put pool information to data management service\" in {\n    val mockEtcdClient = mock[EtcdClient]\n\n    val watcher = TestProbe()\n    val dataManagementService = TestProbe()\n    val counter1 = new AtomicInteger(0)\n    val counter2 = new AtomicInteger(0)\n    val counter3 = new AtomicInteger(0)\n\n    dataManagementService.setAutoPilot((sender: ActorRef, msg: Any) =>\n      msg match {\n        case ElectLeader(key, value, _, _) =>\n          sender ! ElectionResult(Right(EtcdLeader(key, value, 10)))\n          TestActor.KeepRunning\n\n        case UpdateDataOnChange(_, value) if value == SchedulerStates(schedulerId, 1, schedulerEndpoint).serialize =>\n          counter1.getAndIncrement()\n          TestActor.KeepRunning\n\n        case UpdateDataOnChange(_, value) if value == SchedulerStates(schedulerId, 2, schedulerEndpoint).serialize =>\n          counter2.getAndIncrement()\n          TestActor.KeepRunning\n\n        case UpdateDataOnChange(_, value) if value == SchedulerStates(schedulerId, 3, schedulerEndpoint).serialize =>\n          counter3.getAndIncrement()\n          TestActor.KeepRunning\n\n        case _ =>\n          TestActor.KeepRunning\n    })\n\n    val fqn2 = FullyQualifiedEntityName(EntityPath(\"hello1\"), EntityName(\"action1\"))\n    val fqn3 = FullyQualifiedEntityName(EntityPath(\"hello2\"), EntityName(\"action2\"))\n\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    Thread.sleep(2000)\n\n    (queueManager ? testQueueCreationMessage.copy(fqn = fqn2))\n      .mapTo[CreateQueueResponse]\n      .futureValue shouldBe CreateQueueResponse(testInvocationNamespace, fqn = fqn2, success = true)\n\n    Thread.sleep(2000)\n\n    (queueManager ? testQueueCreationMessage.copy(fqn = fqn3))\n      .mapTo[CreateQueueResponse]\n      .futureValue shouldBe CreateQueueResponse(testInvocationNamespace, fqn = fqn3, success = true)\n\n    Thread.sleep(2000)\n\n    counter1.get() should be > 0\n    counter2.get() should be > 0\n    counter3.get() should be > 0\n  }\n\n  it should \"not create a queue if it is a warm-up action\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val dataManagementService = getTestDataManagementService()\n    val watcher = TestProbe()\n\n    val warmUpActionMetaData =\n      WhiskActionMetaData(warmUpAction.namespace.toPath, warmUpAction.name, testExecMetadata, version = semVer)\n\n    val warmUpQueueCreationMessage =\n      CreateQueue(warmUpAction.namespace.toString, warmUpAction, testDocRevision, warmUpActionMetaData)\n\n    val queueManager =\n      TestActorRef(\n        QueueManager\n          .props(\n            entityStore,\n            get,\n            mockEtcdClient,\n            schedulerEndpoint,\n            schedulerId,\n            dataManagementService.ref,\n            watcher.ref,\n            ack,\n            store,\n            childFactory,\n            mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n\n    (queueManager ? warmUpQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      warmUpAction.namespace.toString,\n      warmUpAction,\n      true)\n  }\n\n  behavior of \"zero downtime deployment\"\n\n  it should \"stop all memory queues and corresponding consumers when it receives graceful shutdown message\" in {\n    val mockEtcdClient = mock[EtcdClient]\n    val watcher = TestProbe()\n    val dataManagementService = getTestDataManagementService()\n    val probe = TestProbe()\n    val fqn2 = FullyQualifiedEntityName(EntityPath(\"hello1\"), EntityName(\"action1\"))\n    val fqn3 = FullyQualifiedEntityName(EntityPath(\"hello2\"), EntityName(\"action2\"))\n    val fqn4 = FullyQualifiedEntityName(EntityPath(\"hello3\"), EntityName(\"action3\"))\n    val fqn5 = FullyQualifiedEntityName(EntityPath(\"hello4\"), EntityName(\"action4\"))\n    val fqn6 = FullyQualifiedEntityName(EntityPath(\"hello5\"), EntityName(\"action5\"))\n\n    // probe will watch all actors which are created by these factories\n    val childFactory =\n      (system: ActorRefFactory, _: String, _: FullyQualifiedEntityName, _: DocRevision, _: WhiskActionMetaData) => {\n        system.actorOf(Props(new Actor() {\n          override def receive: Receive = {\n            case GetActivation(_, _, _, _, _, _) =>\n              sender ! ActivationResponse(Right(newActivation()))\n\n            case GracefulShutdown =>\n              probe.ref ! GracefulShutdown\n          }\n        }))\n      }\n\n    val queueManager =\n      TestActorRef(\n        QueueManager.props(\n          entityStore,\n          get,\n          mockEtcdClient,\n          schedulerEndpoint,\n          schedulerId,\n          dataManagementService.ref,\n          watcher.ref,\n          ack,\n          store,\n          childFactory,\n          mockConsumer))\n\n    watcher.expectMsg(watchEndpoint)\n    (queueManager ? testQueueCreationMessage).mapTo[CreateQueueResponse].futureValue shouldBe CreateQueueResponse(\n      testInvocationNamespace,\n      testFQN,\n      true)\n\n    (queueManager ? testQueueCreationMessage.copy(fqn = fqn2))\n      .mapTo[CreateQueueResponse]\n      .futureValue shouldBe CreateQueueResponse(testInvocationNamespace, fqn = fqn2, success = true)\n\n    (queueManager ? testQueueCreationMessage.copy(fqn = fqn3))\n      .mapTo[CreateQueueResponse]\n      .futureValue shouldBe CreateQueueResponse(testInvocationNamespace, fqn = fqn3, success = true)\n\n    queueManager ! GracefulShutdown\n\n    probe.expectMsgAllOf(10.seconds, GracefulShutdown, GracefulShutdown, GracefulShutdown)\n\n    // after shutdown, it can still create/update/recover a queue, and new queue should be shutdown immediately too\n    (queueManager ? testQueueCreationMessage.copy(fqn = fqn4))\n      .mapTo[CreateQueueResponse]\n      .futureValue shouldBe CreateQueueResponse(testInvocationNamespace, fqn = fqn4, success = true)\n    queueManager ! CreateNewQueue(activationMessage, fqn5, testActionMetaData)\n    queueManager ! RecoverQueue(activationMessage, fqn6, testActionMetaData)\n\n    probe.expectMsgAllOf(10.seconds, GracefulShutdown, GracefulShutdown, GracefulShutdown)\n\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/scheduler/queue/test/SchedulingDecisionMakerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.scheduler.queue.test\n\nimport java.util.concurrent.atomic.AtomicInteger\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.{TestKit, TestProbe}\nimport common.StreamLogging\nimport org.apache.openwhisk.core.entity.{EntityName, EntityPath, FullyQualifiedEntityName, SemVer}\nimport org.apache.openwhisk.core.scheduler.SchedulingConfig\nimport org.apache.openwhisk.core.scheduler.queue._\nimport org.junit.runner.RunWith\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.concurrent.duration.DurationInt\n\n@RunWith(classOf[JUnitRunner])\nclass SchedulingDecisionMakerTests\n    extends TestKit(ActorSystem(\"SchedulingDecisionMakerTests\"))\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with StreamLogging {\n\n  behavior of \"SchedulingDecisionMaker\"\n\n  implicit val ec = system.dispatcher\n\n  val testNamespace = \"test-namespace\"\n  val testAction = \"test-action\"\n  val action = FullyQualifiedEntityName(EntityPath(testNamespace), EntityName(testAction), Some(SemVer(0, 0, 1)))\n\n  val schedulingConfig = SchedulingConfig(100.milliseconds, 100.milliseconds, 10.seconds, false, 1.5)\n\n  it should \"decide pausing when the limit is less than equal to 0\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = false,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 0,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 0,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = None,\n      namespaceLimit = 0,\n      actionLimit = 0, // limit is 0,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(Pausing, 0))\n  }\n\n  it should \"skip decision if the state is already Flushing when the limit is <= 0\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = false,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 0,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 0,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = None,\n      namespaceLimit = 0,\n      actionLimit = 0, // limit is 0\n      maxActionConcurrency = 1,\n      stateName = Flushing,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectNoMessage()\n  }\n\n  it should \"skip decision at any time if there is no message\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n\n    // For Throttled states, there will be always at least one message to disable the throttling\n    val states = List(Running, Idle, Flushing, Removing, Removed)\n    val testProbe = TestProbe()\n\n    states.foreach { state =>\n      val msg = QueueSnapshot(\n        initialized = false,\n        incomingMsgCount = new AtomicInteger(0),\n        currentMsgCount = 0,\n        existingContainerCount = 1,\n        inProgressContainerCount = 2,\n        staleActivationNum = 0,\n        existingContainerCountInNamespace = 1,\n        inProgressContainerCountInNamespace = 2,\n        averageDuration = None, // No average duration available\n        namespaceLimit = 10,\n        actionLimit = 10,\n        maxActionConcurrency = 1,\n        stateName = state,\n        recipient = testProbe.ref)\n\n      decisionMaker ! msg\n\n      testProbe.expectNoMessage()\n    }\n  }\n\n  it should \"skip decision at any time if there is no message even with avg duration\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val states = List(Running, Idle, Flushing, Removing, Removed)\n    val testProbe = TestProbe()\n\n    states.foreach { state =>\n      val msg = QueueSnapshot(\n        initialized = false,\n        incomingMsgCount = new AtomicInteger(0),\n        currentMsgCount = 0,\n        existingContainerCount = 3,\n        inProgressContainerCount = 5,\n        staleActivationNum = 0,\n        existingContainerCountInNamespace = 5,\n        inProgressContainerCountInNamespace = 8,\n        averageDuration = Some(1.0), // Some average duration available\n        namespaceLimit = 20,\n        actionLimit = 20,\n        maxActionConcurrency = 1,\n        stateName = state,\n        recipient = testProbe.ref)\n\n      decisionMaker ! msg\n\n      testProbe.expectNoMessage()\n    }\n  }\n\n  it should \"enable namespace throttling with dropping msg when there is not enough capacity, no container, and namespace over-provision disabled\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 0, // there is no container for this action\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1, // but there are already 2 containers in this namespace\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 2,\n      actionLimit = 2,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // this queue cannot create an initial container so enable throttling and drop messages.\n    testProbe.expectMsg(DecisionResults(EnableNamespaceThrottling(dropMsg = true), 0))\n  }\n\n  it should \"enable namespace throttling without dropping msg when there is not enough capacity but are some containers and namespace over-provision disabled\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 1, // there are some containers for this action\n      inProgressContainerCount = 1,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 2, // but there are already 2 containers in this namespace\n      inProgressContainerCountInNamespace = 2, // this value includes the count of this action as well.\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // this queue cannot create more containers\n    testProbe.expectMsg(DecisionResults(EnableNamespaceThrottling(dropMsg = false), 0))\n  }\n\n  it should \"add one container when there is no container, and namespace over-provision has capacity\" in {\n    val schedulingConfigNamespaceOverProvisioning =\n      SchedulingConfig(100.milliseconds, 100.milliseconds, 10.seconds, true, 1.5)\n    val decisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfigNamespaceOverProvisioning))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = false,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 0, // there is no container for this action\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1, // but there are already 2 containers in this namespace\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 2,\n      actionLimit = 2,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // this queue cannot create an initial container so enable throttling and drop messages.\n    testProbe.expectMsg(DecisionResults(AddInitialContainer, 1))\n  }\n\n  it should \"enable namespace throttling with dropping msg when there is no container, and namespace over-provision has no capacity\" in {\n    val schedulingConfigNamespaceOverProvisioning =\n      SchedulingConfig(100.milliseconds, 100.milliseconds, 10.seconds, true, 1.0)\n    val decisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfigNamespaceOverProvisioning))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 0, // there is no container for this action\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1, // but there are already 2 containers in this namespace\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 2,\n      actionLimit = 2,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // this queue cannot create an initial container so enable throttling and drop messages.\n    testProbe.expectMsg(DecisionResults(EnableNamespaceThrottling(dropMsg = true), 0))\n  }\n\n  it should \"disable namespace throttling when namespace over-provision has capacity again\" in {\n    val schedulingConfigNamespaceOverProvisioning =\n      SchedulingConfig(100.milliseconds, 100.milliseconds, 10.seconds, true, 1.1)\n    val decisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfigNamespaceOverProvisioning))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 1, // there is one container for this action\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1, // but there are already 2 containers in this namespace\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 2,\n      actionLimit = 2,\n      maxActionConcurrency = 1,\n      stateName = NamespaceThrottled,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // this queue cannot create an initial container so enable throttling and drop messages.\n    testProbe.expectMsg(DecisionResults(DisableNamespaceThrottling, 0))\n  }\n\n  it should \"enable namespace throttling without dropping msg when there is a container, and namespace over-provision has no additional capacity\" in {\n    val schedulingConfigNamespaceOverProvisioning =\n      SchedulingConfig(100.milliseconds, 100.milliseconds, 10.seconds, true, 1.0)\n    val decisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfigNamespaceOverProvisioning))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1, // but there are already 2 containers in this namespace\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 2,\n      actionLimit = 2,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // this queue cannot create an additional container so enable throttling and drop messages.\n    testProbe.expectMsg(DecisionResults(EnableNamespaceThrottling(dropMsg = false), 0))\n  }\n\n  it should \"not enable namespace throttling when there is not enough capacity but are some containers and namespace over-provision is enabled with capacity\" in {\n    val schedulingConfigNamespaceOverProvisioning =\n      SchedulingConfig(100.milliseconds, 100.milliseconds, 10.seconds, true, 1.5)\n    val decisionMaker =\n      system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfigNamespaceOverProvisioning))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 1, // there are some containers for this action\n      inProgressContainerCount = 1,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 2, // but there are already 2 containers in this namespace\n      inProgressContainerCountInNamespace = 2, // this value includes the count of this action as well.\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // this queue cannot create more containers\n    testProbe.expectNoMessage()\n  }\n\n  it should \"add an initial container if there is not any\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = false, // this queue is not initialized yet\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 0,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(AddInitialContainer, 1))\n  }\n\n  it should \"disable the namespace throttling with adding an initial container when there is no container\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = false, // this queue is not initialized yet\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 1,\n      existingContainerCount = 0,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = NamespaceThrottled,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(DisableNamespaceThrottling, 0))\n  }\n\n  it should \"disable the namespace throttling when there are some containers\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = false, // this queue is not initialized yet\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 0,\n      existingContainerCount = 1,\n      inProgressContainerCount = 1,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = NamespaceThrottled,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(DisableNamespaceThrottling, 0))\n  }\n\n  // this is an exceptional case\n  it should \"add an initial container if there is no container in the Running state\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(1), // if there is no message, it won't add a container.\n      currentMsgCount = 0,\n      existingContainerCount = 0,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(AddContainer, 1))\n  }\n\n  // this is an exceptional case\n  it should \"not add a container if there is no message even in the Running state\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0), // if there is no message, it won't add a container.\n      currentMsgCount = 0,\n      existingContainerCount = 0,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectNoMessage()\n  }\n\n  // this can happen when the limit was 0 for some reason previously but it is increased after some time.\n  it should \"add one container if there is no container in the Paused state\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0), // it should add a container even if there is no message this is because all messages are dropped in the Paused state\n      currentMsgCount = 0,\n      existingContainerCount = 0,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Flushing,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(AddInitialContainer, 1))\n  }\n\n  it should \"add one container if there is no container in the Waiting state\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0), // it should add a container even if there is no message this is because all messages are dropped in the Waiting state\n      currentMsgCount = 0,\n      existingContainerCount = 0,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Flushing,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(AddInitialContainer, 1))\n  }\n\n  it should \"add same number of containers with the number of stale messages if there are any\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 2,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 2,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(AddContainer, 2))\n  }\n\n  it should \"add exclude the number of in-progress container when adding containers for stale messages when there is no available duration\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 2,\n      existingContainerCount = 1,\n      inProgressContainerCount = 1,\n      staleActivationNum = 2,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(AddContainer, 1))\n  }\n\n  it should \"add at most the same number with the limit when adding containers for stale messages when there is no available duration\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 6,\n      existingContainerCount = 1,\n      inProgressContainerCount = 1,\n      staleActivationNum = 6,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = None,\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(EnableNamespaceThrottling(dropMsg = false), 2))\n  }\n\n  it should \"not add any container for stale messages if the increment is <= 0 when there when there is no available duration\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 2,\n      existingContainerCount = 1,\n      inProgressContainerCount = 6,\n      staleActivationNum = 2,\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 6,\n      averageDuration = None,\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectNoMessage()\n  }\n\n  it should \"add containers for stale messages based on duration when there is available duration\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 6,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 6, // stale messages exist\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(50), // the average duration exists\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages / threshold / duration\n    // 6 / 100 / 50 = 3\n    testProbe.expectMsg(DecisionResults(AddContainer, 3))\n  }\n\n  it should \"add containers for stale messages at most the number of messages when there is available duration\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 2,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 2, // stale messages exist\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(1000), // the average duration exists\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages / threshold / duration\n    // 2 / 100 / 1000 = 20, but add only 2 containers for 2 messages\n    testProbe.expectMsg(DecisionResults(AddContainer, 2))\n  }\n\n  it should \"add containers for stale messages within the limit when there is available duration\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 10,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 10, // stale messages exist\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(1000), // the average duration exists\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages / threshold / duration\n    // 10 / 100 / 1000 = 100, but because there is only 10 messages, it is supposed to be 10.\n    // Finally it becomes it becomes 3 because there are not enough capacity.\n    // limit - total containers in a namespace = 4 - 1 = 3\n    testProbe.expectMsg(DecisionResults(EnableNamespaceThrottling(dropMsg = false), 3))\n  }\n\n  it should \"add containers based on duration if there is no stale message\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(4),\n      currentMsgCount = 2,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0, // no stale message exist\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(1000), // the average duration exists\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages / threshold / duration\n    // 6 / 100 / 1000 = 60, but because there is only 6 messages, it is supposed to be 6.\n    // Finally it becomes it becomes 5 because there is already one container.\n    testProbe.expectMsg(DecisionResults(AddContainer, 5))\n  }\n\n  it should \"add containers based on duration within the capacity if there is no stale message\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(4),\n      currentMsgCount = 2,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0, // no stale message exist\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(1000), // the average duration exists\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages / threshold / duration\n    // 6 / 100 / 1000 = 60, but because there is only 6 messages, it is supposed to be 6.\n    // Finally it becomes it becomes 3 because there is not enough capacity\n    // limit - containers in a namespace = 4 - 1 = 3\n    testProbe.expectMsg(DecisionResults(EnableNamespaceThrottling(dropMsg = false), 3))\n  }\n\n  it should \"not add container when expected TPS is bigger than available message if there is no stale message\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 4,\n      existingContainerCount = 5,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0, // no stale message exist\n      existingContainerCountInNamespace = 5,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(1000), // the average duration exists\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages / threshold / duration\n    // 6 / 100 / 1000 = 60, but because there is only 6 messages, it is supposed to be 6.\n    // Finally it becomes it becomes 5 because there is already one container.\n    testProbe.expectNoMessage()\n  }\n\n  it should \"add one container when there is no container and are some messages\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 4,\n      existingContainerCount = 0,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0, // no stale message exist\n      existingContainerCountInNamespace = 0,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = None, // the average duration exists\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    testProbe.expectMsg(DecisionResults(AddContainer, 1))\n  }\n\n  it should \"add more containers when there are stale messages even in the GracefulShuttingDown state\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 4,\n      existingContainerCount = 1, // some containers are running and in-progress\n      inProgressContainerCount = 1,\n      staleActivationNum = 4, // stale message exist\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 1,\n      averageDuration = Some(1000), // the average duration exists\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Removing,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages / threshold / duration\n    // 4 / 100 / 1000 = 40, but because there is only 4 messages, it is supposed to be 4.\n    // Finally it becomes it becomes 3 because there is not enough capacity.\n    testProbe.expectMsg(DecisionResults(AddContainer, 3))\n  }\n\n  it should \"add more containers when there are stale messages even in the GracefulShuttingDown state when there is no duration\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 4,\n      existingContainerCount = 1,\n      inProgressContainerCount = 2, // some containers are in progress\n      staleActivationNum = 4, // stale message exist\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 2,\n      averageDuration = None, // the average duration does not exist\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Removing,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages / threshold / duration\n    // 6 / 100 / 1000 = 60, but because there is only 6 messages, it is supposed to be 6.\n    // Finally it becomes it becomes 5 because there is already one container.\n    testProbe.expectMsg(DecisionResults(AddContainer, 2))\n  }\n\n  it should \"add more containers when there are stale messages and non-stale messages and both message classes need more containers\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 5,\n      existingContainerCount = 2,\n      inProgressContainerCount = 0,\n      staleActivationNum = 2,\n      existingContainerCountInNamespace = 2,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(1000), // the average duration exists\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    //should add two for the stale messages and one to increase tps of non-stale available messages\n    testProbe.expectMsg(DecisionResults(AddContainer, 3))\n  }\n\n  it should \"add more containers when there are stale messages and non-stale messages have needed tps\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 5,\n      existingContainerCount = 2,\n      inProgressContainerCount = 0,\n      staleActivationNum = 2,\n      existingContainerCountInNamespace = 2,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(50), // the average duration gives container throughput of 2\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 1,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    //should add one additional container for stale messages and non-stale messages still meet tps\n    testProbe.expectMsg(DecisionResults(AddContainer, 1))\n  }\n\n  it should \"enable namespace throttling while adding more container when there are stale messages even in the GracefulShuttingDown\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 4,\n      existingContainerCount = 1,\n      inProgressContainerCount = 2, // some containers are in progress\n      staleActivationNum = 4, // stale message exist\n      existingContainerCountInNamespace = 1,\n      inProgressContainerCountInNamespace = 2,\n      averageDuration = None, // the average duration does not exist\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 1,\n      stateName = Removing,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages / threshold / duration\n    // 4 / 100 / 1000 = 40, but because there is only 4 messages, it is supposed to be 4.\n    // it should subtract the in-progress number so it is supposed to be 2\n    // but there is not enough capacity, it becomes 1\n    testProbe.expectMsg(DecisionResults(EnableNamespaceThrottling(false), 1))\n  }\n\n  it should \"correctly calculate demand is met when action concurrency >1 w/ average duration and no stale activations\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    // container\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 4,\n      existingContainerCount = 2,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 2,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(100.0),\n      namespaceLimit = 4,\n      actionLimit = 4,\n      maxActionConcurrency = 2,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages is 4 with duration equaling the stale threshold and action concurrency of 2 so needed containers\n    // should be exactly 2\n    testProbe.expectNoMessage()\n  }\n\n  it should \"add containers when action concurrency >1 w/ average duration and demand is not met\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    // container\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 20,\n      existingContainerCount = 2,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 2,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(50.0),\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 3,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages is 20 and throughput should be 100.0 / 50.0 * 3 = 6\n    // existing container is 2 so can handle 12 messages, therefore need 2 more containers\n    testProbe.expectMsg(DecisionResults(AddContainer, 2))\n  }\n\n  it should \"add containers when action concurrency >1 w/ average duration and demand is not met and has stale activations\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    // container\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 30,\n      existingContainerCount = 2,\n      inProgressContainerCount = 0,\n      staleActivationNum = 10,\n      existingContainerCountInNamespace = 2,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(50.0),\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 3,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // available messages is 30 and throughput should be 100.0 / 50.0 * 3 = 6\n    // existing container is 2 so can handle 12 messages, therefore need 2 more containers for non-stale\n    // stale has 10 activations so need another additional 2\n    testProbe.expectMsg(DecisionResults(AddContainer, 4))\n  }\n\n  it should \"add containers when action concurrency >1 when no average duration and there are stale activations\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    // container\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 10,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 10,\n      existingContainerCountInNamespace = 2,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = None,\n      namespaceLimit = 10,\n      actionLimit = 10,\n      maxActionConcurrency = 3,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // stale messages are 10. want stale to be handled by first pass of requests from containers so\n    // 10 / 3 = 4.0\n    testProbe.expectMsg(DecisionResults(AddContainer, 4))\n  }\n\n  it should \"add only up to the action container limit if less than the namespace limit\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    // container\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 100,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 2,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(100.0),\n      namespaceLimit = 10,\n      actionLimit = 5,\n      maxActionConcurrency = 3,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // one container already exists with an action limit of 5. Number of messages will exceed limit of containers\n    // so use smaller of the two limits\n    testProbe.expectMsg(DecisionResults(AddContainer, 4))\n  }\n\n  it should \"add only up to the namespace limit total if existing containers in namespace prevents reaching action limit\" in {\n    val decisionMaker = system.actorOf(SchedulingDecisionMaker.props(testNamespace, action, schedulingConfig))\n    val testProbe = TestProbe()\n\n    // container\n    val msg = QueueSnapshot(\n      initialized = true,\n      incomingMsgCount = new AtomicInteger(0),\n      currentMsgCount = 100,\n      existingContainerCount = 1,\n      inProgressContainerCount = 0,\n      staleActivationNum = 0,\n      existingContainerCountInNamespace = 7,\n      inProgressContainerCountInNamespace = 0,\n      averageDuration = Some(100.0),\n      namespaceLimit = 10,\n      actionLimit = 5,\n      maxActionConcurrency = 3,\n      stateName = Running,\n      recipient = testProbe.ref)\n\n    decisionMaker ! msg\n\n    // one container already exists with an action limit of 5. There are currently 7 containers in namespace\n    // so can only add 3 more even if that only gives this action 4 containers when it has an action limit of 5\n    testProbe.expectMsg(DecisionResults(EnableNamespaceThrottling(false), 3))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/service/DataManagementServiceTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.service\n\nimport org.apache.pekko.actor.{Actor, ActorRef, ActorRefFactory, ActorSystem, Props}\nimport org.apache.pekko.testkit.{ImplicitSender, TestActor, TestActorRef, TestKit, TestProbe}\nimport org.apache.pekko.util.Timeout\nimport common.StreamLogging\nimport org.apache.openwhisk.core.entity.SchedulerInstanceId\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.collection.mutable\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.Random\n\n@RunWith(classOf[JUnitRunner])\nclass DataManagementServiceTests\n    extends TestKit(ActorSystem(\"DataManagementService\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with StreamLogging {\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n  implicit val ec: ExecutionContext = system.dispatcher\n\n  val schedulerId = SchedulerInstanceId(\"scheduler0\")\n  val instanceId = schedulerId\n  val leaseService = TestProbe()\n  val watcherName = \"data-management-service\"\n  leaseService.setAutoPilot((sender: ActorRef, msg: Any) =>\n    msg match {\n      case GetLease =>\n        sender ! Lease(10, 10)\n        TestActor.KeepRunning\n\n      case _ =>\n        TestActor.KeepRunning\n  })\n\n  private def etcdWorkerFactory(actor: ActorRef) = { (_: ActorRefFactory) =>\n    actor\n  }\n\n  behavior of \"DataManagementService\"\n\n  it should \"distribute work to etcd worker\" in {\n    val watcherService = TestProbe()\n    val worker = TestProbe()\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, etcdWorkerFactory(worker.ref)))\n\n    val requests = Seq(\n      RegisterData(key, value),\n      ElectLeader(key, value, self),\n      RegisterInitialData(key, value, recipient = Some(testActor)),\n      WatcherClosed(key, false))\n\n    requests.foreach { request =>\n      service ! request\n      worker.expectMsg(request)\n\n      service ! FinishWork(key)\n    }\n  }\n\n  it should \"handle request sequentially for a same key\" in {\n    val queue = mutable.Queue.empty[String]\n    val watcherService = TestProbe()\n    val workerFactory = (f: ActorRefFactory) =>\n      f.actorOf(Props(new Actor {\n        override def receive: Receive = {\n          case request: RegisterData =>\n            if (request.value == \"async\")\n              Future {\n                Thread.sleep(1000)\n                queue.enqueue(request.value)\n                context.parent ! FinishWork(request.key)\n              } else {\n              queue.enqueue(request.value)\n              context.parent ! FinishWork(request.key)\n            }\n        }\n      }))\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, workerFactory))\n\n    // the first request will be handled asynchronously, but as the second request has the same key, it will always\n    // processed after the first request is finished\n    val requests = Seq(RegisterData(key, \"async\"), RegisterData(key, value))\n\n    requests.foreach { request =>\n      service ! request\n    }\n\n    Thread.sleep(2000) // wait for two requests are completed\n    queue.dequeue() shouldBe \"async\"\n    queue.dequeue() shouldBe value // the second request should be wait for the first one finished\n    queue.size shouldBe 0\n  }\n\n  it should \"handle request concurrently for different keys\" in {\n    val queue = mutable.Queue.empty[String]\n    val watcherService = TestProbe()\n    val workerFactory = (f: ActorRefFactory) =>\n      f.actorOf(Props(new Actor {\n        override def receive: Receive = {\n          case request: RegisterData =>\n            if (request.value == \"async\")\n              Future {\n                Thread.sleep(1000)\n                queue.enqueue(request.value)\n                context.parent ! FinishWork(request.key)\n              } else {\n              queue.enqueue(request.value)\n              context.parent ! FinishWork(request.key)\n            }\n        }\n      }))\n\n    val key = \"testKey\"\n    val key2 = \"testKey2\"\n    val value = \"testValue\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, workerFactory))\n\n    val requests = Seq(RegisterData(key, \"async\"), RegisterData(key2, value))\n\n    requests.foreach { request =>\n      service ! request\n    }\n\n    Thread.sleep(2000) // wait for two requests are completed\n    queue.dequeue() shouldBe value // the second request should be completed first because it doesn't wait\n    queue.dequeue() shouldBe \"async\"\n    queue.size shouldBe 0\n  }\n\n  it should \"remove unnecessary operation\" in {\n    val watcherService = TestProbe()\n    val worker = TestProbe()\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, etcdWorkerFactory(worker.ref)))\n\n    service ! RegisterData(key, value) // occupy the resource\n    worker.expectMsg(RegisterData(key, value))\n\n    service ! RegisterInitialData(key, value) // this request should also be removed\n\n    val requests = Random.shuffle(\n      Seq(RegisterData(key, value), RegisterData(key, value), WatcherClosed(key, false), WatcherClosed(key, false)))\n    // below requests will be merged into one request and wait in the queue\n    requests.foreach { request =>\n      service ! request\n    }\n    worker.expectNoMessage()\n\n    service ! FinishWork(key) // release the resource\n\n    worker.expectMsg(requests(3)) // only the last one should be distributed\n  }\n\n  it should \"register data when the target endpoint is removed\" in {\n    val watcherService = TestProbe()\n    val worker = TestProbe()\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, etcdWorkerFactory(worker.ref)))\n\n    // no new watcher is registered as it assumes the one is already registered.\n    service ! WatchEndpointRemoved(key, key, value, isPrefix = false)\n    worker.expectMsg(RegisterInitialData(key, value, false, None))\n  }\n\n  it should \"ignore prefixed endpoint-removed results\" in {\n    val watcherService = TestProbe()\n    val worker = TestProbe()\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, etcdWorkerFactory(worker.ref)))\n    service ! WatchEndpointRemoved(\"\", key, value, isPrefix = true)\n\n    worker.expectNoMessage()\n  }\n\n  it should \"deregister data\" in {\n    val watcherService = TestProbe()\n    val worker = TestProbe()\n    watcherService.setAutoPilot((sender, msg) => {\n      msg match {\n        case UnwatchEndpoint(key, isPrefix, _, _) =>\n          sender ! WatcherClosed(key, isPrefix)\n          TestActor.KeepRunning\n      }\n    })\n    val key = \"testKey\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, etcdWorkerFactory(worker.ref)))\n    service ! UnregisterData(key)\n\n    watcherService.expectMsg(UnwatchEndpoint(key, isPrefix = false, watcherName, true))\n    worker.expectMsg(WatcherClosed(key, false))\n  }\n\n  it should \"store the resource data\" in {\n    val watcherService = TestProbe()\n    val worker = TestProbe()\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, etcdWorkerFactory(worker.ref)))\n    service ! UpdateDataOnChange(key, value)\n\n    worker.expectMsg(RegisterData(key, value))\n    service.underlyingActor.dataCache.size shouldBe 1\n  }\n\n  it should \"not store the resource data if there is no change from the last one\" in {\n    val watcherService = TestProbe()\n    val worker = TestProbe()\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, etcdWorkerFactory(worker.ref)))\n    service ! UpdateDataOnChange(key, value)\n\n    worker.expectMsg(RegisterData(key, value))\n    service.underlyingActor.dataCache.size shouldBe 1\n\n    service ! UpdateDataOnChange(key, value)\n    worker.expectNoMessage()\n    service.underlyingActor.dataCache.size shouldBe 1\n  }\n\n  it should \"store the resource data if there is change from the last one\" in {\n    val watcherService = TestProbe()\n    val worker = TestProbe()\n    val key = \"testKey\"\n    val value = \"testValue\"\n    val newValue = \"newTestValue\"\n\n    val service = TestActorRef(new DataManagementService(watcherService.ref, etcdWorkerFactory(worker.ref)))\n    service ! UpdateDataOnChange(key, value)\n\n    worker.expectMsg(RegisterData(key, value))\n    service.underlyingActor.dataCache.size shouldBe 1\n\n    service ! FinishWork(key)\n    service ! UpdateDataOnChange(key, newValue)\n    worker.expectMsg(RegisterData(key, newValue, false))\n    service.underlyingActor.dataCache.size shouldBe 1\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/service/LeaseKeepAliveServiceTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.service\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.testkit.{ImplicitSender, TestFSMRef, TestKit, TestProbe}\nimport org.apache.pekko.util.Timeout\nimport com.ibm.etcd.api.{LeaseGrantResponse, LeaseKeepAliveResponse, LeaseRevokeResponse, PutResponse}\nimport common.StreamLogging\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.entity.{ExecManifest, SchedulerInstanceId}\nimport org.apache.openwhisk.core.etcd.{EtcdClient, EtcdKV}\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{ExecutionContext, Future}\n\n@RunWith(classOf[JUnitRunner])\nclass LeaseKeepAliveServiceTests\n    extends TestKit(ActorSystem(\"LeaseKeepAliveService\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with StreamLogging {\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n  implicit val ec: ExecutionContext = system.dispatcher\n  val config = new WhiskConfig(ExecManifest.requiredProperties)\n  val testInstanceId = SchedulerInstanceId(\"0\")\n  val testLeaseId = 10\n  val newTestLeaseId = 20\n  val testTtl = 1\n  val testLease = Lease(testLeaseId, testTtl)\n  val newTestLease = Lease(newTestLeaseId, testTtl)\n  val testKey = EtcdKV.InstanceKeys.instanceLease(testInstanceId)\n  val newTestKey = EtcdKV.InstanceKeys.instanceLease(testInstanceId)\n\n  val watcherName = \"lease-service\"\n\n  def grant(etcd: EtcdClient): Unit = {\n    (etcd\n      .grant(_: Long))\n      .expects(*)\n      .returning(Future.successful(LeaseGrantResponse.newBuilder().setID(testLeaseId).setTTL(testTtl).build()))\n  }\n\n  def put(etcd: EtcdClient): Unit = {\n    (etcd\n      .put(_: String, _: String, _: Long))\n      .expects(testKey, *, *)\n      .returning(Future.successful(PutResponse.newBuilder().build()))\n  }\n\n  def keepAliveOnce(etcd: EtcdClient): Unit = {\n    (etcd\n      .keepAliveOnce(_: Long))\n      .expects(testLeaseId)\n      .returning(Future.successful(LeaseKeepAliveResponse.newBuilder().setID(testLeaseId).build()))\n      .anyNumberOfTimes()\n  }\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  behavior of \"LeaseKeepAliveService\"\n\n  it should \"grant new lease\" in {\n\n    val mockEtcd = mock[EtcdClient]\n    grant(mockEtcd)\n    put(mockEtcd)\n    keepAliveOnce(mockEtcd)\n\n    val watcher = TestProbe()\n    val service = TestFSMRef(new LeaseKeepAliveService(mockEtcd, testInstanceId, watcher.ref))\n\n    Thread.sleep(1000)\n    service.stateName shouldBe Active\n    service.stateData shouldBe a[ActiveStates]\n    service.stateData match {\n      case ActiveStates(_, lease) => lease shouldBe testLease\n      case _                      => fail()\n    }\n    watcher.expectMsg(WatchEndpoint(testKey, testLease.id.toString, false, watcherName, Set(DeleteEvent)))\n\n  }\n\n  it should \"regrant a new lease while old lease is deleted\" in {\n    val mockEtcd = mock[EtcdClient]\n    grant(mockEtcd)\n    put(mockEtcd)\n    keepAliveOnce(mockEtcd)\n\n    (mockEtcd\n      .revoke(_: Long))\n      .expects(testLeaseId)\n      .returning(Future.successful(LeaseRevokeResponse.newBuilder().build()))\n    (mockEtcd\n      .grant(_: Long))\n      .expects(*)\n      .returning(Future.successful(LeaseGrantResponse.newBuilder().setID(newTestLeaseId).setTTL(testTtl).build()))\n    (mockEtcd\n      .put(_: String, _: String, _: Long))\n      .expects(newTestKey, *, *)\n      .returning(Future.successful(PutResponse.newBuilder().build()))\n    (mockEtcd\n      .keepAliveOnce(_: Long))\n      .expects(newTestLeaseId)\n      .returning(Future.successful(LeaseKeepAliveResponse.newBuilder().setID(newTestLeaseId).build()))\n      .anyNumberOfTimes()\n\n    val watcher = TestProbe()\n    val service = TestFSMRef(new LeaseKeepAliveService(mockEtcd, testInstanceId, watcher.ref))\n\n    service.stateName shouldBe Active\n    service.stateData shouldBe a[ActiveStates]\n    service.stateData match {\n      case ActiveStates(_, lease) => lease shouldBe testLease\n      case _                      => fail()\n    }\n    watcher.expectMsg(WatchEndpoint(testKey, testLease.id.toString, false, watcherName, Set(DeleteEvent)))\n\n    service ! WatchEndpointRemoved(testKey, testKey, testLease.id.toString, false)\n\n    watcher.expectMsg(UnwatchEndpoint(testKey, false, watcherName))\n    Thread.sleep(500) //wait for the lease to be granted\n\n    service.stateName shouldBe Active\n    service.stateData shouldBe a[ActiveStates]\n    service.stateData match {\n      case ActiveStates(_, lease) => lease shouldBe newTestLease\n      case _                      => fail()\n    }\n    watcher.expectMsg(WatchEndpoint(newTestKey, newTestLease.id.toString, false, watcherName, Set(DeleteEvent)))\n  }\n\n  it should \"get lease\" in {\n    val mockEtcd = mock[EtcdClient]\n    grant(mockEtcd)\n    put(mockEtcd)\n    keepAliveOnce(mockEtcd)\n\n    val service = TestFSMRef(new LeaseKeepAliveService(mockEtcd, testInstanceId, TestProbe().ref))\n\n    (service ? GetLease).mapTo[Lease].futureValue shouldBe testLease\n  }\n\n  it should \"regrant a new lease when keepalive is failed\" in {\n    val mockEtcd = mock[EtcdClient]\n    grant(mockEtcd)\n    put(mockEtcd)\n\n    (mockEtcd\n      .keepAliveOnce(_: Long))\n      .expects(testLeaseId)\n      .returning(Future.successful(LeaseKeepAliveResponse.newBuilder().setID(testLeaseId).build()))\n      .noMoreThanTwice()\n\n    (mockEtcd\n      .keepAliveOnce(_: Long))\n      .expects(testLeaseId)\n      .returning(Future.failed(new RuntimeException(\"failed to keep alive the lease\")))\n      .noMoreThanOnce()\n\n    (mockEtcd\n      .revoke(_: Long))\n      .expects(testLeaseId)\n      .returning(Future.successful(LeaseRevokeResponse.newBuilder().build()))\n\n    (mockEtcd\n      .keepAliveOnce(_: Long))\n      .expects(newTestLeaseId)\n      .returning(Future.successful(LeaseKeepAliveResponse.newBuilder().setID(newTestLeaseId).build()))\n      .anyNumberOfTimes()\n\n    (mockEtcd\n      .grant(_: Long))\n      .expects(*)\n      .returning(Future.successful(LeaseGrantResponse.newBuilder().setID(newTestLeaseId).setTTL(testTtl).build()))\n    (mockEtcd\n      .put(_: String, _: String, _: Long))\n      .expects(newTestKey, *, *)\n      .returning(Future.successful(PutResponse.newBuilder().build()))\n\n    val watcher = TestProbe()\n    val service = TestFSMRef(new LeaseKeepAliveService(mockEtcd, testInstanceId, watcher.ref))\n    service.stateName shouldBe Active\n    service.stateData shouldBe a[ActiveStates]\n    service.stateData match {\n      case ActiveStates(_, lease) => lease shouldBe testLease\n      case _                      => fail()\n    }\n    watcher.expectMsg(WatchEndpoint(testKey, testLease.id.toString, false, watcherName, Set(DeleteEvent)))\n\n    watcher.expectMsg(UnwatchEndpoint(testKey, false, watcherName))\n    Thread.sleep(1500) //wait for the lease to be granted\n\n    service.stateName shouldBe Active\n    service.stateData shouldBe a[ActiveStates]\n    service.stateData match {\n      case ActiveStates(_, lease) => lease shouldBe newTestLease\n      case _                      => fail()\n    }\n    watcher.expectMsg(WatchEndpoint(newTestKey, newTestLease.id.toString, false, watcherName, Set(DeleteEvent)))\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/core/service/WatcherServiceTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.service\n\nimport java.{lang, util}\nimport java.util.concurrent.Executor\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.testkit.{ImplicitSender, TestActorRef, TestKit, TestProbe}\nimport org.apache.pekko.util.Timeout\nimport com.google.protobuf.ByteString\nimport com.ibm.etcd.api.{Event, KeyValue, ResponseHeader}\nimport com.ibm.etcd.api.Event.EventType\nimport com.ibm.etcd.client.kv.KvClient.Watch\nimport com.ibm.etcd.client.kv.WatchUpdate\nimport com.ibm.etcd.client.{EtcdClient => Client}\nimport common.StreamLogging\nimport org.apache.openwhisk.core.entity.SchedulerInstanceId\nimport org.apache.openwhisk.core.etcd.EtcdClient\nimport org.junit.runner.RunWith\nimport org.scalamock.scalatest.MockFactory\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\n\nimport scala.concurrent.ExecutionContextExecutor\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass WatcherServiceTests\n    extends TestKit(ActorSystem(\"WatcherService\"))\n    with ImplicitSender\n    with AnyFlatSpecLike\n    with ScalaFutures\n    with Matchers\n    with MockFactory\n    with BeforeAndAfterAll\n    with StreamLogging {\n\n  implicit val timeout: Timeout = Timeout(5.seconds)\n  implicit val ece: ExecutionContextExecutor = system.dispatcher\n\n  private val watchName = \"test-watcher-service\"\n\n  val schedulerId = SchedulerInstanceId(\"scheduler0\")\n\n  val client: Client = {\n    val hostAndPorts = \"172.17.0.1:2379\"\n    Client.forEndpoints(hostAndPorts).withPlainText().build()\n  }\n\n  val watch = new Watch {\n    override def close(): Unit = {}\n\n    override def addListener(listener: Runnable, executor: Executor): Unit = {}\n\n    override def cancel(mayInterruptIfRunning: Boolean): Boolean = true\n\n    override def isCancelled: Boolean = true\n\n    override def isDone: Boolean = true\n\n    override def get(): lang.Boolean = true\n\n    override def get(timeout: Long, unit: TimeUnit): lang.Boolean = true\n  }\n\n  private def watchEtcd(etcdClient: EtcdClient): Unit = {\n    (etcdClient\n      .watchAllKeys(_: WatchUpdate => Unit, _: Throwable => Unit, _: () => Unit))\n      .expects(*, *, *)\n      .returning(watch)\n  }\n\n  behavior of \"WatcherService\"\n\n  it should \"watch a endpoint\" in {\n    val etcdClient = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    watchEtcd(etcdClient)\n\n    val service = TestActorRef(new WatcherService(etcdClient))\n    service ! WatchEndpoint(key, value, isPrefix = false, watchName, Set(DeleteEvent))\n    service.underlyingActor.deleteWatchers.size shouldBe 1\n\n    service ! WatchEndpoint(key, value, isPrefix = false, watchName, Set(PutEvent))\n    service.underlyingActor.putWatchers.size shouldBe 1\n\n    service ! WatchEndpoint(key, value, isPrefix = false, watchName + 1, Set(DeleteEvent, PutEvent))\n    service.underlyingActor.deleteWatchers.size shouldBe 2\n    service.underlyingActor.putWatchers.size shouldBe 2\n\n    service ! WatchEndpoint(key, value, isPrefix = true, watchName, Set(DeleteEvent))\n    service.underlyingActor.prefixDeleteWatchers.size shouldBe 1\n\n    service ! WatchEndpoint(key, value, isPrefix = true, watchName, Set(PutEvent))\n    service.underlyingActor.prefixPutWatchers.size shouldBe 1\n\n    service ! WatchEndpoint(key, value, isPrefix = true, watchName + 1, Set(DeleteEvent, PutEvent))\n    service.underlyingActor.prefixDeleteWatchers.size shouldBe 2\n    service.underlyingActor.prefixPutWatchers.size shouldBe 2\n  }\n\n  it should \"close the watcher upon UnWatchEndpoint event\" in {\n    val etcdClient = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    watchEtcd(etcdClient)\n\n    val service = TestActorRef(new WatcherService(etcdClient))\n\n    service ! WatchEndpoint(key, value, isPrefix = false, watchName, Set(DeleteEvent))\n    service.underlyingActor.deleteWatchers.size shouldBe 1\n\n    service ! WatchEndpoint(key, value, isPrefix = false, watchName + \"1\", Set(DeleteEvent))\n    service.underlyingActor.deleteWatchers.size shouldBe 2\n\n    service ! UnwatchEndpoint(key, isPrefix = false, watchName)\n    service.underlyingActor.deleteWatchers.size shouldBe 1\n\n    service ! UnwatchEndpoint(key, isPrefix = false, watchName + \"1\")\n    service.underlyingActor.deleteWatchers.size shouldBe 0\n  }\n\n  it should \"notify the recipient if a deletion or put event occurs\" in {\n    val etcdClient = new MockWatchClient(client)(ece)\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val probe = TestProbe()\n    val service = TestActorRef(new WatcherService(etcdClient))\n    val request = WatchEndpoint(key, value, isPrefix = false, watchName, Set(DeleteEvent, PutEvent))\n\n    probe.send(service, request)\n\n    service.underlyingActor.deleteWatchers.size shouldBe 1\n    service.underlyingActor.putWatchers.size shouldBe 1\n\n    etcdClient.onNext should not be null\n\n    etcdClient.publishEvents(EventType.DELETE, key, value)\n    probe.expectMsg(WatchEndpointRemoved(request.key, key, value, request.isPrefix))\n\n    etcdClient.publishEvents(EventType.PUT, key, value)\n    probe.expectMsg(WatchEndpointInserted(request.key, key, value, request.isPrefix))\n\n    service ! UnwatchEndpoint(key, false, watchName)\n\n    val request2 = WatchEndpoint(\"test\", \"\", isPrefix = true, watchName, Set(DeleteEvent, PutEvent))\n    probe.send(service, request2)\n\n    etcdClient.publishEvents(EventType.DELETE, key, value)\n    probe.expectMsg(WatchEndpointRemoved(request2.key, key, value, request2.isPrefix))\n\n    etcdClient.publishEvents(EventType.PUT, key, value)\n    probe.expectMsg(WatchEndpointInserted(request2.key, key, value, request2.isPrefix))\n  }\n\n  it should \"not notify the recipient if event type mismatched\" in {\n    val etcdClient = new MockWatchClient(client)(ece)\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val probe = TestProbe()\n    val service = TestActorRef(new WatcherService(etcdClient))\n    val request = WatchEndpoint(key, value, isPrefix = false, watchName, Set(DeleteEvent))\n\n    probe.send(service, request)\n\n    service.underlyingActor.deleteWatchers.size shouldBe 1\n\n    etcdClient.onNext should not be null\n\n    etcdClient.publishEvents(EventType.PUT, key, value)\n    probe.expectNoMessage()\n    service ! UnwatchEndpoint(key, false, watchName) // close the watcher for delete event\n\n    val request2 = WatchEndpoint(key, value, isPrefix = false, watchName, Set(PutEvent))\n\n    probe.send(service, request2)\n\n    service.underlyingActor.putWatchers.size shouldBe 1\n\n    etcdClient.onNext should not be null\n\n    etcdClient.publishEvents(EventType.DELETE, key, value)\n    probe.expectNoMessage()\n  }\n\n  it should \"not register a watch request if there is already registered one\" in {\n    val etcdClient = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n    val numOfTries = 3\n\n    watchEtcd(etcdClient)\n\n    val service = TestActorRef(new WatcherService(etcdClient))\n\n    (1 to numOfTries).foreach { _ =>\n      service ! WatchEndpoint(key, value, isPrefix = false, watchName, Set(DeleteEvent))\n    }\n\n    service.underlyingActor.deleteWatchers.size shouldBe 1\n  }\n\n  it should \"register a watch request if there is already registered one but with different watch name\" in {\n    val etcdClient = mock[EtcdClient]\n\n    val key = \"testKey\"\n    val value = \"testValue\"\n    val numOfTries = 3\n\n    watchEtcd(etcdClient)\n\n    val service = TestActorRef(new WatcherService(etcdClient))\n\n    (1 to numOfTries).foreach { index =>\n      service ! WatchEndpoint(key, value, isPrefix = false, watchName + index, Set(DeleteEvent))\n    }\n\n    service.underlyingActor.deleteWatchers.size shouldBe 3\n  }\n\n  it should \"restart underlying etcd watch if error occurs\" in {\n    val etcdClient = new MockWatchClient(client)(ece)\n    val key = \"testKey\"\n    val value = \"testValue\"\n\n    val probe = TestProbe()\n    val service = TestActorRef(new WatcherService(etcdClient))\n\n    etcdClient.onNext should not be null\n    etcdClient.onError should not be null\n    etcdClient.watchAllKeysCallCount shouldBe 1\n\n    val t = new Throwable(\"error\")\n    etcdClient.onError(t)\n\n    etcdClient.onNext should not be null\n    etcdClient.onError should not be null\n    etcdClient.watchAllKeysCallCount shouldBe 2\n  }\n\n}\n\nclass mockWatchUpdate extends WatchUpdate {\n  private val eventLists: util.List[Event] = new util.ArrayList[Event]()\n  override def getHeader: ResponseHeader = ???\n\n  def addEvents(event: Event): WatchUpdate = {\n    eventLists.add(event)\n    this\n  }\n\n  override def getEvents: util.List[Event] = eventLists\n}\n\nclass MockWatchClient(client: Client)(ece: ExecutionContextExecutor) extends EtcdClient(client)(ece) {\n  var onNext: WatchUpdate => Unit = null\n  var onError: Throwable => Unit = null\n  var watchAllKeysCallCount = 0\n\n  override def watchAllKeys(next: WatchUpdate => Unit, error: Throwable => Unit, completed: () => Unit): Watch = {\n    onNext = next\n    onError = error\n    watchAllKeysCallCount += 1\n    new Watch {\n      override def close(): Unit = {}\n\n      override def addListener(listener: Runnable, executor: Executor): Unit = {}\n\n      override def cancel(mayInterruptIfRunning: Boolean): Boolean = true\n\n      override def isCancelled: Boolean = true\n\n      override def isDone: Boolean = true\n\n      override def get(): lang.Boolean = true\n\n      override def get(timeout: Long, unit: TimeUnit): lang.Boolean = true\n    }\n  }\n\n  def publishEvents(eventType: EventType, key: String, value: String): Unit = {\n    val eType = eventType match {\n      case EventType.PUT          => EventType.PUT\n      case EventType.DELETE       => EventType.DELETE\n      case EventType.UNRECOGNIZED => EventType.UNRECOGNIZED\n    }\n    val event = Event\n      .newBuilder()\n      .setType(eType)\n      .setPrevKv(\n        KeyValue\n          .newBuilder()\n          .setKey(ByteString.copyFromUtf8(key))\n          .setValue(ByteString.copyFromUtf8(value))\n          .build())\n      .setKv(\n        KeyValue\n          .newBuilder()\n          .setKey(ByteString.copyFromUtf8(key))\n          .setValue(ByteString.copyFromUtf8(value))\n          .build())\n      .build()\n    onNext(new mockWatchUpdate().addEvents(event))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/http/PoolingRestClientTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.http\n\nimport org.junit.runner.RunWith\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpecLike\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.apache.pekko.NotUsed\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.Flow\nimport org.apache.pekko.testkit.TestKit\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.{GET, POST}\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.{InternalServerError, NotFound}\nimport org.apache.pekko.http.scaladsl.model.headers.RawHeader\nimport org.apache.pekko.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException\nimport common.StreamLogging\nimport spray.json.JsObject\nimport spray.json.DefaultJsonProtocol._\n\nimport scala.concurrent.duration._\nimport scala.concurrent.{Await, ExecutionContext, Future, Promise, TimeoutException}\nimport scala.util.{Success, Try}\nimport org.apache.openwhisk.http.PoolingRestClient._\n\n@RunWith(classOf[JUnitRunner])\nclass PoolingRestClientTests\n    extends TestKit(ActorSystem(\"PoolingRestClientTests\"))\n    with AnyFlatSpecLike\n    with Matchers\n    with BeforeAndAfterAll\n    with ScalaFutures\n    with StreamLogging {\n  implicit val ec: ExecutionContext = system.dispatcher\n\n  override def afterAll(): Unit = {\n    TestKit.shutdownActorSystem(system)\n    super.afterAll()\n  }\n\n  def testFlow(httpResponse: HttpResponse = HttpResponse(), httpRequest: HttpRequest = HttpRequest())\n    : Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), NotUsed] =\n    Flow[(HttpRequest, Promise[HttpResponse])]\n      .mapAsyncUnordered(1) {\n        case (request, userContext) =>\n          request shouldBe httpRequest\n          Future.successful((Success(httpResponse), userContext))\n      }\n\n  def failFlow(httpResponse: HttpResponse = HttpResponse(), httpRequest: HttpRequest = HttpRequest())\n    : Flow[(HttpRequest, Promise[HttpResponse]), (Try[HttpResponse], Promise[HttpResponse]), NotUsed] =\n    Flow[(HttpRequest, Promise[HttpResponse])]\n      .mapAsyncUnordered(1) {\n        case (request, userContext) =>\n          Future.failed(new Exception)\n      }\n\n  def await[T](awaitable: Future[T], timeout: FiniteDuration = 10.seconds) = Await.result(awaitable, timeout)\n\n  behavior of \"Pooling REST Client\"\n\n  it should \"error when configuration protocol is invalid\" in {\n    a[IllegalArgumentException] should be thrownBy new PoolingRestClient(\"invalid\", \"host\", 443, 1)\n  }\n\n  it should \"get a non-200 status code when performing a request\" in {\n    val httpResponse = HttpResponse(InternalServerError)\n    val httpRequest = HttpRequest()\n    val poolingRestClient = new PoolingRestClient(\"https\", \"host\", 443, 1, Some(testFlow(httpResponse)))\n\n    await(poolingRestClient.request(Future.successful(httpRequest))) shouldBe httpResponse\n  }\n\n  it should \"return payload from a request\" in {\n    val httpResponse = HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val httpRequest = HttpRequest()\n    val poolingRestClient = new PoolingRestClient(\"https\", \"host\", 443, 1, Some(testFlow(httpResponse)))\n\n    await(poolingRestClient.request(Future.successful(httpRequest))) shouldBe httpResponse\n  }\n\n  it should \"send headers when making a request\" in {\n    val httpResponse = HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val httpRequest = HttpRequest(headers = List(RawHeader(\"key\", \"value\")))\n    val poolingRestClient = new PoolingRestClient(\"https\", \"host\", 443, 1, Some(testFlow(httpResponse, httpRequest)))\n\n    await(poolingRestClient.request(Future.successful(httpRequest))) shouldBe httpResponse\n  }\n\n  it should \"send uri when making a request\" in {\n    val httpResponse = HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val httpRequest = HttpRequest(uri = Uri(\"/some/where\"))\n    val poolingRestClient = new PoolingRestClient(\"https\", \"host\", 443, 1, Some(testFlow(httpResponse, httpRequest)))\n\n    await(poolingRestClient.request(Future.successful(httpRequest))) shouldBe httpResponse\n  }\n\n  it should \"send a payload when making a request\" in {\n    val httpResponse = HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val httpRequest = HttpRequest(POST, entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, \"payload\"))\n    val poolingRestClient = new PoolingRestClient(\"https\", \"host\", 443, 1, Some(testFlow(httpResponse, httpRequest)))\n\n    await(poolingRestClient.request(Future.successful(httpRequest))) shouldBe httpResponse\n  }\n\n  it should \"return JSON when making a request\" in {\n    val httpResponse = HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val httpRequest = HttpRequest(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val poolingRestClient = new PoolingRestClient(\"https\", \"host\", 443, 1, Some(testFlow(httpResponse, httpRequest)))\n    val request = mkJsonRequest(GET, Uri./, JsObject.empty, List.empty)\n\n    await(poolingRestClient.requestJson[JsObject](request)) shouldBe Right(JsObject.empty)\n  }\n\n  it should \"throw timeout exception when Future fails in httpFlow\" in {\n    val httpResponse = HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val httpRequest = HttpRequest(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val poolingRestClient = new PoolingRestClient(\"https\", \"host\", 443, 1, Some(failFlow(httpResponse, httpRequest)))\n    val request = mkJsonRequest(GET, Uri./, JsObject.empty, List.empty)\n\n    a[TimeoutException] should be thrownBy await(poolingRestClient.requestJson[JsObject](request))\n  }\n\n  it should \"return a status code on request failure\" in {\n    val body = \"Limit is too large, must not exceed 268435456\"\n    val httpResponse = HttpResponse(NotFound, entity = body)\n    val httpRequest = HttpRequest(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val poolingRestClient = new PoolingRestClient(\"https\", \"host\", 443, 1, Some(testFlow(httpResponse, httpRequest)))\n    val request = mkJsonRequest(GET, Uri./, JsObject.empty, List.empty)\n\n    val reqResult = await(poolingRestClient.requestJson[JsObject](request))\n    reqResult shouldBe Left(NotFound)\n    reqResult.left.get.reason shouldBe s\"${NotFound.reason} (details: ${body})\"\n  }\n\n  it should \"throw an unsupported content-type exception when unexpected content-type is returned\" in {\n    val httpResponse = HttpResponse(entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, \"plain text\"))\n    val httpRequest = HttpRequest(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val poolingRestClient = new PoolingRestClient(\"https\", \"host\", 443, 1, Some(testFlow(httpResponse, httpRequest)))\n    val request = mkJsonRequest(GET, Uri./, JsObject.empty, List.empty)\n\n    a[UnsupportedContentTypeException] should be thrownBy await(poolingRestClient.requestJson[JsObject](request))\n  }\n\n  it should \"create an HttpRequest without a payload\" in {\n    val httpRequest = HttpRequest()\n\n    await(mkRequest(GET, Uri./)) shouldBe httpRequest\n  }\n\n  it should \"create an HttpRequest with a JSON payload\" in {\n    val httpRequest = HttpRequest(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n\n    await(mkJsonRequest(GET, Uri./, JsObject.empty, List.empty)) shouldBe httpRequest\n  }\n\n  it should \"create an HttpRequest with a payload\" in {\n    val httpRequest = HttpRequest(entity = HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint))\n    val request = mkRequest(\n      GET,\n      Uri./,\n      Future.successful(HttpEntity(ContentTypes.`application/json`, JsObject.empty.compactPrint)),\n      List.empty)\n\n    await(request) shouldBe httpRequest\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/spi/SpiTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.spi\n\nimport com.typesafe.config.ConfigException\nimport common.StreamLogging\nimport common.WskActorSystem\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass SpiTests extends AnyFlatSpec with Matchers with WskActorSystem with StreamLogging {\n\n  behavior of \"SpiProvider\"\n\n  it should \"load an Spi from SpiLoader via typesafe config\" in {\n    val simpleSpi = SpiLoader.get[SimpleSpi]\n    simpleSpi shouldBe a[SimpleSpi]\n  }\n\n  it should \"throw an exception if the impl defined in application.conf is missing\" in {\n    a[ClassNotFoundException] should be thrownBy SpiLoader.get[MissingSpi]\n  }\n\n  it should \"throw an exception if the module is missing\" in {\n    a[ClassNotFoundException] should be thrownBy SpiLoader.get[MissingModule]\n  }\n\n  it should \"throw an exception if the config key is missing\" in {\n    a[ConfigException] should be thrownBy SpiLoader.get[MissingKey]\n  }\n}\n\ntrait SimpleSpi extends Spi\nobject SimpleSpiImpl extends SimpleSpi\n\ntrait MissingSpi extends Spi\ntrait MissingModule extends Spi\ntrait MissingKey extends Spi\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneApiGwTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport com.google.common.base.Stopwatch\nimport common.{FreePortFinder, WskProps}\nimport org.apache.openwhisk.core.cli.test.ApiGwRestTests\nimport org.apache.openwhisk.utils.retry\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass StandaloneApiGwTests extends ApiGwRestTests with StandaloneServerFixture {\n  override implicit val wskprops = WskProps().copy(apihost = serverUrl)\n\n  override protected def extraArgs: Seq[String] = Seq(\"--api-gw\", \"--api-gw-port\", FreePortFinder.freePort().toString)\n\n  override protected def waitForOtherThings(): Unit = {\n    val w = Stopwatch.createStarted()\n    retry({\n      println(s\"Waiting for route management actions to be installed since $w\")\n      require(logLines.exists(_.contains(\"Installed Route Management Actions\")))\n    }, 30, Some(500.millis))\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneCouchTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport common.WskProps\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport system.basic.WskRestBasicTests\n\n@RunWith(classOf[JUnitRunner])\nclass StandaloneCouchTests extends WskRestBasicTests with StandaloneServerFixture with StandaloneSanityTestSupport {\n  override implicit val wskprops = WskProps().copy(apihost = serverUrl)\n\n  override protected def extraArgs: Seq[String] =\n    Seq(\"--couchdb\")\n\n  override protected def extraVMArgs: Seq[String] = Seq(\"-Dwhisk.standalone.couchdb.volumes-enabled=false\")\n\n  //This is more of a sanity test. So just run one of the test which trigger interaction with couchdb\n  //and skip running all other tests\n  override protected def supportedTests: Set[String] =\n    Set(\"Wsk Action REST should create, update, get and list an action\")\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneKCFTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.nio.charset.StandardCharsets.UTF_8\nimport java.nio.file.Files\n\nimport common.WskProps\nimport org.apache.commons.io.FileUtils\nimport org.apache.openwhisk.core.containerpool.kubernetes.test.KubeClientSupport\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport system.basic.WskRestBasicTests\n\n@RunWith(classOf[JUnitRunner])\nclass StandaloneKCFTests\n    extends WskRestBasicTests\n    with StandaloneServerFixture\n    with StandaloneSanityTestSupport\n    with KubeClientSupport {\n  override implicit val wskprops = WskProps().copy(apihost = serverUrl)\n\n  //Turn on to debug locally easily\n  override protected val dumpLogsAlways = false\n\n  override protected val dumpStartupLogs = false\n\n  override protected def useMockServer = false\n\n  // override protected def supportedTests = Set(\"Wsk Action REST should invoke a blocking action and get only the result\") // disabled; very frequently fails in travis-ci because after 60 seconds we return a 202 with the activation id.  KCF often gets cold starts > 60 seconds in travis environment\n  override protected def supportedTests = Set()\n\n  override protected def extraArgs: Seq[String] = Seq(\"--dev-mode\", \"--dev-kcf\")\n\n  private val podTemplate = \"\"\"---\n                              |apiVersion: \"v1\"\n                              |kind: \"Pod\"\n                              |metadata:\n                              |  annotations:\n                              |    allow-outbound : \"true\"\n                              |  labels:\n                              |     launcher: standalone\"\"\".stripMargin\n\n  private val podTemplateFile = Files.createTempFile(\"whisk\", null).toFile\n\n  override val customConfig = {\n    FileUtils.write(podTemplateFile, podTemplate, UTF_8)\n    Some(s\"\"\"include classpath(\"standalone-kcf.conf\")\n         |\n         |whisk {\n         |  kubernetes {\n         |    pod-template = \"${podTemplateFile.toURI}\"\n         |  }\n         |}\"\"\".stripMargin)\n  }\n\n  override def afterAll(): Unit = {\n    checkPodState()\n    super.afterAll()\n    podTemplateFile.delete()\n  }\n\n  def checkPodState(): Unit = {\n    val podList = kubeClient.pods().withLabel(\"launcher\").list()\n    podList.getItems.isEmpty shouldBe false\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneKafkaTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport common.WskProps\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport system.basic.WskRestBasicTests\n\n@RunWith(classOf[JUnitRunner])\nclass StandaloneKafkaTests extends WskRestBasicTests with StandaloneServerFixture with StandaloneSanityTestSupport {\n  override implicit val wskprops = WskProps().copy(apihost = serverUrl)\n\n  override protected def supportedTests: Set[String] =\n    Set(\"Wsk Action REST should invoke a blocking action and get only the result\")\n\n  override protected def extraArgs: Seq[String] = Seq(\"--dev-mode\", \"--kafka\")\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneSanityTestSupport.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport org.scalatest.{Canceled, Outcome, TestSuite}\n\ntrait StandaloneSanityTestSupport extends TestSuite {\n\n  protected def supportedTests: Set[String]\n\n  override def withFixture(test: NoArgTest): Outcome = {\n    if (supportedTests.contains(test.name)) {\n      super.withFixture(test)\n    } else {\n      Canceled()\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerFixture.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport java.io.File\nimport java.net.URI\nimport java.nio.charset.StandardCharsets.UTF_8\n\nimport com.google.common.base.Stopwatch\nimport common.WhiskProperties.WHISK_SERVER\nimport common.{FreePortFinder, StreamLogging, WhiskProperties, Wsk}\nimport io.restassured.RestAssured\nimport org.apache.commons.io.{FileUtils, FilenameUtils}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.utils.retry\nimport org.scalatest.{BeforeAndAfterAll, Suite, TestSuite}\n\nimport scala.collection.mutable.ListBuffer\nimport scala.concurrent.duration._\nimport scala.sys.process._\nimport scala.util.control.NonFatal\n\ntrait StandaloneServerFixture extends TestSuite with BeforeAndAfterAll with StreamLogging {\n  self: Suite =>\n\n  private val jarPathProp = \"whisk.server.jar\"\n  private var serverProcess: Process = _\n  protected val serverPort: Int = FreePortFinder.freePort()\n  protected var serverUrl: String = System.getProperty(WHISK_SERVER, s\"http://localhost:$serverPort/\")\n  private val disablePullConfig = \"whisk.docker.standalone.container-factory.pull-standard-images\"\n  private var serverStartedForTest = false\n  private val tempFiles = ListBuffer[File]()\n\n  private val whiskServerPreDefined = System.getProperty(WHISK_SERVER) != null\n\n  protected def extraArgs: Seq[String] = Seq.empty\n  protected def extraVMArgs: Seq[String] = Seq.empty\n  protected def customConfig: Option[String] = None\n\n  protected def waitForOtherThings(): Unit = {}\n\n  protected def dumpLogsAlways: Boolean = false\n\n  protected def dumpStartupLogs: Boolean = false\n\n  protected def disablePlayGround: Boolean = true\n\n  protected val dataDirPath: String = FilenameUtils.concat(FileUtils.getTempDirectoryPath, \"standalone\")\n\n  override def beforeAll(): Unit = {\n    val serverUrlViaSysProp = Option(System.getProperty(WHISK_SERVER))\n    serverUrlViaSysProp match {\n      case Some(u) =>\n        serverUrl = u\n        println(s\"Connecting to existing server at $serverUrl\")\n      case None =>\n        System.setProperty(WHISK_SERVER, serverUrl)\n        super.beforeAll()\n        println(s\"Running standalone server from ${standaloneServerJar.getAbsolutePath}\")\n        val pgArgs = if (disablePlayGround) Seq(\"--no-ui\") else Seq.empty\n        val args = Seq(\n          Seq(\n            \"java\",\n            //For tests let it bound on all ip to make it work on travis which uses linux\n            \"-Dwhisk.controller.interface=0.0.0.0\",\n            s\"-Dwhisk.standalone.wsk=${Wsk.defaultCliPath}\",\n            s\"-D$disablePullConfig=false\")\n            ++ extraVMArgs\n            ++ Seq(\"-jar\", standaloneServerJar.getAbsolutePath, \"--disable-color-logging\", \"--data-dir\", dataDirPath)\n            ++ configFileOpts\n            ++ manifestFileOpts\n            ++ pgArgs\n            ++ extraArgs,\n          Seq(\"-p\", serverPort.toString)).flatten\n\n        serverProcess = args.run(ProcessLogger(s => printstream.println(s)))\n        val w = waitForServerToStart()\n        serverStartedForTest = true\n        println(s\"Started test server at $serverUrl in [$w]\")\n        waitForOtherThings()\n        if (dumpStartupLogs) {\n          println(logLines.mkString(\"\\n\"))\n        }\n    }\n  }\n\n  override def afterAll(): Unit = {\n    super.afterAll()\n    if (!whiskServerPreDefined) {\n      System.clearProperty(WHISK_SERVER)\n    }\n    if (serverStartedForTest) {\n      serverProcess.destroy()\n      FileUtils.forceDelete(new File(dataDirPath))\n      tempFiles.foreach(FileUtils.deleteQuietly)\n    }\n  }\n\n  override def withFixture(test: NoArgTest) = {\n    val outcome = super.withFixture(test)\n    if (outcome.isFailed || (outcome.isSucceeded && dumpLogsAlways)) {\n      println(logLines.mkString(\"\\n\"))\n    }\n    stream.reset()\n    outcome\n  }\n\n  def waitForServerToStart(): Stopwatch = {\n    val w = Stopwatch.createStarted()\n    try {\n      retry({\n        println(s\"Waiting for OpenWhisk server to start since $w\")\n        val response = RestAssured.get(new URI(serverUrl))\n        require(response.statusCode() == 200)\n      }, 60, Some(1.second))\n    } catch {\n      case NonFatal(e) =>\n        println(logLines.mkString(\"\\n\"))\n        throw e\n    }\n    w\n  }\n\n  private def configFileOpts: Seq[String] = {\n    customConfig\n      .map(fileOpt(\"-c\", _))\n      .getOrElse(Seq.empty)\n  }\n\n  private def manifestFileOpts: Seq[String] = {\n    Option(WhiskProperties.getProperty(WhiskConfig.runtimesManifest))\n      .map(fileOpt(\"-m\", _))\n      .getOrElse(Seq.empty)\n  }\n\n  private def fileOpt(optName: String, content: String): Seq[String] = {\n    val f = File.createTempFile(\"whisktest\", null, null)\n    tempFiles += f\n    FileUtils.write(f, content, UTF_8)\n    Seq(optName, f.getAbsolutePath)\n  }\n\n  private def standaloneServerJar: File = {\n    Option(System.getProperty(jarPathProp)) match {\n      case Some(p) =>\n        val jarFile = new File(p)\n        assert(\n          jarFile.canRead,\n          s\"OpenWhisk standalone server jar file [$p] specified via system property [$jarPathProp] not found\")\n        jarFile\n      case None =>\n        fail(s\"No jar file specified via system property [$jarPathProp]\")\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport common.WskProps\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport system.basic.WskRestBasicTests\n\n@RunWith(classOf[JUnitRunner])\nclass StandaloneServerTests extends WskRestBasicTests with StandaloneServerFixture {\n  override implicit val wskprops = WskProps().copy(apihost = serverUrl)\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneUserEventTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.standalone\n\nimport common.{FreePortFinder, WskProps}\nimport org.apache.openwhisk.common.UserEventTests\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\n@RunWith(classOf[JUnitRunner])\nclass StandaloneUserEventTests extends UserEventTests with StandaloneServerFixture {\n  private val kafkaPort = sys.props.get(\"whisk.kafka.port\").map(_.toInt).getOrElse(FreePortFinder.freePort())\n\n  protected override val customConfig = Some(\"\"\"\n      |include classpath(\"standalone.conf\")\n      |whisk {\n      |  user-events {\n      |    enabled = true\n      |  }\n      |}\n      \"\"\".stripMargin)\n\n  override protected def extraArgs: Seq[String] =\n    Seq(\"--kafka\", \"--dev-mode\", \"--kafka-port\", kafkaPort.toString)\n\n  override implicit val wskprops = WskProps().copy(apihost = serverUrl)\n\n  override def userEventsEnabled = true\n\n  override def kafkaHosts = s\"localhost:$kafkaPort\"\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/test/http/RESTProxy.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.test.http\n\nimport scala.concurrent.Await\nimport scala.concurrent.duration.DurationInt\nimport scala.concurrent.duration.FiniteDuration\n\nimport org.apache.pekko.actor.Actor\nimport org.apache.pekko.actor.ActorLogging\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.model._\nimport org.apache.pekko.http.scaladsl.model.headers._\nimport org.apache.pekko.stream.scaladsl._\nimport org.apache.pekko.pattern.ask\nimport org.apache.pekko.pattern.pipe\nimport org.apache.pekko.util.Timeout\n\nobject RESTProxy {\n  // Orders the proxy to immediately unbind and rebind after the duration has passed.\n  case class UnbindFor(duration: FiniteDuration)\n}\n\n/**\n * A simple REST proxy that can receive commands to change its behavior (e.g.\n * simulate failures of the proxied service). Not for use in production.\n */\nclass RESTProxy(val host: String, val port: Int)(val serviceAuthority: Uri.Authority, val useHTTPS: Boolean)\n    extends Actor\n    with ActorLogging {\n  private implicit val actorSystem = context.system\n  private implicit val executionContex = actorSystem.dispatcher\n\n  private val destHost = serviceAuthority.host.address\n  private val destPort = serviceAuthority.port\n\n  // These change as connections come and go\n  private var binding: Option[Http.ServerBinding] = None\n\n  // Public messages\n  import RESTProxy._\n\n  // Internal messages\n  private case object DoBind\n  private case object DoUnbind\n  private case class Request(request: HttpRequest)\n\n  // Route requests through messages to this actor, to serialize w.r.t events such as unbinding\n  private def mkRequestFlow: Flow[HttpRequest, HttpResponse, _] = {\n\n    Flow.apply[HttpRequest].mapAsync(4) { request =>\n      ask(self, Request(request))(timeout = Timeout(1.minute)).mapTo[HttpResponse]\n    }\n  }\n\n  private def bind(checkState: Boolean = true): Unit = {\n    assert(!checkState || binding.isEmpty, \"Proxy is already bound\")\n\n    if (binding.isEmpty) {\n      log.debug(s\"[RESTProxy] Binding to '$host:$port'.\")\n      val b = Await.result(Http().newServerAt(host, port).bindFlow(mkRequestFlow), 5.seconds)\n      binding = Some(b)\n    }\n  }\n\n  private def unbind(checkState: Boolean = true): Unit = {\n    assert(!checkState || binding.isDefined, \"Proxy is not bound\")\n\n    binding.foreach { b =>\n      log.debug(s\"[RESTProxy] Unbinding from '${b.localAddress}'\")\n      Await.result(b.unbind(), 5.seconds)\n      binding = None\n    }\n  }\n\n  override def preStart() = bind()\n\n  override def postStop() = unbind(checkState = false)\n\n  override def receive = {\n    case UnbindFor(d) =>\n      self ! DoUnbind\n      actorSystem.scheduler.scheduleOnce(d, self, DoBind)\n\n    case DoUnbind =>\n      unbind(checkState = false)\n\n    case DoBind =>\n      bind(checkState = false)\n\n    case Request(request) =>\n      // If the actor isn't bound to the port / has no materializer,\n      // the request is simply dropped.\n\n      log.debug(s\"[RESTProxy] Proxying '${request.uri}' to '${serviceAuthority}'\")\n\n      val flow = if (useHTTPS) {\n        Http().outgoingConnectionHttps(destHost, destPort)\n      } else {\n        Http().outgoingConnection(destHost, destPort)\n      }\n\n      // pekko-http doesn't like us to set those headers ourselves.\n      val upstreamRequest = request.withHeaders(headers = request.headers.filter(_ match {\n        case `Timeout-Access`(_) => false\n        case _                   => true\n      }))\n\n      Source\n        .single(upstreamRequest)\n        .via(flow)\n        .runWith(Sink.head)\n        .pipeTo(sender)\n\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/org/apache/openwhisk/utils/test/ExecutionContextFactoryTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.utils.test\n\nimport scala.concurrent.Await\nimport scala.concurrent.Future\nimport scala.concurrent.duration.DurationInt\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.WskActorSystem\nimport org.apache.openwhisk.utils.ExecutionContextFactory.FutureExtensions\n\n@RunWith(classOf[JUnitRunner])\nclass ExecutionContextFactoryTests extends AnyFlatSpec with Matchers with WskActorSystem {\n\n  behavior of \"future extensions\"\n\n  it should \"take first to complete\" in {\n    val f1 = Future.successful({}).withTimeout(500.millis, new Throwable(\"error\"))\n    Await.result(f1, 1.second) shouldBe ({})\n\n    val failure = new Throwable(\"error\")\n    val f2 = Future { Thread.sleep(1.second.toMillis) }.withTimeout(500.millis, failure)\n    a[Throwable] shouldBe thrownBy { Await.result(f2, 1.seconds) }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/services/HeadersTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage services\n\nimport scala.concurrent.Future\nimport scala.concurrent.duration.DurationInt\nimport scala.language.postfixOps\nimport scala.collection.immutable.Seq\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.time.Span.convertDurationToSpan\nimport common.TestUtils\nimport common.WhiskProperties\nimport common.rest.{HttpConnection, WskRestOperations}\nimport common.WskProps\nimport common.WskTestHelpers\nimport org.apache.pekko.http.scaladsl.model.Uri\nimport org.apache.pekko.http.scaladsl.model.Uri.Path\nimport org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials\nimport org.apache.pekko.http.scaladsl.model.HttpRequest\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Accepted\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.OK\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.DELETE\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.GET\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.POST\nimport org.apache.pekko.http.scaladsl.model.HttpMethods.PUT\nimport org.apache.pekko.http.scaladsl.model.HttpMethods._\nimport org.apache.pekko.http.scaladsl.Http\nimport org.apache.pekko.http.scaladsl.model.HttpResponse\nimport org.apache.pekko.http.scaladsl.model.headers._\nimport org.apache.pekko.http.scaladsl.model.HttpMethod\nimport org.apache.pekko.http.scaladsl.model.HttpHeader\nimport common.WskActorSystem\nimport pureconfig._\n\n@RunWith(classOf[JUnitRunner])\nclass HeadersTests extends AnyFlatSpec with Matchers with ScalaFutures with WskActorSystem with WskTestHelpers {\n\n  behavior of \"Headers at general API\"\n\n  val controllerProtocol = loadConfigOrThrow[String](\"whisk.controller.protocol\")\n  val whiskAuth = WhiskProperties.getBasicAuth\n  val creds = BasicHttpCredentials(whiskAuth.fst, whiskAuth.snd)\n  val allMethods = Some(Set(DELETE.name, GET.name, POST.name, PUT.name))\n  val allowOrigin = `Access-Control-Allow-Origin`.*\n  val allowHeaders = `Access-Control-Allow-Headers`(\n    \"Authorization\",\n    \"Origin\",\n    \"X-Requested-With\",\n    \"Content-Type\",\n    \"Accept\",\n    \"User-Agent\")\n  val url = Uri(s\"$controllerProtocol://${WhiskProperties.getBaseControllerAddress()}\")\n\n  def request(method: HttpMethod, uri: Uri, headers: Option[Seq[HttpHeader]] = None): Future[HttpResponse] = {\n    val httpRequest = headers match {\n      case Some(headers) => HttpRequest(method, uri, headers)\n      case None          => HttpRequest(method, uri)\n    }\n\n    val connectionContext = HttpConnection.getContext(controllerProtocol)\n    Http().singleRequest(httpRequest, connectionContext = connectionContext)\n  }\n\n  implicit val config = PatienceConfig(10 seconds, 100 milliseconds)\n\n  val basePath = Path(\"/api/v1\")\n  implicit val wskprops = WskProps()\n  val wsk = new WskRestOperations\n\n  /**\n   * Checks, if the required headers are in the list of all headers.\n   * For the allowed method, it checks, if only the allowed methods are in the response headers.\n   */\n  def containsHeaders(headers: Seq[HttpHeader], allowedMethods: Option[Set[String]] = None) = {\n    headers should contain allOf (allowOrigin, allowHeaders)\n\n    // TODO: commented out for now as allowed methods are not supported currently\n    //        val headersMap = headers map { header =>\n    //            header.name -> header.value.split(\",\").map(_.trim).toSet\n    //        } toMap\n    //        allowedMethods map { allowedMethods =>\n    //            headersMap should contain key \"Access-Control-Allow-Methods\"\n    //            headersMap(\"Access-Control-Allow-Methods\") should contain theSameElementsAs (allowedMethods)\n    //        }\n  }\n\n  it should \"respond to OPTIONS with all headers\" in {\n    request(OPTIONS, url.withPath(basePath)).futureValue.headers should contain allOf (allowOrigin, allowHeaders)\n  }\n\n  ignore should \"not respond to OPTIONS for non existing path\" in {\n    val path = basePath / \"foo\" / \"bar\"\n\n    request(OPTIONS, url.withPath(path)).futureValue.status should not be OK\n  }\n\n  // Actions\n  it should \"respond to OPTIONS for listing actions\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"actions\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"GET\")))\n  }\n\n  it should \"respond to OPTIONS for actions path\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"actions\" / \"foobar\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, allMethods)\n  }\n\n  it should \"respond to POST action with headers\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val packageName = \"samples\"\n    val actionName = \"helloWorld\"\n    val fullActionName = s\"$packageName/$actionName\"\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, shared = Some(true))\n    }\n\n    assetHelper.withCleaner(wsk.action, fullActionName) { (action, _) =>\n      action.create(fullActionName, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n    val path = basePath / \"namespaces\" / \"_\" / \"actions\" / packageName / actionName\n    val response = request(POST, url.withPath(path), Some(List(Authorization(creds)))).futureValue\n\n    response.status shouldBe Accepted\n    containsHeaders(response.headers)\n  }\n\n  // Activations\n  it should \"respond to OPTIONS for listing activations\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"activations\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"GET\")))\n  }\n\n  it should \"respond to OPTIONS for activations get\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"activations\" / \"foobar\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"GET\")))\n  }\n\n  it should \"respond to OPTIONS for activations logs\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"activations\" / \"foobar\" / \"logs\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"GET\")))\n  }\n\n  it should \"respond to OPTIONS for activations results\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"activations\" / \"foobar\" / \"result\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"GET\")))\n  }\n\n  it should \"respond to GET for listing activations with Headers\" in {\n    val path = basePath / \"namespaces\" / \"_\" / \"activations\"\n    val response = request(GET, url.withPath(path), Some(List(Authorization(creds)))).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers)\n  }\n\n  // Namespaces\n  it should \"respond to OPTIONS for listing namespaces\" in {\n    val path = basePath / \"namespaces\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"GET\")))\n  }\n\n  // Packages\n  it should \"respond to OPTIONS for listing packages\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"packages\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"GET\")))\n  }\n\n  it should \"respond to OPTIONS for packages path\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"packages\" / \"foobar\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"DELETE\", \"GET\", \"PUT\")))\n  }\n\n  it should \"respond to GET for listing packages with headers\" in {\n    val path = basePath / \"namespaces\" / \"_\" / \"packages\"\n    val response = request(GET, url.withPath(path), Some(List(Authorization(creds)))).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers)\n  }\n\n  // Rules\n  it should \"respond to OPTIONS for listing rules\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"rules\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"GET\")))\n  }\n\n  it should \"respond to OPTIONS for rules path\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"rules\" / \"foobar\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, allMethods)\n  }\n\n  it should \"respond to GET for listing rules with headers\" in {\n    val path = basePath / \"namespaces\" / \"_\" / \"rules\"\n    val response = request(GET, url.withPath(path), Some(List(Authorization(creds)))).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers)\n  }\n\n  // Triggers\n  it should \"respond to OPTIONS for listing triggers\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"triggers\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, Some(Set(\"GET\")))\n  }\n\n  it should \"respond to OPTIONS for triggers path\" in {\n    val path = basePath / \"namespaces\" / \"barfoo\" / \"triggers\" / \"foobar\"\n    val response = request(OPTIONS, url.withPath(path)).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers, allMethods)\n  }\n\n  it should \"respond to GET for listing triggers with headers\" in {\n    val path = basePath / \"namespaces\" / \"_\" / \"triggers\"\n    val response = request(GET, url.withPath(path), Some(List(Authorization(creds)))).futureValue\n\n    response.status shouldBe OK\n    containsHeaders(response.headers)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/services/KafkaConnectorTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage services\n\nimport java.io.File\nimport java.nio.charset.StandardCharsets\nimport java.util.Calendar\n\nimport common.{StreamLogging, TestUtils, WhiskProperties, WskActorSystem}\nimport ha.ShootComponentUtils\nimport org.apache.kafka.clients.consumer.CommitFailedException\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.apache.openwhisk.common.TransactionId\nimport org.apache.openwhisk.connector.kafka.{KafkaConsumerConnector, KafkaMessagingProvider, KafkaProducerConnector}\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.core.connector.Message\nimport org.apache.openwhisk.utils.{retry, ExecutionContextFactory}\n\nimport scala.concurrent.duration.{DurationInt, FiniteDuration}\nimport scala.concurrent.{Await, ExecutionContext}\nimport scala.language.postfixOps\nimport scala.util.Try\n\n@RunWith(classOf[JUnitRunner])\nclass KafkaConnectorTests\n    extends AnyFlatSpec\n    with Matchers\n    with WskActorSystem\n    with BeforeAndAfterAll\n    with StreamLogging\n    with ShootComponentUtils {\n  implicit val transid: TransactionId = TransactionId.testing\n  implicit val ec: ExecutionContext = ExecutionContextFactory.makeCachedThreadPoolExecutionContext()\n\n  val config = new WhiskConfig(WhiskConfig.kafkaHosts)\n  assert(config.isValid)\n\n  val groupid = \"kafkatest\"\n  val topic = \"KafkaConnectorTestTopic\"\n  val maxPollInterval = 10.seconds\n  System.setProperty(\"whisk.kafka.consumer.max-poll-interval-ms\", maxPollInterval.toMillis.toString)\n\n  // Need to overwrite replication factor for tests that shut down and start\n  // Kafka instances intentionally. These tests will fail if there is more than\n  // one Kafka host but a replication factor of 1.\n  val kafkaHosts: Array[String] = config.kafkaHosts.split(\",\")\n  val replicationFactor: Int = kafkaHosts.length / 2 + 1\n  System.setProperty(\"whisk.kafka.replication-factor\", replicationFactor.toString)\n\n  println(s\"Create test topic '$topic' with replicationFactor=$replicationFactor\")\n  KafkaMessagingProvider.ensureTopic(config, topic, topic) shouldBe 'success\n\n  val producer = new KafkaProducerConnector(config.kafkaHosts)\n  val consumer = new KafkaConsumerConnector(config.kafkaHosts, groupid, topic)\n\n  override def afterAll(): Unit = {\n    producer.close()\n    consumer.close()\n    super.afterAll()\n  }\n\n  def commandComponent(host: String, command: String, component: String): TestUtils.RunResult = {\n    def file(path: String) = Try(new File(path)).filter(_.exists).map(_.getAbsolutePath).toOption\n    val docker = (file(\"/usr/bin/docker\") orElse file(\"/usr/local/bin/docker\")).getOrElse(\"docker\")\n    val dockerPort = WhiskProperties.getProperty(WhiskConfig.dockerPort)\n    val cmd = Seq(docker, \"--host\", host + \":\" + dockerPort, command, component)\n\n    TestUtils.runCmd(0, new File(\".\"), cmd: _*)\n  }\n\n  def sendAndReceiveMessage(message: Message,\n                            waitForSend: FiniteDuration,\n                            waitForReceive: FiniteDuration): Iterable[String] = {\n    retry {\n      val start = java.lang.System.currentTimeMillis\n      println(s\"Send message to topic.\")\n      val sent = Await.result(producer.send(topic, message), waitForSend)\n      println(s\"Successfully sent message to topic: $sent\")\n      println(s\"Receiving message from topic.\")\n      val received =\n        consumer.peek(waitForReceive).map { case (_, _, _, msg) => new String(msg, StandardCharsets.UTF_8) }\n      val end = java.lang.System.currentTimeMillis\n      val elapsed = end - start\n      println(s\"Received ${received.size}. Took $elapsed msec: $received\")\n\n      received.last should be(message.serialize)\n      received\n    }\n  }\n\n  def createMessage(): Message = new Message { override val serialize: String = Calendar.getInstance.getTime.toString }\n\n  behavior of \"Kafka connector\"\n\n  it should \"send and receive a kafka message which sets up the topic\" in {\n    for (i <- 0 until 5) {\n      val message = createMessage()\n      val received = sendAndReceiveMessage(message, 20 seconds, 10 seconds)\n      received.size should be >= 1\n      consumer.commit()\n    }\n  }\n\n  it should \"send and receive a kafka message even after session timeout\" in {\n    // \"clear\" the topic so there are 0 messages to be read\n    sendAndReceiveMessage(createMessage(), 1 seconds, 1 seconds)\n    consumer.commit()\n\n    (1 to 2).foreach { i =>\n      val message = createMessage()\n      val received = sendAndReceiveMessage(message, 1 seconds, 1 seconds)\n      received.size shouldBe i // should accumulate since the commits fail\n\n      Thread.sleep((maxPollInterval + 1.second).toMillis)\n      a[CommitFailedException] should be thrownBy consumer.commit()\n    }\n\n    val message3 = createMessage()\n    val received3 = sendAndReceiveMessage(message3, 1 seconds, 1 seconds)\n    received3.size shouldBe 2 + 1 // since the last commit still failed\n    consumer.commit()\n\n    val message4 = createMessage()\n    val received4 = sendAndReceiveMessage(message4, 1 seconds, 1 seconds)\n    received4.size shouldBe 1\n    consumer.commit()\n  }\n\n  if (kafkaHosts.length > 1) {\n    it should \"send and receive a kafka message even after shutdown one of instances\" in {\n      kafkaHosts.indices.foreach { i =>\n        val message = createMessage()\n        val kafkaHost = kafkaHosts(i).split(\":\")(0)\n        val startLog = s\"\\\\[KafkaServer id=$i\\\\] started\"\n        val prevCount = startLog.r.findAllMatchIn(commandComponent(kafkaHost, \"logs\", s\"kafka$i\").stdout).length\n\n        // 1. stop one of kafka node\n        stopComponent(kafkaHost, s\"kafka$i\")\n\n        // 2. kafka cluster should be ok at least after three retries\n        retry({\n          val received = sendAndReceiveMessage(message, 40 seconds, 40 seconds)\n          received.size should be >= 1\n        }, 3, Some(100.milliseconds))\n        consumer.commit()\n\n        // 3. recover stopped node\n        startComponent(kafkaHost, s\"kafka$i\")\n\n        // 4. wait until kafka is up\n        retry({\n          startLog.r\n            .findAllMatchIn(commandComponent(kafkaHost, \"logs\", s\"kafka$i\").stdout)\n            .length shouldBe prevCount + 1\n        }, 20, Some(1.second))\n\n        // 5. kafka cluster should be ok at least after three retires\n        retry({\n          val received = sendAndReceiveMessage(message, 40 seconds, 40 seconds)\n          received.size should be >= 1\n        }, 3, Some(100.milliseconds))\n        consumer.commit()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskActionTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport java.io.File\nimport java.nio.charset.StandardCharsets\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common._\nimport common.rest.WskRestOperations\nimport org.apache.openwhisk.core.entity.Annotations\nimport org.apache.commons.io.FileUtils\nimport org.apache.openwhisk.core.FeatureFlags\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n@RunWith(classOf[JUnitRunner])\nclass WskActionTests extends TestHelpers with WskTestHelpers with JsHelpers with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  // wsk must have type WskOperations so that tests using CLI (class Wsk)\n  // instead of REST (WskRestOperations) still work.\n  val wsk: WskOperations = new WskRestOperations\n\n  val testString = \"this is a test\"\n  val testResult = JsObject(\"count\" -> testString.split(\" \").length.toJson)\n  val guestNamespace = wskprops.namespace\n\n  behavior of \"Whisk actions\"\n\n  it should \"create an action with an empty file\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"empty\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"empty.js\")))\n    }\n  }\n\n  it should \"invoke an action returning a promise\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"hello promise\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"helloPromise.js\")))\n    }\n\n    val run = wsk.action.invoke(name)\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"done\" -> true.toJson))\n      activation.logs.get.mkString(\" \") shouldBe empty\n    }\n  }\n\n  it should \"invoke an action with a space in the name\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"hello Async\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"helloAsync.js\")))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(testResult)\n      activation.logs.get.mkString(\" \") should include(testString)\n    }\n  }\n\n  it should \"invoke an action that throws an uncaught exception and returns correct status code\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val name = \"throwExceptionAction\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"runexception.js\")))\n    }\n\n    withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>\n      val response = activation.response\n      activation.response.status shouldBe \"action developer error\"\n      activation.response.result shouldBe Some(\n        JsObject(\"error\" -> \"An error has occurred: Extraordinary exception\".toJson))\n    }\n  }\n\n  it should \"pass parameters bound on creation-time to the action\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"printParams\"\n    val params = Map(\"param1\" -> \"test1\", \"param2\" -> \"test2\")\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(\n        name,\n        Some(TestUtils.getTestActionFilename(\"printParams.js\")),\n        parameters = params.mapValues(_.toJson).toMap)\n    }\n\n    val invokeParams = Map(\"payload\" -> testString)\n    val run = wsk.action.invoke(name, invokeParams.mapValues(_.toJson).toMap)\n    withActivation(wsk.activation, run) { activation =>\n      val logs = activation.logs.get.mkString(\" \")\n\n      (params ++ invokeParams).foreach {\n        case (key, value) =>\n          logs should include(s\"params.$key: $value\")\n      }\n    }\n  }\n\n  it should \"copy an action and invoke it successfully\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"copied\"\n    val packageName = \"samples\"\n    val actionName = \"wordcount\"\n    val fullQualifiedName = s\"/$guestNamespace/$packageName/$actionName\"\n\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, shared = Some(true))\n    }\n\n    assetHelper.withCleaner(wsk.action, fullQualifiedName) {\n      val file = Some(TestUtils.getTestActionFilename(\"wc.js\"))\n      (action, _) =>\n        action.create(fullQualifiedName, file)\n    }\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(fullQualifiedName), Some(\"copy\"))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(testResult)\n      activation.logs.get.mkString(\" \") should include(testString)\n    }\n  }\n\n  it should \"copy an action and ensure exec, parameters, and annotations copied\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val origActionName = \"origAction\"\n      val copiedActionName = \"copiedAction\"\n      val params = Map(\"a\" -> \"A\".toJson)\n      val annots = Map(\"b\" -> \"B\".toJson)\n\n      assetHelper.withCleaner(wsk.action, origActionName) {\n        val file = Some(TestUtils.getTestActionFilename(\"wc.js\"))\n        (action, _) =>\n          action.create(origActionName, file, parameters = params, annotations = annots)\n      }\n\n      assetHelper.withCleaner(wsk.action, copiedActionName) { (action, _) =>\n        action.create(copiedActionName, Some(origActionName), Some(\"copy\"))\n      }\n\n      val copiedAction = wsk.parseJsonString(wsk.action.get(copiedActionName).stdout)\n      val origAction = wsk.parseJsonString(wsk.action.get(copiedActionName).stdout)\n\n      copiedAction.fields(\"annotations\") shouldBe origAction.fields(\"annotations\")\n      copiedAction.fields(\"parameters\") shouldBe origAction.fields(\"parameters\")\n      copiedAction.fields(\"exec\") shouldBe origAction.fields(\"exec\")\n      copiedAction.fields(\"version\") shouldBe JsString(\"0.0.1\")\n  }\n\n  it should \"add new parameters and annotations while copying an action\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val runtime = \"nodejs:default\"\n      val origName = \"origAction\"\n      val copiedName = \"copiedAction\"\n      val origParams = Map(\"origParam1\" -> \"origParamValue1\".toJson, \"origParam2\" -> 999.toJson)\n      val copiedParams = Map(\"copiedParam1\" -> \"copiedParamValue1\".toJson, \"copiedParam2\" -> 123.toJson)\n      val origAnnots = Map(\"origAnnot1\" -> \"origAnnotValue1\".toJson, \"origAnnot2\" -> true.toJson)\n      val copiedAnnots = Map(\"copiedAnnot1\" -> \"copiedAnnotValue1\".toJson, \"copiedAnnot2\" -> false.toJson)\n      val resParams = Seq(\n        JsObject(\"key\" -> JsString(\"copiedParam1\"), \"value\" -> JsString(\"copiedParamValue1\")),\n        JsObject(\"key\" -> JsString(\"copiedParam2\"), \"value\" -> JsNumber(123)),\n        JsObject(\"key\" -> JsString(\"origParam1\"), \"value\" -> JsString(\"origParamValue1\")),\n        JsObject(\"key\" -> JsString(\"origParam2\"), \"value\" -> JsNumber(999)))\n      val baseAnnots = Seq(\n        JsObject(\"key\" -> JsString(\"origAnnot1\"), \"value\" -> JsString(\"origAnnotValue1\")),\n        JsObject(\"key\" -> JsString(\"copiedAnnot2\"), \"value\" -> JsFalse),\n        JsObject(\"key\" -> JsString(\"copiedAnnot1\"), \"value\" -> JsString(\"copiedAnnotValue1\")),\n        JsObject(\"key\" -> JsString(\"origAnnot2\"), \"value\" -> JsTrue),\n        JsObject(\"key\" -> Annotations.ProvideApiKeyAnnotationName.toJson, \"value\" -> JsFalse))\n      val resAnnots: Seq[JsObject] = if (FeatureFlags.requireApiKeyAnnotation) {\n        baseAnnots ++ Seq(JsObject(\"key\" -> Annotations.ProvideApiKeyAnnotationName.toJson, \"value\" -> JsFalse))\n      } else baseAnnots\n\n      assetHelper.withCleaner(wsk.action, origName) {\n        val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n        (action, _) =>\n          action.create(origName, file, parameters = origParams, annotations = origAnnots, kind = Some(runtime))\n      }\n\n      assetHelper.withCleaner(wsk.action, copiedName) { (action, _) =>\n        println(\"created copied \")\n        action.create(copiedName, Some(origName), Some(\"copy\"), parameters = copiedParams, annotations = copiedAnnots)\n      }\n\n      val copiedAction = wsk.parseJsonString(wsk.action.get(copiedName).stdout)\n\n      // first we check the returned execution runtime for 'nodejs:*'\n      copiedAction\n        .fields(\"annotations\")\n        .convertTo[Seq[JsObject]]\n        .find(_.fields(\"key\").convertTo[String] == \"exec\")\n        .map(_.fields(\"value\"))\n        .map(exec => { exec.convertTo[String] should startWith(\"nodejs:\") })\n        .getOrElse(fail())\n\n      // CLI does not guarantee order of annotations and parameters so do a diff to compare the values\n      copiedAction.fields(\"parameters\").convertTo[Seq[JsObject]] diff resParams shouldBe List.empty\n\n      // for the anotations we ignore the exec field here, since we already compared it above\n      copiedAction\n        .fields(\"annotations\")\n        .convertTo[Seq[JsObject]]\n        .filter(annotation => annotation.fields(\"key\").convertTo[String] != \"exec\") diff resAnnots shouldBe List.empty\n\n  }\n\n  it should \"recreate and invoke a new action with different code\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"recreatedAction\"\n    assetHelper.withCleaner(wsk.action, name, false) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"wc.js\")))\n    }\n\n    val run1 = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson))\n    withActivation(wsk.activation, run1) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.logs.get.mkString(\" \") should include(s\"The message '$testString' has\")\n    }\n\n    wsk.action.delete(name)\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    val run2 = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson))\n    withActivation(wsk.activation, run2) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.logs.get.mkString(\" \") should include(s\"hello, $testString\")\n    }\n  }\n\n  it should \"fail to invoke an action with an empty file\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"empty\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"empty.js\")))\n    }\n    val run = wsk.action.invoke(name)\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"action developer error\"\n      activation.response.result shouldBe Some(JsObject(\"error\" -> \"Missing main/no code to execute.\".toJson))\n    }\n  }\n\n  it should \"blocking invoke of nested blocking actions\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"nestedBlockingAction\"\n    val child = \"wc\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      val annotations =\n        if (FeatureFlags.requireApiKeyAnnotation) Map(Annotations.ProvideApiKeyAnnotationName -> JsTrue)\n        else Map.empty[String, JsValue]\n      action.create(name, Some(TestUtils.getTestActionFilename(\"wcbin.js\")), annotations = annotations)\n\n    }\n    assetHelper.withCleaner(wsk.action, child) { (action, _) =>\n      action.create(child, Some(TestUtils.getTestActionFilename(\"wc.js\")))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson), blocking = true)\n    val activation = wsk.parseJsonString(run.stdout).convertTo[ActivationResult]\n\n    withClue(s\"check failed for activation: $activation\") {\n      val wordCount = testString.split(\" \").length\n      activation.response.result.get shouldBe JsObject(\"binaryCount\" -> s\"${wordCount.toBinaryString} (base 2)\".toJson)\n    }\n  }\n\n  it should \"blocking invoke an asynchronous action\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"helloAsync\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"helloAsync.js\")))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> testString.toJson), blocking = true)\n    val activation = wsk.parseJsonString(run.stdout).convertTo[ActivationResult]\n\n    withClue(s\"check failed for activation: $activation\") {\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(testResult)\n      activation.logs shouldBe Some(List.empty)\n    }\n  }\n\n  it should \"not be able to use 'ping' in an action\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"ping\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"ping.js\")))\n    }\n\n    val run = wsk.action.invoke(name, Map(\"payload\" -> \"google.com\".toJson))\n    withActivation(wsk.activation, run) { activation =>\n      val result = activation.response.result.get\n      result.asJsObject.getFields(\"stdout\", \"code\") match {\n        case Seq(JsString(stdout), JsNumber(code)) =>\n          stdout should not include \"bytes from\"\n          code.intValue should not be 0\n        case _ => fail(s\"fields 'stdout' or 'code' where not of the expected format, was $result\")\n      }\n    }\n  }\n\n  it should \"support UTF-8 as input and output format\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"utf8Test\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    val utf8 = \"«ταБЬℓσö»: 1<2 & 4+1>³, now 20%€§$ off!\"\n    val run = wsk.action.invoke(name, Map(\"payload\" -> utf8.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.logs.get.mkString(\" \") should include(s\"hello, $utf8\")\n    }\n  }\n\n  it should \"invoke action with large code\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"big-hello\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      val filePath = TestUtils.getTestActionFilename(\"hello.js\")\n      val code = FileUtils.readFileToString(new File(filePath), StandardCharsets.UTF_8)\n      val largeCode = code + \" \" * (WhiskProperties.getMaxActionSizeMB * FileUtils.ONE_MB).toInt\n      val tmpFile = File.createTempFile(\"whisk\", \".js\")\n      FileUtils.write(tmpFile, largeCode, StandardCharsets.UTF_8)\n      val result = action.create(name, Some(tmpFile.getAbsolutePath))\n      tmpFile.delete()\n      result\n    }\n\n    val hello = \"hello\"\n    val run = wsk.action.invoke(name, Map(\"payload\" -> hello.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.logs.get.mkString(\" \") should include(s\"hello, $hello\")\n    }\n  }\n\n  it should \"not delete existing annotations when updating action with new annotation\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"hello\"\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        val annotations = Map(\"key1\" -> \"value1\".toJson, \"key2\" -> \"value2\".toJson)\n        action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")), annotations = annotations)\n        val annotationString = wsk.parseJsonString(wsk.action.get(name).stdout).fields(\"annotations\").toString\n\n        annotationString should include(\"\"\"\"key\":\"key1\"\"\"\")\n        annotationString should include(\"\"\"\"value\":\"value1\"\"\"\")\n        annotationString should include(\"\"\"\"key\":\"key2\"\"\"\")\n        annotationString should include(\"\"\"\"value\":\"value2\"\"\"\")\n\n        val newAnnotations = Map(\"key3\" -> \"value3\".toJson, \"key4\" -> \"value4\".toJson)\n        action.create(\n          name,\n          Some(TestUtils.getTestActionFilename(\"hello.js\")),\n          annotations = newAnnotations,\n          update = true)\n        val newAnnotationString = wsk.parseJsonString(wsk.action.get(name).stdout).fields(\"annotations\").toString\n\n        newAnnotationString should include(\"\"\"\"key\":\"key1\"\"\"\")\n        newAnnotationString should include(\"\"\"\"value\":\"value1\"\"\"\")\n        newAnnotationString should include(\"\"\"\"key\":\"key2\"\"\"\")\n        newAnnotationString should include(\"\"\"\"value\":\"value2\"\"\"\")\n        newAnnotationString should include(\"\"\"\"key\":\"key3\"\"\"\")\n        newAnnotationString should include(\"\"\"\"value\":\"value3\"\"\"\")\n        newAnnotationString should include(\"\"\"\"key\":\"key4\"\"\"\")\n        newAnnotationString should include(\"\"\"\"value\":\"value4\"\"\"\")\n\n        action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")), update = true)\n      }\n  }\n\n  it should \"invoke an action with a array result\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"helloArray\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"helloArray.js\")))\n    }\n\n    val run = wsk.action.invoke(name)\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(\n        JsArray(JsObject(\"key1\" -> JsString(\"value1\")), JsObject(\"key2\" -> JsString(\"value2\"))))\n    }\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskActivationLogsTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport common._\nimport common.rest.WskRestOperations\nimport org.apache.openwhisk.utils.retry\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration._\n\n@RunWith(classOf[JUnitRunner])\nclass WskActivationLogsTests extends TestHelpers with WskTestHelpers with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  val wsk: WskOperations = new WskRestOperations\n\n  behavior of \"Whisk activation logs\"\n\n  it should \"fetch logs using activation logs API\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"logFetch\"\n    val logFormat = \"\\\\d+-\\\\d+-\\\\d+T\\\\d+:\\\\d+:\\\\d+.\\\\d+Z\\\\s+%s: %s\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"log.js\")))\n    }\n\n    val run = wsk.action.invoke(name)\n\n    // Even though the activation was blocking, the activation itself might not have appeared in the database.\n    withActivation(wsk.activation, run) { activation =>\n      // Needs to be retried because there might be an SPI being plugged in which is handling logs not consistent with\n      // the database where the activation itself comes from (activation in CouchDB, logs in Elasticsearch for\n      // example).\n      retry({\n        val logs = wsk.activation.logs(Some(activation.activationId)).stdout\n\n        logs should include regex logFormat.format(\"stdout\", \"this is stdout\")\n        logs should include regex logFormat.format(\"stderr\", \"this is stderr\")\n      }, 60 * 5, Some(1.second)) // retry for 5 minutes\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskActivationTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport common.rest.WskRestOperations\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common._\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n@RunWith(classOf[JUnitRunner])\nclass WskActivationTests extends TestHelpers with WskTestHelpers with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  val wsk: WskOperations = new WskRestOperations\n\n  behavior of \"Whisk activations\"\n\n  it should \"fetch result using activation result API\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"hello\"\n    val expectedResult = JsObject(\n      \"result\" -> JsObject(\"payload\" -> \"hello, undefined!\".toJson),\n      \"success\" -> true.toJson,\n      \"status\" -> \"success\".toJson)\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    withActivation(wsk.activation, wsk.action.invoke(name)) { activation =>\n      val result = wsk.activation.result(Some(activation.activationId)).stdout.parseJson.asJsObject\n      //Remove size from comparison as its exact value may vary\n      val resultWithoutSize = JsObject(result.fields - \"size\")\n      resultWithoutSize shouldBe expectedResult\n    }\n  }\n\n  it should \"invoke a shared action under a different invocation namespace\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val packageName = \"shared-package\"\n      val actionName = \"echo\"\n      var invocationNamespace = if (wskprops.namespace == \"_\") \"guest\" else wskprops.namespace\n      val packageActionName = s\"/${invocationNamespace}/${packageName}/${actionName}\"\n\n      assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n        pkg.create(packageName, shared = Some(true))(wp)\n      }\n\n      assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>\n        action.create(packageActionName, Some(TestUtils.getTestActionFilename(\"echo.js\")))(wp)\n      }\n\n      withActivation(wsk.activation, wsk.action.invoke(packageActionName)(wp)) { activation =>\n        activation.namespace shouldBe invocationNamespace\n      }(wp)\n\n      val systemId = \"whisk.system\"\n      val wskprops2 = WskProps(authKey = WskAdmin.listKeys(systemId)(0)._1, namespace = systemId)\n      invocationNamespace = if (wskprops2.namespace == \"_\") \"guest\" else wskprops2.namespace\n\n      withActivation(wsk.activation, wsk.action.invoke(packageActionName)(wskprops2)) { activation =>\n        activation.namespace shouldBe invocationNamespace\n      }(wskprops2)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskConductorTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport scala.concurrent.duration.DurationInt\nimport scala.language.postfixOps\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common._\nimport common.rest.WskRestOperations\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.entity.size.SizeInt\nimport org.apache.openwhisk.core.WhiskConfig\nimport org.apache.openwhisk.http.Messages._\n\n@RunWith(classOf[JUnitRunner])\nclass WskConductorTests extends TestHelpers with WskTestHelpers with JsHelpers with StreamLogging with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  val wsk: WskOperations = new WskRestOperations\n\n  val allowedActionDuration = 120 seconds\n\n  val testString = \"this is a test\"\n  val invalid = \"invalid#Action\"\n  val missing = \"missingAction\"\n\n  val whiskConfig = new WhiskConfig(Map(WhiskConfig.actionSequenceMaxLimit -> \"50\"))\n  val limit = whiskConfig.actionSequenceLimit.toInt\n\n  behavior of \"Whisk conductor actions\"\n\n  it should \"invoke a conductor action with no continuation\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val echo = \"echo\" // echo conductor action\n    assetHelper.withCleaner(wsk.action, echo) { (action, _) =>\n      action.create(\n        echo,\n        Some(TestUtils.getTestActionFilename(\"echo.js\")),\n        annotations = Map(\"conductor\" -> true.toJson))\n    }\n\n    // the conductor annotation should not affect the behavior of an action\n    // that returns a dictionary without a params or action field\n    val run = wsk.action.invoke(echo, Map(\"payload\" -> testString.toJson, \"state\" -> testString.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"payload\" -> testString.toJson, \"state\" -> testString.toJson))\n      checkConductorLogsAndAnnotations(activation, 1) // echo\n    }\n\n    // the conductor annotation should not affect the behavior of an action that returns an error\n    val secondrun = wsk.action.invoke(echo, Map(\"error\" -> testString.toJson))\n    withActivation(wsk.activation, secondrun) { activation =>\n      activation.response.status shouldBe \"application error\"\n      activation.response.result shouldBe Some(JsObject(\"error\" -> testString.toJson))\n      checkConductorLogsAndAnnotations(activation, 1) // echo\n    }\n\n    // the controller should unwrap a wrapped result { params: result, ... } for an action with a conductor annotation\n    // discarding other fields if there is no action field\n    val thirdrun = wsk.action.invoke(\n      echo,\n      Map(\n        \"params\" -> JsObject(\"payload\" -> testString.toJson),\n        \"result\" -> testString.toJson,\n        \"state\" -> testString.toJson))\n    withActivation(wsk.activation, thirdrun) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"payload\" -> testString.toJson))\n      checkConductorLogsAndAnnotations(activation, 1) // echo\n    }\n  }\n\n  it should \"invoke a conductor action with an invalid continuation\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val echo = \"echo\" // echo conductor action\n      assetHelper.withCleaner(wsk.action, echo) { (action, _) =>\n        action.create(\n          echo,\n          Some(TestUtils.getTestActionFilename(\"echo.js\")),\n          annotations = Map(\"conductor\" -> true.toJson))\n      }\n\n      // an invalid action name\n      val invalidrun =\n        wsk.action.invoke(echo, Map(\"payload\" -> testString.toJson, \"action\" -> invalid.toJson))\n      withActivation(wsk.activation, invalidrun) { activation =>\n        activation.response.status shouldBe \"application error\"\n        activation.response.result.get.asJsObject.fields.get(\"error\") shouldBe Some(\n          JsString(compositionComponentInvalid(JsString(invalid))))\n        checkConductorLogsAndAnnotations(activation, 2) // echo\n      }\n\n      // an undefined action\n      val undefinedrun = wsk.action.invoke(echo, Map(\"payload\" -> testString.toJson, \"action\" -> missing.toJson))\n      val namespace = wsk.namespace.whois()\n\n      withActivation(wsk.activation, undefinedrun) { activation =>\n        activation.response.status shouldBe \"application error\"\n        activation.response.result.get.asJsObject.fields.get(\"error\") shouldBe Some(\n          JsString(compositionComponentNotFound(s\"$namespace/$missing\")))\n        checkConductorLogsAndAnnotations(activation, 2) // echo\n      }\n  }\n\n  it should \"invoke a conductor action with a continuation\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val conductor = \"conductor\" // conductor action\n    assetHelper.withCleaner(wsk.action, conductor) { (action, _) =>\n      action.create(\n        conductor,\n        Some(TestUtils.getTestActionFilename(\"conductor.js\")),\n        annotations = Map(\"conductor\" -> true.toJson))\n    }\n\n    val step = \"step\" // step action with higher memory limit than conductor to test max memory computation\n    assetHelper.withCleaner(wsk.action, step) { (action, _) =>\n      action.create(step, Some(TestUtils.getTestActionFilename(\"step.js\")), memory = Some(257 MB))\n    }\n\n    // dynamically invoke step action\n    val run = wsk.action.invoke(conductor, Map(\"action\" -> step.toJson, \"n\" -> 1.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"n\" -> 2.toJson))\n      checkConductorLogsAndAnnotations(activation, 3) // conductor, step, conductor\n    }\n\n    // dynamically invoke step action with an error result\n    val errorrun = wsk.action.invoke(conductor, Map(\"action\" -> step.toJson))\n    withActivation(wsk.activation, errorrun) { activation =>\n      activation.response.status shouldBe \"application error\"\n      activation.response.result shouldBe Some(JsObject(\"error\" -> JsString(\"missing parameter\")))\n      checkConductorLogsAndAnnotations(activation, 3) // conductor, step, conductor\n    }\n\n    // dynamically invoke step action, blocking invocation\n    val blockingrun = wsk.action.invoke(conductor, Map(\"action\" -> step.toJson, \"n\" -> 1.toJson), blocking = true)\n    val activation = wsk.parseJsonString(blockingrun.stdout).convertTo[ActivationResult]\n\n    withClue(s\"check failed for blocking conductor activation: $activation\") {\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"n\" -> 2.toJson))\n      checkConductorLogsAndAnnotations(activation, 3) // conductor, step, conductor\n    }\n\n    // dynamically invoke step action, forwarding state\n    val secondrun = wsk.action.invoke(\n      conductor,\n      Map(\n        \"action\" -> step.toJson, // invoke step\n        \"state\" -> JsObject(\"witness\" -> 42.toJson), // dummy state\n        \"n\" -> 1.toJson))\n    withActivation(wsk.activation, secondrun) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"n\" -> 2.toJson, \"witness\" -> 42.toJson))\n      checkConductorLogsAndAnnotations(activation, 3) // conductor, step, conductor\n    }\n\n    // dynamically invoke step action twice, forwarding state\n    val thirdrun = wsk.action.invoke(\n      conductor,\n      Map(\n        \"action\" -> step.toJson, // invoke step\n        \"state\" -> JsObject(\"action\" -> step.toJson), // invoke step again\n        \"n\" -> 1.toJson))\n    withActivation(wsk.activation, thirdrun) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"n\" -> 3.toJson))\n      checkConductorLogsAndAnnotations(activation, 5) // conductor, step, conductor, step, conductor\n    }\n  }\n\n  it should \"invoke nested conductor actions\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val conductor = \"conductor\" // conductor action\n    assetHelper.withCleaner(wsk.action, conductor) { (action, _) =>\n      action.create(\n        conductor,\n        Some(TestUtils.getTestActionFilename(\"conductor.js\")),\n        annotations = Map(\"conductor\" -> true.toJson))\n    }\n\n    val step = \"step\" // step action with lower memory limit than conductor to test max memory computation\n    assetHelper.withCleaner(wsk.action, step) { (action, _) =>\n      action.create(step, Some(TestUtils.getTestActionFilename(\"step.js\")), memory = Some(255 MB))\n    }\n\n    // invoke nested conductor with single step\n    val run = wsk.action.invoke(\n      conductor,\n      Map(\n        \"action\" -> conductor.toJson, // invoke nested conductor\n        \"params\" -> JsObject(\"action\" -> step.toJson), // invoke step (level 1)\n        \"n\" -> 1.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"n\" -> 2.toJson))\n      checkConductorLogsAndAnnotations(activation, 3) // conductor, nested conductor, conductor\n      // check nested conductor invocation\n      withActivation(\n        wsk.activation,\n        activation.logs.get(1),\n        initialWait = 1 second,\n        pollPeriod = 60 seconds,\n        totalWait = allowedActionDuration) { nestedActivation =>\n        nestedActivation.response.status shouldBe \"success\"\n        nestedActivation.response.result shouldBe Some(JsObject(\"n\" -> 2.toJson))\n        checkConductorLogsAndAnnotations(nestedActivation, 3) // conductor, step, conductor\n      }\n    }\n\n    // invoke nested conductor with single step, blocking invocation\n    val blockingrun = wsk.action.invoke(\n      conductor,\n      Map(\n        \"action\" -> conductor.toJson, // invoke nested conductor\n        \"params\" -> JsObject(\"action\" -> step.toJson), // invoke step (level 1)\n        \"n\" -> 1.toJson),\n      blocking = true)\n    val activation = wsk.parseJsonString(blockingrun.stdout).convertTo[ActivationResult]\n\n    withClue(s\"check failed for blocking conductor activation: $activation\") {\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"n\" -> 2.toJson))\n      checkConductorLogsAndAnnotations(activation, 3) // conductor, nested conductor, conductor\n    }\n\n    // nested step followed by outer step\n    val secondrun = wsk.action.invoke(\n      conductor,\n      Map(\n        \"action\" -> conductor.toJson, // invoke nested conductor\n        \"state\" -> JsObject(\"action\" -> step.toJson), // invoked step on return of nested conductor (level 0)\n        \"params\" -> JsObject(\"action\" -> step.toJson), // invoke step (level 1)\n        \"n\" -> 1.toJson))\n    withActivation(wsk.activation, secondrun) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"n\" -> 3.toJson))\n      checkConductorLogsAndAnnotations(activation, 5)\n    }\n\n    // two levels of nesting, three steps\n    val thirdrun = wsk.action.invoke(\n      conductor,\n      Map(\n        \"action\" -> conductor.toJson, // invoke nested conductor\n        \"state\" -> JsObject(\"action\" -> step.toJson), // invoke step on return (level 0)\n        \"params\" -> JsObject(\n          \"action\" -> conductor.toJson, // invoked nested nested conductor\n          \"state\" -> JsObject(\"action\" -> step.toJson), // invoke step on return (level 1)\n          \"params\" -> JsObject(\"action\" -> step.toJson)), // invoke step (level 2)\n        \"n\" -> 1.toJson))\n    withActivation(wsk.activation, thirdrun) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"n\" -> 4.toJson))\n      checkConductorLogsAndAnnotations(activation, 5)\n    }\n  }\n\n  it should \"invoke a conductor action in a package binding\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val ns = wsk.namespace.whois()\n    val actionName = \"echo\" // echo conductor action\n    val packageName = \"package1\"\n    val bindName = \"package2\"\n    val packageActionName = packageName + \"/\" + actionName\n    val bindActionName = bindName + \"/\" + actionName\n    val bindNameWithNamespace = ns + \"/\" + bindName\n\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName)\n    }\n    assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>\n      pkg.bind(packageName, bindName)\n    }\n\n    assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>\n      action.create(\n        packageActionName,\n        Some(TestUtils.getTestActionFilename(\"echo.js\")),\n        annotations = Map(\"conductor\" -> true.toJson))\n    }\n\n    // the conductor annotation should not affect the behavior of an action\n    // that returns a dictionary without a params or action field\n    val run = wsk.action.invoke(bindActionName, Map(\"payload\" -> testString.toJson, \"state\" -> testString.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"payload\" -> testString.toJson, \"state\" -> testString.toJson))\n\n      val binding = activation.getAnnotationValue(\"binding\")\n      binding shouldBe defined\n      binding.get shouldBe JsString(bindNameWithNamespace)\n\n      checkConductorLogsAndAnnotations(activation, 1) // echo\n    }\n\n    // the conductor annotation should not affect the behavior of an action that returns an error\n    val secondrun = wsk.action.invoke(bindActionName, Map(\"error\" -> testString.toJson))\n    withActivation(wsk.activation, secondrun) { activation =>\n      activation.response.status shouldBe \"application error\"\n      activation.response.result shouldBe Some(JsObject(\"error\" -> testString.toJson))\n\n      val binding = activation.getAnnotationValue(\"binding\")\n      binding shouldBe defined\n      binding.get shouldBe JsString(bindNameWithNamespace)\n\n      checkConductorLogsAndAnnotations(activation, 1) // echo\n    }\n\n    // the controller should unwrap a wrapped result { params: result, ... } for an action with a conductor annotation\n    // discarding other fields if there is no action field\n    val thirdrun = wsk.action.invoke(\n      bindActionName,\n      Map(\n        \"params\" -> JsObject(\"payload\" -> testString.toJson),\n        \"result\" -> testString.toJson,\n        \"state\" -> testString.toJson))\n    withActivation(wsk.activation, thirdrun) { activation =>\n      activation.response.status shouldBe \"success\"\n      activation.response.result shouldBe Some(JsObject(\"payload\" -> testString.toJson))\n\n      val binding = activation.getAnnotationValue(\"binding\")\n      binding shouldBe defined\n      binding.get shouldBe JsString(bindNameWithNamespace)\n\n      checkConductorLogsAndAnnotations(activation, 1) // echo\n    }\n  }\n\n  /**\n   * checks logs for the activation of a conductor action (length/size and ids)\n   * checks that the cause field for nested invocations is set properly\n   * checks duration\n   * checks memory\n   */\n  private def checkConductorLogsAndAnnotations(activation: ActivationResult, size: Int) = {\n    activation.logs shouldBe defined\n    // check that the logs are what they are supposed to be (activation ids)\n    // check that the cause field is properly set for these activations\n    activation.logs.get should have length size // the number of activations in this sequence\n    var totalTime: Long = 0\n    var maxMemory: Long = 0\n    activation.logs.get.foreach { id =>\n      withActivation(\n        wsk.activation,\n        id,\n        initialWait = 1 second,\n        pollPeriod = 60 seconds,\n        totalWait = allowedActionDuration) { componentActivation =>\n        componentActivation.cause shouldBe defined\n        componentActivation.cause.get shouldBe (activation.activationId)\n        // check waitTime\n        val waitTime = componentActivation.getAnnotationValue(\"waitTime\")\n        waitTime shouldBe defined\n        // check causedBy\n        val causedBy = componentActivation.getAnnotationValue(\"causedBy\")\n        causedBy shouldBe defined\n        causedBy.get shouldBe (JsString(\"sequence\"))\n        totalTime += componentActivation.duration\n        // extract memory\n        val mem = extractMemoryAnnotation(componentActivation)\n        maxMemory = maxMemory max mem\n      }\n    }\n    // extract duration\n    activation.duration shouldBe (totalTime)\n    // extract memory\n    activation.annotations shouldBe defined\n    val memory = extractMemoryAnnotation(activation)\n    memory shouldBe (maxMemory)\n  }\n\n  private def extractMemoryAnnotation(activation: ActivationResult): Long = {\n    val limits = activation.getAnnotationValue(\"limits\")\n    limits shouldBe defined\n    limits.get.asJsObject.getFields(\"memory\")(0).convertTo[Long]\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskConsoleTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic;\n\nimport java.time.Instant\n\nimport scala.concurrent.duration.Duration\nimport scala.concurrent.duration.DurationInt\nimport scala.concurrent.duration.MILLISECONDS\nimport scala.language.postfixOps\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.TestHelpers\nimport common.TestUtils\nimport common.WskOperations\nimport common.WskProps\nimport common.WskTestHelpers\nimport spray.json.DefaultJsonProtocol._\nimport spray.json._\nimport org.apache.openwhisk.core.entity.Annotations\n\n/**\n * Tests of the text console\n */\n@RunWith(classOf[JUnitRunner])\nabstract class WskConsoleTests extends TestHelpers with WskTestHelpers {\n\n  implicit val wskprops = WskProps()\n  val wsk: WskOperations\n  val guestNamespace = wskprops.namespace\n\n  behavior of \"Wsk Activation Console\"\n\n  it should \"show an activation log message for hello world\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val packageName = withTimestamp(\"samples\")\n    val actionName = withTimestamp(\"helloWorld\")\n    val fullActionName = s\"/$guestNamespace/$packageName/$actionName\"\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, shared = Some(true))\n    }\n\n    assetHelper.withCleaner(wsk.action, fullActionName) { (action, _) =>\n      action.create(fullActionName, Some(TestUtils.getTestActionFilename(\"hello.js\")))\n    }\n\n    // Some contingency to make query more robust\n    // Account for time differences between controller and invoker\n    val start = Instant.now.minusSeconds(5)\n    val payload = new String(\"from the console!\".getBytes, \"UTF-8\")\n    val run = wsk.action.invoke(fullActionName, Map(\"payload\" -> payload.toJson))\n    withActivation(wsk.activation, run, totalWait = 30.seconds) { activation =>\n      // Time recorded by invoker, some contingency to make query more robust\n      val queryTime = activation.start.minusMillis(500)\n      // since: poll for activations since specified point in time (absolute)\n      val activations =\n        wsk.activation.pollFor(N = 1, Some(s\"$packageName/$actionName\"), since = Some(queryTime), retries = 80).length\n      withClue(\n        s\"expected activations of action '$fullActionName' since $queryTime, initial activation ${activation.activationId}:\") {\n        activations should be(1)\n      }\n\n      val duration = Duration(Instant.now.minusMillis(start.toEpochMilli).toEpochMilli, MILLISECONDS)\n      val pollTime = 10 seconds\n      // since: poll for activations since specified number of seconds ago (relative)\n      val console = wsk.activation.console(pollTime, since = Some(duration))\n      withClue(s\"Polled since ${duration.toSeconds} seconds, did not find expected result:\") {\n        console.stdout should include(payload)\n      }\n    }\n  }\n\n  it should \"show repeated activations\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = withTimestamp(\"countdown\")\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(\n        name,\n        Some(TestUtils.getTestActionFilename(\"countdown.js\")),\n        annotations = Map(Annotations.ProvideApiKeyAnnotationName -> JsTrue))\n    }\n\n    val count = 3\n    // Some contingency to make query more robust\n    // Account for time differences between controller and invoker\n    val start = Instant.now.minusSeconds(5)\n    val run = wsk.action.invoke(name, Map(\"n\" -> count.toJson))\n    withActivation(wsk.activation, run) { activation =>\n      // Time recorded by invoker, some contingency to make query more robust\n      val queryTime = activation.start.minusMillis(500)\n      // since: poll for activations since specified point in time (absolute)\n      val activations = wsk.activation.pollFor(N = 4, Some(name), since = Some(queryTime), retries = 80).length\n      withClue(\n        s\"expected activations of action '$name' since $queryTime, initial activation ${activation.activationId}:\") {\n        activations should be(count + 1)\n      }\n      val duration = Duration(Instant.now.minusMillis(start.toEpochMilli).toEpochMilli, MILLISECONDS)\n      val pollTime = 10 seconds\n      // since: poll for activations since specified number of seconds ago (relative)\n      val console = wsk.activation.console(pollTime, since = Some(duration))\n      withClue(s\"Polled for ${duration.toSeconds} seconds, did not find expected result:\") {\n        console.stdout should include(\"Happy New Year\")\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskMultiRuntimeTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\nimport common.JsHelpers\nimport common.TestHelpers\nimport common.TestUtils\nimport common.WskActorSystem\nimport common.WskOperations\nimport common.WskProps\nimport common.WskTestHelpers\nimport common.rest.WskRestOperations\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n@RunWith(classOf[JUnitRunner])\nclass WskMultiRuntimeTests extends TestHelpers with WskTestHelpers with JsHelpers with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  // wsk must have type WskOperations so that tests using CLI (class Wsk)\n  // instead of REST (WskRestOperations) still work.\n  val wsk: WskOperations = new WskRestOperations\n  val testString = \"this is a test\"\n\n  it should \"update an action with different language and check preserving params\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"updatedAction\"\n\n      assetHelper.withCleaner(wsk.action, name, false) { (action, _) =>\n        wsk.action.create(\n          name,\n          Some(TestUtils.getTestActionFilename(\"hello.js\")),\n          parameters = Map(\"name\" -> testString.toJson)) //unused in the first function\n      }\n\n      wsk.action.create(name, Some(TestUtils.getTestActionFilename(\"hello.py\")), update = true)\n\n      val run = wsk.action.invoke(name)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"success\"\n        activation.logs.get.mkString(\" \") should include(s\"Hello $testString\")\n      }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskPackageTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport java.util.Date\n\nimport scala.language.postfixOps\nimport scala.collection.mutable.HashMap\nimport scala.concurrent.duration.DurationInt\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common._\nimport spray.json._\nimport spray.json.DefaultJsonProtocol.StringJsonFormat\nimport common.WskProps\nimport common.rest.WskRestOperations\nimport org.apache.openwhisk.core.entity.WhiskActivation\n\n@RunWith(classOf[JUnitRunner])\nclass WskPackageTests extends TestHelpers with WskTestHelpers with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  val wsk: WskOperations = new WskRestOperations\n\n  val LOG_DELAY = 80 seconds\n\n  behavior of \"Wsk Package\"\n\n  it should \"allow creation and deletion of a package\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"simplepackage\"\n    assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n      pkg.create(name, Map.empty)\n    }\n  }\n\n  val params1 = Map(\"p1\" -> \"v1\".toJson, \"p2\" -> \"\".toJson)\n  val params2 = Map(\"p1\" -> \"v1\".toJson, \"p2\" -> \"v2\".toJson, \"p3\" -> \"v3\".toJson)\n\n  it should \"allow creation of a package with parameters\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"simplepackagewithparams\"\n    assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n      pkg.create(name, params1)\n    }\n  }\n\n  it should \"allow updating a package\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"simplepackagetoupdate\"\n    assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n      pkg.create(name, params1)\n      pkg.create(name, params2, update = true)\n    }\n  }\n\n  it should \"allow binding of a package\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"simplepackagetobind\"\n    val bindName = \"simplebind\"\n    assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n      pkg.create(name, params1)\n    }\n    assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>\n      pkg.bind(name, bindName, params2)\n    }\n  }\n\n  it should \"perform package binds so parameters are inherited\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val packageName = \"package1\"\n    val bindName = \"package2\"\n    val actionName = \"print\"\n    val packageActionName = packageName + \"/\" + actionName\n    val bindActionName = bindName + \"/\" + actionName\n    val packageParams = Map(\"key1a\" -> \"value1a\".toJson, \"key1b\" -> \"value1b\".toJson)\n    val bindParams = Map(\"key2a\" -> \"value2a\".toJson, \"key1b\" -> \"value2b\".toJson)\n    val actionParams = Map(\"key0\" -> \"value0\".toJson)\n    val file = TestUtils.getTestActionFilename(\"printParams.js\")\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, packageParams)\n    }\n    assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>\n      action.create(packageActionName, Some(file), parameters = actionParams)\n    }\n    assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>\n      pkg.bind(packageName, bindName, bindParams)\n    }\n\n    // Check that the description of packages and actions includes all the inherited parameters.\n    val packageDescription = wsk.pkg.get(packageName).stdout\n    val bindDescription = wsk.pkg.get(bindName).stdout\n    val packageActionDescription = wsk.action.get(packageActionName).stdout\n    val bindActionDescription = wsk.action.get(bindActionName).stdout\n    checkForParameters(packageDescription, packageParams)\n    checkForParameters(bindDescription, packageParams, bindParams)\n    checkForParameters(packageActionDescription, packageParams, actionParams)\n    checkForParameters(bindActionDescription, packageParams, bindParams, actionParams)\n\n    // Check that inherited parameters are passed to the action.\n    val now = new Date().toString()\n    val run = wsk.action.invoke(bindActionName, Map(\"payload\" -> now.toJson))\n    withActivation(wsk.activation, run, totalWait = LOG_DELAY) {\n      _.logs.get.mkString(\" \") should include regex (String\n        .format(\".*key0: value0.*key1a: value1a.*key1b: value2b.*key2a: value2a.*payload: %s\", now))\n    }\n  }\n\n  it should \"contain an binding annotation if invoked action is in the package binding\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val ns = wsk.namespace.whois()\n      val packageName = \"package1\"\n      val bindName = \"package2\"\n      val actionName = \"print\"\n      val packageActionName = packageName + \"/\" + actionName\n      val bindActionName = bindName + \"/\" + actionName\n      val file = TestUtils.getTestActionFilename(\"echo.js\")\n\n      assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n        pkg.create(packageName)\n      }\n      assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>\n        action.create(packageActionName, Some(file))\n      }\n      assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>\n        pkg.bind(packageName, bindName)\n      }\n\n      val run = wsk.action.invoke(bindActionName)\n      withActivation(wsk.activation, run, totalWait = LOG_DELAY) { activation =>\n        val binding = activation.getAnnotationValue(WhiskActivation.bindingAnnotation)\n        binding shouldBe defined\n        binding.get shouldBe JsString(ns + \"/\" + bindName)\n      }\n  }\n\n  it should \"not contain an binding annotation if invoked action is not in the package binding\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val packageName = \"package1\"\n    val actionName = \"print\"\n    val packageActionName = packageName + \"/\" + actionName\n\n    val file = TestUtils.getTestActionFilename(\"echo.js\")\n\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName)\n    }\n    assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>\n      action.create(packageActionName, Some(file))\n    }\n    assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n      action.create(actionName, Some(file))\n    }\n\n    withActivation(wsk.activation, wsk.action.invoke(packageActionName), totalWait = LOG_DELAY) { activation =>\n      val binding = activation.getAnnotationValue(WhiskActivation.bindingAnnotation)\n      binding shouldBe empty\n    }\n    withActivation(wsk.activation, wsk.action.invoke(actionName), totalWait = LOG_DELAY) { activation =>\n      val binding = activation.getAnnotationValue(WhiskActivation.bindingAnnotation)\n      binding shouldBe empty\n    }\n  }\n\n  /**\n   * Check that a description of an item includes the specified parameters.\n   * Parameters keys in later parameter maps override earlier ones.\n   */\n  def checkForParameters(itemDescription: String, paramSets: Map[String, JsValue]*): Unit = {\n    // Merge and the parameters handling overrides.\n    val merged = HashMap.empty[String, JsValue]\n    paramSets.foreach { merged ++= _ }\n    val flatDescription = itemDescription.replace(\"\\n\", \"\").replace(\"\\r\", \"\")\n    merged.foreach {\n      case (key: String, value: JsValue) =>\n        val toFind = s\"\"\"\"key\":.*\"${key}\",.*\"value\":.*${value.toString}\"\"\"\n        flatDescription should include regex toFind\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskRestBasicTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Accepted\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.BadGateway\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Conflict\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.Unauthorized\nimport org.apache.pekko.http.scaladsl.model.StatusCodes.NotFound\nimport java.time.Instant\n\nimport scala.concurrent.duration.DurationInt\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common._\nimport common.rest.WskRestOperations\nimport common.rest.RestResult\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport org.apache.openwhisk.core.containerpool.Container\nimport org.apache.openwhisk.core.entity.Annotations\nimport org.apache.openwhisk.http.Messages\n\n@RunWith(classOf[JUnitRunner])\nclass WskRestBasicTests extends TestHelpers with WskTestHelpers with WskActorSystem {\n\n  implicit def wskprops: WskProps = WskProps()\n  val wsk = new WskRestOperations\n\n  val defaultAction: Some[String] = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n\n  val requireAPIKeyAnnotation = WhiskProperties.getBooleanProperty(\"whisk.feature.requireApiKeyAnnotation\", true);\n\n  /**\n   * Retry operations that need to settle the controller cache\n   */\n  def cacheRetry[T](fn: => T) = org.apache.openwhisk.utils.retry(fn, 5, Some(1.second))\n\n  behavior of \"Wsk REST\"\n\n  it should \"reject creating duplicate entity\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"testDuplicateCreate\"\n    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n      trigger.create(name)\n    }\n    assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) =>\n      action.create(name, defaultAction, expectedExitCode = Conflict.intValue)\n    }\n  }\n\n  it should \"reject deleting entity in wrong collection\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"testCrossDelete\"\n    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n      trigger.create(name)\n    }\n    wsk.action.delete(name, expectedExitCode = Conflict.intValue)\n  }\n\n  it should \"reject unauthenticated access\" in {\n    implicit val wskprops = WskProps(\"xxx\") // shadow properties\n    val errormsg = \"The supplied authentication is invalid\"\n    wsk.namespace.list(expectedExitCode = Unauthorized.intValue).stderr should include(errormsg)\n  }\n\n  behavior of \"Wsk Package REST\"\n\n  it should \"create, update, get and list a package\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"testPackage\"\n    val params = Map(\"a\" -> \"A\".toJson)\n    assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n      pkg.create(name, parameters = params, shared = Some(true))\n      pkg.create(name, update = true)\n    }\n\n    // Add retry to ensure cache with \"0.0.1\" is replaced\n    val pack = cacheRetry({\n      val p = wsk.pkg.get(name)\n      p.getField(\"version\") shouldBe \"0.0.2\"\n      p\n    })\n    pack.getFieldJsValue(\"publish\") shouldBe JsTrue\n    pack.getFieldJsValue(\"parameters\") shouldBe JsArray(JsObject(\"key\" -> JsString(\"a\"), \"value\" -> JsString(\"A\")))\n    val packageList = wsk.pkg.list()\n    val packages = packageList.getBodyListJsObject\n    packages.exists(pack => RestResult.getField(pack, \"name\") == name) shouldBe true\n  }\n\n  it should \"create, and get a package summary\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val runtime = \"nodejs:default\"\n    val packageName = \"packageName\"\n    val actionName = \"actionName\"\n    val packageAnnots = Map(\n      \"description\" -> JsString(\"Package description\"),\n      \"parameters\" -> JsArray(\n        JsObject(\"name\" -> JsString(\"paramName1\"), \"description\" -> JsString(\"Parameter description 1\")),\n        JsObject(\"name\" -> JsString(\"paramName2\"), \"description\" -> JsString(\"Parameter description 2\"))))\n    val actionAnnots = Map(\n      \"description\" -> JsString(\"Action description\"),\n      \"parameters\" -> JsArray(\n        JsObject(\"name\" -> JsString(\"paramName1\"), \"description\" -> JsString(\"Parameter description 1\")),\n        JsObject(\"name\" -> JsString(\"paramName2\"), \"description\" -> JsString(\"Parameter description 2\"))))\n\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, annotations = packageAnnots)\n    }\n\n    wsk.action.create(packageName + \"/\" + actionName, defaultAction, annotations = actionAnnots, kind = Some(runtime))\n    val result = cacheRetry({\n      val p = wsk.pkg.get(packageName)\n      p.getFieldListJsObject(\"actions\") should have size 1\n      p\n    })\n    val ns = wsk.namespace.whois()\n    wsk.action.delete(packageName + \"/\" + actionName)\n\n    result.getField(\"name\") shouldBe packageName\n    result.getField(\"namespace\") shouldBe ns\n    val annos = result.getFieldJsValue(\"annotations\")\n    annos shouldBe JsArray(\n      JsObject(\"key\" -> JsString(\"description\"), \"value\" -> JsString(\"Package description\")),\n      JsObject(\n        \"key\" -> JsString(\"parameters\"),\n        \"value\" -> JsArray(\n          JsObject(\"name\" -> JsString(\"paramName1\"), \"description\" -> JsString(\"Parameter description 1\")),\n          JsObject(\"name\" -> JsString(\"paramName2\"), \"description\" -> JsString(\"Parameter description 2\")))))\n    val action = result.getFieldListJsObject(\"actions\")(0)\n    RestResult.getField(action, \"name\") shouldBe actionName\n\n    val annoAction = RestResult.getFieldJsValue(action, \"annotations\")\n\n    // first we check the returned execution runtime for 'nodejs:*'\n    annoAction\n      .convertTo[Seq[JsObject]]\n      .find(_.fields(\"key\").convertTo[String] == \"exec\")\n      .map(_.fields(\"value\"))\n      .map(exec => { exec.convertTo[String] should startWith(\"nodejs:\") })\n      .getOrElse(fail())\n\n    // since we checked it, we can remove the exec field from the annotations to make the following checks easier\n    val annoActionWithoutExec =\n      annoAction\n        .convertTo[Seq[JsObject]]\n        .filter(annotation => annotation.fields(\"key\").convertTo[String] != \"exec\")\n        .toJson\n\n    annoActionWithoutExec shouldBe (if (requireAPIKeyAnnotation) {\n                                      JsArray(\n                                        JsObject(\n                                          \"key\" -> JsString(\"description\"),\n                                          \"value\" -> JsString(\"Action description\")),\n                                        JsObject(\n                                          \"key\" -> JsString(\"parameters\"),\n                                          \"value\" -> JsArray(\n                                            JsObject(\n                                              \"name\" -> JsString(\"paramName1\"),\n                                              \"description\" -> JsString(\"Parameter description 1\")),\n                                            JsObject(\n                                              \"name\" -> JsString(\"paramName2\"),\n                                              \"description\" -> JsString(\"Parameter description 2\")))),\n                                        JsObject(\n                                          \"key\" -> Annotations.ProvideApiKeyAnnotationName.toJson,\n                                          \"value\" -> JsFalse))\n                                    } else {\n                                      JsArray(\n                                        JsObject(\n                                          \"key\" -> JsString(\"description\"),\n                                          \"value\" -> JsString(\"Action description\")),\n                                        JsObject(\n                                          \"key\" -> JsString(\"parameters\"),\n                                          \"value\" -> JsArray(\n                                            JsObject(\n                                              \"name\" -> JsString(\"paramName1\"),\n                                              \"description\" -> JsString(\"Parameter description 1\")),\n                                            JsObject(\n                                              \"name\" -> JsString(\"paramName2\"),\n                                              \"description\" -> JsString(\"Parameter description 2\")))))\n                                    })\n  }\n\n  it should \"create a package with a name that contains spaces\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"package with spaces\"\n\n    assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n      pkg.create(name)\n    }\n\n    val pack = wsk.pkg.get(name)\n    pack.getField(\"name\") shouldBe name\n  }\n\n  it should \"create a package, and get its individual fields\" in withAssetCleaner(wskprops) {\n    val name = \"packageFields\"\n    val paramInput = Map(\"payload\" -> \"test\".toJson)\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n        pkg.create(name, parameters = paramInput)\n      }\n\n      val expectedParam = JsObject(\"payload\" -> JsString(\"test\"))\n      val ns = wsk.namespace.whois()\n\n      var result = wsk.pkg.get(name)\n      result.getField(\"namespace\") shouldBe ns\n      result.getField(\"name\") shouldBe name\n      result.getField(\"version\") shouldBe \"0.0.1\"\n      result.getFieldJsValue(\"publish\") shouldBe JsFalse\n      result.getFieldJsValue(\"binding\") shouldBe JsObject.empty\n      result.getField(\"invalid\") shouldBe \"\"\n  }\n\n  it should \"reject creation of duplication packages\" in withAssetCleaner(wskprops) {\n    val name = \"dupePackage\"\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>\n        pkg.create(name)\n      }\n\n      val stderr = wsk.pkg.create(name, expectedExitCode = Conflict.intValue).stderr\n      stderr should include(\"resource already exists\")\n  }\n\n  it should \"reject delete of package that does not exist\" in {\n    val name = \"nonexistentPackage\"\n    val stderr = wsk.pkg.delete(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject get of package that does not exist\" in {\n    val name = \"nonexistentPackage\"\n    val ns = wsk.namespace.whois()\n    val stderr = wsk.pkg.get(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(s\"The requested resource '$ns/$name' does not exist\")\n  }\n\n  behavior of \"Wsk Action REST\"\n\n  it should \"create the same action twice with different cases\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    assetHelper.withCleaner(wsk.action, \"TWICE\") { (action, name) =>\n      action.create(name, defaultAction)\n    }\n    assetHelper.withCleaner(wsk.action, \"twice\") { (action, name) =>\n      action.create(name, defaultAction)\n    }\n  }\n\n  it should \"create, update, get and list an action\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"createAndUpdate\"\n    val file = Some(TestUtils.getTestActionFilename(\"hello.js\"))\n    val params = Map(\"a\" -> \"A\".toJson)\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, file, parameters = params)\n      action.create(name, None, parameters = Map(\"b\" -> \"B\".toJson), update = true)\n    }\n\n    // Add retry to ensure cache with \"0.0.1\" is replaced\n    val action = cacheRetry({\n      val a = wsk.action.get(name)\n      a.getField(\"version\") shouldBe \"0.0.2\"\n      a\n    })\n    action.getFieldJsValue(\"parameters\") shouldBe JsArray(JsObject(\"key\" -> JsString(\"b\"), \"value\" -> JsString(\"B\")))\n    action.getFieldJsValue(\"publish\") shouldBe JsFalse\n    val actionList = wsk.action.list()\n    val actions = actionList.getBodyListJsObject\n    actions.exists(action => RestResult.getField(action, \"name\") == name) shouldBe true\n  }\n\n  it should \"reject create of an action that already exists\" in withAssetCleaner(wskprops) {\n    val name = \"dupeAction\"\n    val file = Some(TestUtils.getTestActionFilename(\"echo.js\"))\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file)\n      }\n\n      val stderr = wsk.action.create(name, file, expectedExitCode = Conflict.intValue).stderr\n      stderr should include(\"resource already exists\")\n  }\n\n  it should \"reject delete of action that does not exist\" in {\n    val name = \"nonexistentAction\"\n    val stderr = wsk.action.delete(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject invocation of action that does not exist\" in {\n    val name = \"nonexistentAction\"\n    val stderr = wsk.action.invoke(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject get of an action that does not exist\" in {\n    val name = \"nonexistentAction\"\n    val stderr = wsk.action.get(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"create, and invoke an action that utilizes a docker container\" in withAssetCleaner(wskprops) {\n    val name = \"dockerContainer\"\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, name) {\n        // this docker image will be need to be pulled from dockerhub and hence has to be published there first\n        (action, _) =>\n          action.create(name, None, docker = Some(\"openwhisk/example\"))\n      }\n\n      val args = Map(\"payload\" -> \"test\".toJson)\n      val run = wsk.action.invoke(name, args)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.result shouldBe Some(\n          JsObject(\"args\" -> args.toJson, \"msg\" -> \"Hello from arbitrary C program!\".toJson))\n      }\n  }\n\n  it should \"create, and invoke an action that utilizes dockerskeleton with native zip\" in withAssetCleaner(wskprops) {\n    val name = \"dockerContainerWithZip\"\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, name) {\n        // this docker image will be need to be pulled from dockerhub and hence has to be published there first\n        (action, _) =>\n          action.create(name, Some(TestUtils.getTestActionFilename(\"blackbox.zip\")), kind = Some(\"native\"))\n      }\n\n      val run = wsk.action.invoke(name, Map.empty)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.result shouldBe Some(JsObject(\"msg\" -> \"hello zip\".toJson))\n        activation.logs shouldBe defined\n        val logs = activation.logs.get.toString\n        logs should include(\"This is an example zip used with the docker skeleton action.\")\n        logs should not include Container.ACTIVATION_LOG_SENTINEL\n      }\n  }\n\n  it should \"create, and invoke an action using a parameter file\" in withAssetCleaner(wskprops) {\n    val name = \"paramFileAction\"\n    val file = Some(TestUtils.getTestActionFilename(\"argCheck.js\"))\n    val argInput = Some(TestUtils.getTestActionFilename(\"validInput2.json\"))\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, file)\n      }\n\n      val expectedOutput = JsObject(\"payload\" -> JsString(\"test\"))\n      val run = wsk.action.invoke(name, parameterFile = argInput)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.result shouldBe Some(expectedOutput)\n      }\n  }\n\n  it should \"create an action, and get its individual fields\" in withAssetCleaner(wskprops) {\n    val runtime = \"nodejs:default\"\n    val name = \"actionFields\"\n    val paramInput = Map(\"payload\" -> \"test\".toJson)\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, defaultAction, parameters = paramInput, kind = Some(runtime))\n      }\n\n      val expectedParam = JsObject(\"payload\" -> JsString(\"test\"))\n      val ns = wsk.namespace.whois()\n      val result = wsk.action.get(name)\n      result.getField(\"name\") shouldBe name\n      result.getField(\"namespace\") shouldBe ns\n      result.getFieldJsValue(\"publish\") shouldBe JsFalse\n      result.getField(\"version\") shouldBe \"0.0.1\"\n      val exec = result.getFieldJsObject(\"exec\")\n      RestResult.getField(exec, \"kind\") should startWith(\"nodejs:\")\n      RestResult.getField(exec, \"code\") should not be \"\"\n      result.getFieldJsValue(\"parameters\") shouldBe JsArray(\n        JsObject(\"key\" -> JsString(\"payload\"), \"value\" -> JsString(\"test\")))\n\n      // first we check the returned execution runtime for 'nodejs:*'\n      result\n        .getFieldJsValue(\"annotations\")\n        .convertTo[Seq[JsObject]]\n        .find(_.fields(\"key\").convertTo[String] == \"exec\")\n        .map(_.fields(\"value\"))\n        .map(exec => { exec.convertTo[String] should startWith(\"nodejs:\") })\n        .getOrElse(fail())\n\n      // since we checked it, we can remove the exec field from the annotations to make the following checks easier\n      result\n        .getFieldJsValue(\"annotations\")\n        .convertTo[Seq[JsObject]]\n        .filter(annotation => annotation.fields(\"key\").convertTo[String] != \"exec\")\n        .toJson shouldBe (if (requireAPIKeyAnnotation) {\n                            JsArray(\n                              JsObject(\"key\" -> Annotations.ProvideApiKeyAnnotationName.toJson, \"value\" -> JsFalse))\n                          } else {\n                            JsArray()\n                          })\n      result.getFieldJsValue(\"limits\") shouldBe JsObject(\n        \"timeout\" -> JsNumber(60000),\n        \"memory\" -> JsNumber(256),\n        \"logs\" -> JsNumber(10),\n        \"concurrency\" -> JsNumber(1))\n      result.getField(\"invalid\") shouldBe \"\"\n  }\n\n  /**\n   * Tests creating an action from a malformed js file. This should fail in\n   * some way - preferably when trying to create the action. If not, then\n   * surely when it runs there should be some indication in the logs. Don't\n   * think this is true currently.\n   */\n  it should \"create and invoke action with malformed js resulting in activation error\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"MALFORMED\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"malformed.js\")))\n      }\n\n      val run = wsk.action.invoke(name, Map(\"payload\" -> \"whatever\".toJson))\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.status shouldBe \"action developer error\"\n        // representing nodejs giving an error when given malformed.js\n        activation.response.result.get.toString should include(\"ReferenceError\")\n      }\n  }\n\n  it should \"create and invoke a blocking action resulting in an application error response\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val name = \"applicationError\"\n    val strErrInput = Map(\"error\" -> \"Error message\".toJson)\n    val numErrInput = Map(\"error\" -> 502.toJson)\n    val boolErrInput = Map(\"error\" -> true.toJson)\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"echo.js\")))\n    }\n\n    Seq(strErrInput, numErrInput, boolErrInput) foreach { input =>\n      val result = wsk.action.invoke(name, parameters = input, blocking = true, expectedExitCode = BadGateway.intValue)\n      val response = result.getFieldJsObject(\"response\")\n      val res = RestResult.getFieldJsObject(response, \"result\")\n      res shouldBe input.toJson.asJsObject\n      val resultTrue =\n        wsk.action.invoke(\n          name,\n          parameters = input,\n          blocking = true,\n          result = true,\n          expectedExitCode = BadGateway.intValue)\n      resultTrue.respData shouldBe input.toJson.asJsObject.toString()\n    }\n  }\n\n  it should \"create and invoke a blocking action resulting in an failed promise\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"errorResponseObject\"\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"asyncError.js\")))\n      }\n\n      val result = wsk.action.invoke(name, blocking = true, expectedExitCode = BadGateway.intValue)\n      val response = result.getFieldJsObject(\"response\")\n      val res = RestResult.getFieldJsObject(response, \"result\")\n      res shouldBe JsObject(\"error\" -> JsObject(\"msg\" -> \"failed activation on purpose\".toJson))\n  }\n\n  it should \"invoke a blocking action and get only the result\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"basicInvoke\"\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"wc.js\")))\n    }\n\n    val result = wsk.action\n      .invoke(name, Map(\"payload\" -> \"one two three\".toJson), blocking = true, result = true)\n    result.stdout.parseJson.asJsObject shouldBe JsObject(\"count\" -> JsNumber(3))\n  }\n\n  it should \"create, and get an action summary\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val runtime = \"nodejs:default\"\n    val name = \"actionName\"\n    val annots = Map(\n      \"description\" -> JsString(\"Action description\"),\n      \"parameters\" -> JsArray(\n        JsObject(\"name\" -> JsString(\"paramName1\"), \"description\" -> JsString(\"Parameter description 1\")),\n        JsObject(\"name\" -> JsString(\"paramName2\"), \"description\" -> JsString(\"Parameter description 2\"))))\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, defaultAction, annotations = annots, kind = Some(runtime))\n    }\n\n    val result = wsk.action.get(name, summary = true)\n    val ns = wsk.namespace.whois()\n\n    result.getField(\"name\") shouldBe name\n    result.getField(\"namespace\") shouldBe ns\n\n    val annos = result.getFieldJsValue(\"annotations\")\n\n    // first we check the returned execution runtime for 'nodejs:*'\n    annos\n      .convertTo[Seq[JsObject]]\n      .find(_.fields(\"key\").convertTo[String] == \"exec\")\n      .map(_.fields(\"value\"))\n      .map(exec => { exec.convertTo[String] should startWith(\"nodejs:\") })\n      .getOrElse(fail())\n\n    // since we checked it, we can remove the exec field from the annotations to make the following checks easier\n    val annosWithoutExec =\n      annos.convertTo[Seq[JsObject]].filter(annotation => annotation.fields(\"key\").convertTo[String] != \"exec\").toJson\n\n    annosWithoutExec shouldBe (if (requireAPIKeyAnnotation) {\n                                 JsArray(\n                                   JsObject(\n                                     \"key\" -> JsString(\"description\"),\n                                     \"value\" -> JsString(\"Action description\")),\n                                   JsObject(\n                                     \"key\" -> JsString(\"parameters\"),\n                                     \"value\" -> JsArray(\n                                       JsObject(\n                                         \"name\" -> JsString(\"paramName1\"),\n                                         \"description\" -> JsString(\"Parameter description 1\")),\n                                       JsObject(\n                                         \"name\" -> JsString(\"paramName2\"),\n                                         \"description\" -> JsString(\"Parameter description 2\")))),\n                                   JsObject(\n                                     \"key\" -> Annotations.ProvideApiKeyAnnotationName.toJson,\n                                     \"value\" -> JsFalse))\n                               } else {\n                                 JsArray(\n                                   JsObject(\n                                     \"key\" -> JsString(\"description\"),\n                                     \"value\" -> JsString(\"Action description\")),\n                                   JsObject(\n                                     \"key\" -> JsString(\"parameters\"),\n                                     \"value\" -> JsArray(\n                                       JsObject(\n                                         \"name\" -> JsString(\"paramName1\"),\n                                         \"description\" -> JsString(\"Parameter description 1\")),\n                                       JsObject(\n                                         \"name\" -> JsString(\"paramName2\"),\n                                         \"description\" -> JsString(\"Parameter description 2\")))))\n                               })\n  }\n\n  it should \"create an action with a name that contains spaces\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"action with spaces\"\n\n    assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, defaultAction)\n    }\n\n    wsk.action.get(name).getField(\"name\") shouldBe name\n  }\n\n  it should \"create an action, and invoke an action that returns an empty JSON object\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"emptyJSONAction\"\n\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, Some(TestUtils.getTestActionFilename(\"emptyJSONResult.js\")))\n      }\n\n      val result = wsk.action.invoke(name, blocking = true, result = true)\n      result.stdout.parseJson.asJsObject shouldBe JsObject.empty\n  }\n\n  it should \"create, and invoke an action that times out to ensure the proper response is received\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val name = \"sleepAction-\" + System.currentTimeMillis()\n    // Must be larger than 60 seconds to see the expected exit code\n    val allowedActionDuration = 120.seconds\n    // Sleep time must be larger than 60 seconds to see the expected exit code\n    // Set sleep time to a value smaller than allowedActionDuration to not raise a timeout\n    val sleepTime = allowedActionDuration - 20.seconds\n    val params = Map(\"sleepTimeInMs\" -> sleepTime.toMillis.toJson)\n    val res = assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n      action.create(name, Some(TestUtils.getTestActionFilename(\"sleep.js\")), timeout = Some(allowedActionDuration))\n      action.invoke(name, parameters = params, result = true, expectedExitCode = Accepted.intValue)\n    }\n  }\n\n  it should \"create, and get docker action get ensure exec code is omitted\" in withAssetCleaner(wskprops) {\n    val name = \"dockerContainer\"\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n        action.create(name, None, docker = Some(\"fake-container\"))\n      }\n\n      wsk.action.get(name).stdout should not include \"\"\"\"code\"\"\"\"\n  }\n\n  behavior of \"Wsk Trigger REST\"\n\n  it should \"create, update, get, fire and list trigger\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val ruleName = withTimestamp(\"r1toa1\")\n    val triggerName = withTimestamp(\"t1tor1\")\n    val actionName = withTimestamp(\"a1\")\n    val params = Map(\"a\" -> \"A\".toJson)\n    val ns = wsk.namespace.whois()\n\n    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>\n      trigger.create(triggerName, parameters = params)\n      trigger.create(triggerName, update = true)\n    }\n\n    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n      action.create(name, defaultAction)\n    }\n\n    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n      rule.create(name, trigger = triggerName, action = actionName)\n    }\n\n    // Add retry to ensure cache with \"0.0.1\" is replaced\n    val trigger = cacheRetry({\n      val t = wsk.trigger.get(triggerName)\n      t.getField(\"version\") shouldBe \"0.0.2\"\n      t\n    })\n    trigger.getFieldJsValue(\"parameters\") shouldBe JsArray(JsObject(\"key\" -> JsString(\"a\"), \"value\" -> JsString(\"A\")))\n    trigger.getFieldJsValue(\"publish\") shouldBe JsFalse\n\n    val expectedRules = JsObject(\n      ns + \"/\" + ruleName -> JsObject(\n        \"action\" -> JsObject(\"name\" -> JsString(actionName), \"path\" -> JsString(ns)),\n        \"status\" -> JsString(\"active\")))\n    trigger.getFieldJsValue(\"rules\") shouldBe expectedRules\n\n    val dynamicParams = Map(\"t\" -> \"T\".toJson)\n    val run = wsk.trigger.fire(triggerName, dynamicParams)\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.result shouldBe Some(dynamicParams.toJson)\n      activation.duration shouldBe 0L // shouldn't exist but CLI generates it\n      activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it\n      activation.logs shouldBe defined\n      activation.logs.get.size shouldBe 1\n\n      val logEntry = activation.logs.get(0).parseJson.asJsObject\n      val logs = JsArray(logEntry)\n      val ruleActivationId: String = logEntry.getFields(\"activationId\")(0).convertTo[String]\n      val expectedLogs = JsArray(\n        JsObject(\n          \"statusCode\" -> JsNumber(0),\n          \"activationId\" -> JsString(ruleActivationId),\n          \"success\" -> JsTrue,\n          \"rule\" -> JsString(ns + \"/\" + ruleName),\n          \"action\" -> JsString(ns + \"/\" + actionName)))\n      logs shouldBe expectedLogs\n    }\n\n    val runWithNoParams = wsk.trigger.fire(triggerName, Map.empty)\n    withActivation(wsk.activation, runWithNoParams) { activation =>\n      activation.response.result shouldBe Some(JsObject.empty)\n      activation.duration shouldBe 0L // shouldn't exist but CLI generates it\n      activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it\n    }\n\n    val triggerList = wsk.trigger.list()\n    val triggers = triggerList.getBodyListJsObject\n    triggers.exists(trigger => RestResult.getField(trigger, \"name\") == triggerName) shouldBe true\n  }\n\n  it should \"create, and get a trigger summary\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"triggerName\"\n    val annots = Map(\n      \"description\" -> JsString(\"Trigger description\"),\n      \"parameters\" -> JsArray(\n        JsObject(\"name\" -> JsString(\"paramName1\"), \"description\" -> JsString(\"Parameter description 1\")),\n        JsObject(\"name\" -> JsString(\"paramName2\"), \"description\" -> JsString(\"Parameter description 2\"))))\n\n    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n      trigger.create(name, annotations = annots)\n    }\n\n    val result = wsk.trigger.get(name)\n    val ns = wsk.namespace.whois()\n\n    result.getField(\"name\") shouldBe name\n    result.getField(\"namespace\") shouldBe ns\n    val annos = result.getFieldJsValue(\"annotations\")\n    annos shouldBe JsArray(\n      JsObject(\"key\" -> JsString(\"description\"), \"value\" -> JsString(\"Trigger description\")),\n      JsObject(\n        \"key\" -> JsString(\"parameters\"),\n        \"value\" -> JsArray(\n          JsObject(\"name\" -> JsString(\"paramName1\"), \"description\" -> JsString(\"Parameter description 1\")),\n          JsObject(\"name\" -> JsString(\"paramName2\"), \"description\" -> JsString(\"Parameter description 2\")))))\n  }\n\n  it should \"create a trigger with a name that contains spaces\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"trigger with spaces\"\n\n    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n      trigger.create(name)\n    }\n\n    val res = wsk.trigger.get(name)\n    res.getField(\"name\") shouldBe name\n  }\n\n  it should \"create, and fire a trigger using a parameter file\" in withAssetCleaner(wskprops) {\n    val ruleName = withTimestamp(\"r1toa1\")\n    val triggerName = withTimestamp(\"paramFileTrigger\")\n    val actionName = withTimestamp(\"a1\")\n    val argInput = Some(TestUtils.getTestActionFilename(\"validInput2.json\"))\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>\n        trigger.create(triggerName)\n      }\n\n      assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n        action.create(name, defaultAction)\n      }\n\n      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n        rule.create(name, trigger = triggerName, action = actionName)\n      }\n\n      val expectedOutput = JsObject(\"payload\" -> JsString(\"test\"))\n      val run = wsk.trigger.fire(triggerName, parameterFile = argInput)\n      withActivation(wsk.activation, run) { activation =>\n        activation.response.result shouldBe Some(expectedOutput)\n      }\n  }\n\n  it should \"create a trigger, and get its individual fields\" in withAssetCleaner(wskprops) {\n    val name = \"triggerFields\"\n    val paramInput = Map(\"payload\" -> \"test\".toJson)\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n        trigger.create(name, parameters = paramInput)\n      }\n\n      val expectedParam = JsObject(\"payload\" -> JsString(\"test\"))\n      val ns = wsk.namespace.whois()\n\n      val result = wsk.trigger\n        .get(name, fieldFilter = Some(\"namespace\"))\n      result.getField(\"namespace\") shouldBe ns\n      result.getField(\"name\") shouldBe name\n      result.getField(\"version\") shouldBe \"0.0.1\"\n      result.getFieldJsValue(\"publish\") shouldBe JsFalse\n      result.getFieldJsValue(\"annotations\").toString shouldBe \"[]\"\n      result.getFieldJsValue(\"parameters\") shouldBe JsArray(\n        JsObject(\"key\" -> JsString(\"payload\"), \"value\" -> JsString(\"test\")))\n      result.getFieldJsValue(\"limits\") shouldBe JsObject.empty\n      result.getField(\"invalid\") shouldBe \"\"\n  }\n\n  it should \"create, and fire a trigger to ensure result is empty\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val ruleName = withTimestamp(\"r1toa1\")\n    val triggerName = withTimestamp(\"emptyResultTrigger\")\n    val actionName = withTimestamp(\"a1\")\n\n    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>\n      trigger.create(triggerName)\n    }\n\n    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n      action.create(name, defaultAction)\n    }\n\n    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n      rule.create(name, trigger = triggerName, action = actionName)\n    }\n\n    val run = wsk.trigger.fire(triggerName)\n    withActivation(wsk.activation, run) { activation =>\n      activation.response.result shouldBe Some(JsObject.empty)\n    }\n  }\n\n  it should \"reject creation of duplicate triggers\" in withAssetCleaner(wskprops) {\n    val name = \"dupeTrigger\"\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>\n        trigger.create(name)\n      }\n\n      val stderr = wsk.trigger.create(name, expectedExitCode = Conflict.intValue).stderr\n      stderr should include(\"resource already exists\")\n  }\n\n  it should \"reject delete of trigger that does not exist\" in {\n    val name = \"nonexistentTrigger\"\n    val stderr = wsk.trigger.delete(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject get of trigger that does not exist\" in {\n    val name = \"nonexistentTrigger\"\n    val stderr = wsk.trigger.get(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject firing of a trigger that does not exist\" in {\n    val name = \"nonexistentTrigger\"\n    val stderr = wsk.trigger.fire(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"create and fire a trigger with a rule whose action has been deleted\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val ruleName1 = withTimestamp(\"r1toa1\")\n      val ruleName2 = withTimestamp(\"r2toa2\")\n      val triggerName = withTimestamp(\"t1tor1r2\")\n      val actionName1 = withTimestamp(\"a1\")\n      val actionName2 = withTimestamp(\"a2\")\n      val ns = wsk.namespace.whois()\n\n      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>\n        trigger.create(triggerName)\n        trigger.create(triggerName, update = true)\n      }\n\n      assetHelper.withCleaner(wsk.action, actionName1) { (action, name) =>\n        action.create(name, defaultAction)\n      }\n      wsk.action.create(actionName2, defaultAction) // Delete this after the rule is created\n\n      assetHelper.withCleaner(wsk.rule, ruleName1) { (rule, name) =>\n        rule.create(name, trigger = triggerName, action = actionName1)\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName2) { (rule, name) =>\n        rule.create(name, trigger = triggerName, action = actionName2)\n      }\n      wsk.action.delete(actionName2)\n\n      val run = wsk.trigger.fire(triggerName)\n      withActivation(wsk.activation, run) { activation =>\n        activation.duration shouldBe 0L // shouldn't exist but CLI generates it\n        activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it\n        activation.logs shouldBe defined\n        activation.logs.get.size shouldBe 2\n\n        val logEntry1 = activation.logs.get(0).parseJson.asJsObject\n        val logEntry2 = activation.logs.get(1).parseJson.asJsObject\n        val logs = JsArray(logEntry1, logEntry2)\n        val ruleActivationId: String = if (logEntry1.getFields(\"activationId\").size == 1) {\n          logEntry1.getFields(\"activationId\")(0).convertTo[String]\n        } else {\n          logEntry2.getFields(\"activationId\")(0).convertTo[String]\n        }\n        val expectedLogs = JsArray(\n          JsObject(\n            \"statusCode\" -> JsNumber(0),\n            \"activationId\" -> JsString(ruleActivationId),\n            \"success\" -> JsTrue,\n            \"rule\" -> JsString(ns + \"/\" + ruleName1),\n            \"action\" -> JsString(ns + \"/\" + actionName1)),\n          JsObject(\n            \"statusCode\" -> JsNumber(1),\n            \"success\" -> JsFalse,\n            \"error\" -> JsString(\"The requested resource does not exist.\"),\n            \"rule\" -> JsString(ns + \"/\" + ruleName2),\n            \"action\" -> JsString(ns + \"/\" + actionName2)))\n        logs shouldBe expectedLogs\n      }\n  }\n\n  it should \"create and fire a trigger having an active rule and an inactive rule\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val ruleName1 = withTimestamp(\"r1toa1\")\n      val ruleName2 = withTimestamp(\"r2toa2\")\n      val triggerName = withTimestamp(\"t1tor1r2\")\n      val actionName1 = withTimestamp(\"a1\")\n      val actionName2 = withTimestamp(\"a2\")\n      val ns = wsk.namespace.whois()\n\n      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>\n        trigger.create(triggerName)\n        trigger.create(triggerName, update = true)\n      }\n\n      assetHelper.withCleaner(wsk.action, actionName1) { (action, name) =>\n        action.create(name, defaultAction)\n      }\n      assetHelper.withCleaner(wsk.action, actionName2) { (action, name) =>\n        action.create(name, defaultAction)\n      }\n\n      assetHelper.withCleaner(wsk.rule, ruleName1) { (rule, name) =>\n        rule.create(name, trigger = triggerName, action = actionName1)\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName2) { (rule, name) =>\n        rule.create(name, trigger = triggerName, action = actionName2)\n        rule.disable(ruleName2)\n      }\n\n      val run = wsk.trigger.fire(triggerName)\n      withActivation(wsk.activation, run) { activation =>\n        activation.duration shouldBe 0L // shouldn't exist but CLI generates it\n        activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it\n        activation.logs shouldBe defined\n        activation.logs.get.size shouldBe 2\n\n        val logEntry1 = activation.logs.get(0).parseJson.asJsObject\n        val logEntry2 = activation.logs.get(1).parseJson.asJsObject\n        val logs = JsArray(logEntry1, logEntry2)\n        val ruleActivationId: String = if (logEntry1.getFields(\"activationId\").size == 1) {\n          logEntry1.getFields(\"activationId\")(0).convertTo[String]\n        } else {\n          logEntry2.getFields(\"activationId\")(0).convertTo[String]\n        }\n        val expectedLogs = JsArray(\n          JsObject(\n            \"statusCode\" -> JsNumber(0),\n            \"activationId\" -> JsString(ruleActivationId),\n            \"success\" -> JsTrue,\n            \"rule\" -> JsString(ns + \"/\" + ruleName1),\n            \"action\" -> JsString(ns + \"/\" + actionName1)),\n          JsObject(\n            \"statusCode\" -> JsNumber(1),\n            \"success\" -> JsFalse,\n            \"error\" -> JsString(Messages.triggerWithInactiveRule(s\"$ns/$ruleName2\", s\"$ns/$actionName2\")),\n            \"rule\" -> JsString(ns + \"/\" + ruleName2),\n            \"action\" -> JsString(ns + \"/\" + actionName2)))\n        logs shouldBe expectedLogs\n      }\n  }\n\n  behavior of \"Wsk Rule REST\"\n\n  it should \"create rule, get rule, update rule and list rule\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val ruleName = \"listRules\"\n    val triggerName = \"listRulesTrigger\"\n    val actionName = \"listRulesAction\"\n\n    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, name) =>\n      trigger.create(name)\n    }\n    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n      action.create(name, defaultAction)\n    }\n    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n      rule.create(name, trigger = triggerName, action = actionName)\n    }\n\n    // finally, we perform the update, and expect success this time\n    wsk.rule.create(ruleName, trigger = triggerName, action = actionName, update = true)\n\n    // Add retry to ensure cache with \"0.0.1\" is replaced\n    val rule = cacheRetry({\n      val r = wsk.rule.get(ruleName)\n      r.getField(\"version\") shouldBe \"0.0.2\"\n      r\n    })\n    rule.getField(\"name\") shouldBe ruleName\n    RestResult.getField(rule.getFieldJsObject(\"trigger\"), \"name\") shouldBe triggerName\n    RestResult.getField(rule.getFieldJsObject(\"action\"), \"name\") shouldBe actionName\n    val rules = wsk.rule.list().getBodyListJsObject\n    rules.exists { rule =>\n      RestResult.getField(rule, \"name\") == ruleName\n    } shouldBe true\n  }\n\n  it should \"create rule, get rule, ensure rule is enabled by default\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val ruleName = \"enabledRule\"\n      val triggerName = \"enabledRuleTrigger\"\n      val actionName = \"enabledRuleAction\"\n\n      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, name) =>\n        trigger.create(name)\n      }\n      assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n        action.create(name, defaultAction)\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n        rule.create(name, trigger = triggerName, action = actionName)\n      }\n\n      val rule = wsk.rule.get(ruleName)\n      rule.getField(\"status\") shouldBe \"active\"\n  }\n\n  it should \"display a rule summary when --summary flag is used with 'wsk rule get'\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val ruleName = \"mySummaryRule\"\n      val triggerName = \"summaryRuleTrigger\"\n      val actionName = \"summaryRuleAction\"\n\n      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, name) =>\n        trigger.create(name)\n      }\n      assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n        action.create(name, defaultAction)\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName, confirmDelete = false) { (rule, name) =>\n        rule.create(name, trigger = triggerName, action = actionName)\n      }\n\n      // Summary namespace should match one of the allowable namespaces (typically 'guest')\n      val ns = wsk.namespace.whois()\n      val result = wsk.rule.get(ruleName)\n      result.getField(\"name\") shouldBe ruleName\n      result.getField(\"namespace\") shouldBe ns\n      result.getField(\"status\") shouldBe \"active\"\n  }\n\n  it should \"create a rule, and get its individual fields\" in withAssetCleaner(wskprops) {\n    val ruleName = \"ruleFields\"\n    val triggerName = \"ruleTriggerFields\"\n    val actionName = \"ruleActionFields\"\n    val paramInput = Map(\"payload\" -> \"test\".toJson)\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, name) =>\n        trigger.create(name)\n      }\n      assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n        action.create(name, defaultAction)\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n        rule.create(name, trigger = triggerName, action = actionName)\n      }\n\n      val ns = wsk.namespace.whois()\n      val rule = wsk.rule.get(ruleName)\n      rule.getField(\"namespace\") shouldBe ns\n      rule.getField(\"name\") shouldBe ruleName\n      rule.getField(\"version\") shouldBe \"0.0.1\"\n      rule.getField(\"status\") shouldBe \"active\"\n      val result = wsk.rule.get(ruleName)\n      val trigger = result.getFieldJsValue(\"trigger\").toString\n      trigger should include(triggerName)\n      trigger should not include actionName\n      val action = result.getFieldJsValue(\"action\").toString\n      action should not include triggerName\n      action should include(actionName)\n  }\n\n  it should \"reject creation of duplicate rules\" in withAssetCleaner(wskprops) {\n    val ruleName = \"dupeRule\"\n    val triggerName = \"triggerName\"\n    val actionName = \"actionName\"\n\n    (wp, assetHelper) =>\n      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, name) =>\n        trigger.create(name)\n      }\n      assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n        action.create(name, defaultAction)\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n        rule.create(name, trigger = triggerName, action = actionName)\n      }\n\n      val stderr =\n        wsk.rule\n          .create(ruleName, trigger = triggerName, action = actionName, expectedExitCode = Conflict.intValue)\n          .stderr\n      stderr should include(\"resource already exists\")\n  }\n\n  it should \"reject delete of rule that does not exist\" in {\n    val name = \"nonexistentRule\"\n    val stderr = wsk.rule.delete(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject enable of rule that does not exist\" in {\n    val name = \"nonexistentRule\"\n    val stderr = wsk.rule.enable(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject disable of rule that does not exist\" in {\n    val name = \"nonexistentRule\"\n    val stderr = wsk.rule.disable(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject status of rule that does not exist\" in {\n    val name = \"nonexistentRule\"\n    val stderr = wsk.rule.state(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject get of rule that does not exist\" in {\n    val name = \"nonexistentRule\"\n    val stderr = wsk.rule.get(name, expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  behavior of \"Wsk Namespace REST\"\n\n  it should \"return a list of exactly one namespace\" in {\n    val lines = wsk.namespace.list()\n    lines.getBodyListString.size shouldBe 1\n  }\n\n  behavior of \"Wsk Activation REST\"\n\n  it should \"create a trigger, and fire a trigger to get its individual fields from an activation\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val ruleName = withTimestamp(\"r1toa1\")\n    val triggerName = withTimestamp(\"activationFields\")\n    val actionName = withTimestamp(\"a1\")\n\n    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>\n      trigger.create(triggerName)\n    }\n\n    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n      action.create(name, defaultAction)\n    }\n\n    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n      rule.create(name, trigger = triggerName, action = actionName)\n    }\n\n    val ns = wsk.namespace.whois()\n    val run = wsk.trigger.fire(triggerName)\n    withActivation(wsk.activation, run) { activation =>\n      var result = wsk.activation.get(Some(activation.activationId))\n      result.getField(\"namespace\") shouldBe ns\n      result.getField(\"name\") shouldBe triggerName\n      result.getField(\"version\") shouldBe \"0.0.1\"\n      result.getFieldJsValue(\"publish\") shouldBe JsFalse\n      result.getField(\"subject\") shouldBe ns\n      result.getField(\"activationId\") shouldBe activation.activationId\n      result.getFieldJsValue(\"start\").toString should not be JsObject.empty.toString\n      result.getFieldJsValue(\"end\").toString shouldBe JsObject.empty.toString\n      result.getFieldJsValue(\"duration\").toString shouldBe JsObject.empty.toString\n      result.getFieldListJsObject(\"annotations\").length shouldBe 0\n    }\n  }\n\n  it should \"reject get of activation that does not exist\" in {\n    val name = \"0\" * 32\n    val stderr = wsk.activation.get(Some(name), expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject logs of activation that does not exist\" in {\n    val name = \"0\" * 32\n    val stderr = wsk.activation.logs(Some(name), expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n\n  it should \"reject result of activation that does not exist\" in {\n    val name = \"0\" * 32\n    val stderr = wsk.activation.result(Some(name), expectedExitCode = NotFound.intValue).stderr\n    stderr should include(\"The requested resource does not exist.\")\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskRestRuleTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport common.TestUtils.RunResult\nimport common.rest.WskRestOperations\nimport common.rest.RestResult\nimport common.WskActorSystem\n\nimport org.apache.openwhisk.utils.retry\n\nimport scala.concurrent.duration._\n\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\n\n@RunWith(classOf[JUnitRunner])\nclass WskRestRuleTests extends WskRuleTests with WskActorSystem {\n  override val wsk = new WskRestOperations\n\n  override def verifyRuleList(ruleListResult: RunResult,\n                              ruleNameEnable: String,\n                              ruleName: String): org.scalatest.Assertion = {\n    val ruleListResultRest = ruleListResult.asInstanceOf[RestResult]\n    val rules = ruleListResultRest.getBodyListJsObject\n    val ruleEnable = wsk.rule.get(ruleNameEnable)\n    ruleEnable.getField(\"status\") shouldBe \"active\"\n    val ruleDisable = wsk.rule.get(ruleName)\n    ruleDisable.getField(\"status\") shouldBe \"inactive\"\n    rules.exists(rule => RestResult.getField(rule, \"name\") == ruleNameEnable) shouldBe true\n    rules.exists(rule => RestResult.getField(rule, \"name\") == ruleName) shouldBe true\n    ruleListResultRest.respData should not include \"Unknown\"\n  }\n\n  it should \"preserve rule status when a rule is updated\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val ruleName = withTimestamp(\"r1to1\")\n    val triggerName = withTimestamp(\"t1to1\")\n    val actionName = withTimestamp(\"a1 to 1\")\n    val triggerName2 = withTimestamp(\"t2to1\")\n    val active = Some(\"active\".toJson)\n    val inactive = Some(\"inactive\".toJson)\n    val statusPermutations =\n      Seq((triggerName, active), (triggerName, inactive), (triggerName2, active), (triggerName, inactive))\n\n    ruleSetup(Seq((ruleName, triggerName, (actionName, actionName, defaultAction))), assetHelper)\n    assetHelper.withCleaner(wsk.trigger, triggerName2) { (trigger, name) =>\n      trigger.create(name)\n    }\n\n    statusPermutations.foreach {\n      case (trigger, status) =>\n        // Needs to be retried since the enable/disable causes a cache invalidation which needs to propagate first\n        retry({\n          if (status == active) wsk.rule.enable(ruleName) else wsk.rule.disable(ruleName)\n          val createStdout = wsk.rule.create(ruleName, trigger, actionName, update = true).stdout\n          val getStdout = wsk.rule.get(ruleName).stdout\n          wsk.parseJsonString(createStdout).fields.get(\"status\") shouldBe status\n          wsk.parseJsonString(getStdout).fields.get(\"status\") shouldBe status\n        }, 10, Some(1.second))\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskRuleTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common.TestHelpers\nimport common.TestUtils\nimport common.TestUtils.RunResult\nimport common.WskOperations\nimport common.WskProps\nimport common.WskTestHelpers\nimport common.RuleActivationResult\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport java.time.Instant\n\n@RunWith(classOf[JUnitRunner])\nabstract class WskRuleTests extends TestHelpers with WskTestHelpers {\n\n  implicit val wskprops = WskProps()\n  val wsk: WskOperations\n  val defaultAction = TestUtils.getTestActionFilename(\"wc.js\")\n  val secondAction = TestUtils.getTestActionFilename(\"hello.js\")\n  val testString = \"this is a test\"\n  val testResult = JsObject(\"count\" -> testString.split(\" \").length.toJson)\n\n  /**\n   * Sets up trigger -> rule -> action triplets. Deduplicates triggers and rules\n   * and links it all up.\n   *\n   * @param rules Tuple3s containing\n   *   (rule, trigger, (action name for created action, action name for the rule binding, actionFile))\n   *   where the action name for the created action is allowed to differ from that used by the rule binding\n   *   for cases that reference actions in a package binding.\n   */\n  def ruleSetup(rules: Seq[(String, String, (String, String, String))], assetHelper: AssetCleaner): Unit = {\n    val triggers = rules.map(_._2).distinct\n    val actions = rules.map(_._3).distinct\n\n    triggers.foreach { trigger =>\n      assetHelper.withCleaner(wsk.trigger, trigger) { (trigger, name) =>\n        trigger.create(name)\n      }\n    }\n\n    actions.foreach {\n      case (actionName, _, file) =>\n        assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n          action.create(name, Some(file))\n        }\n    }\n\n    rules.foreach {\n      case (ruleName, triggerName, action) =>\n        assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n          rule.create(name, triggerName, action._2)\n        }\n    }\n  }\n\n  behavior of \"Whisk rules\"\n\n  it should \"invoke the action attached on trigger fire, creating an activation for each entity including the cause\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val ruleName = withTimestamp(\"r1to1\")\n    val triggerName = withTimestamp(\"t1to1\")\n    val actionName = withTimestamp(\"a1 to 1\") // spaces in name intended for greater test coverage\n\n    ruleSetup(Seq((ruleName, triggerName, (actionName, actionName, defaultAction))), assetHelper)\n\n    val run = wsk.trigger.fire(triggerName, Map(\"payload\" -> testString.toJson))\n\n    withActivation(wsk.activation, run) { triggerActivation =>\n      triggerActivation.cause shouldBe None\n\n      val ruleActivations = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult])\n      ruleActivations should have size 1\n      val ruleActivation = ruleActivations.head\n      ruleActivation.success shouldBe true\n      ruleActivation.statusCode shouldBe 0\n\n      withActivation(wsk.activation, ruleActivation.activationId) { actionActivation =>\n        actionActivation.response.result shouldBe Some(testResult)\n        actionActivation.cause shouldBe None\n      }\n    }\n  }\n\n  it should \"invoke the action from a package attached on trigger fire, creating an activation for each entity including the cause\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val ruleName = withTimestamp(\"pr1to1\")\n    val triggerName = withTimestamp(\"pt1to1\")\n    val pkgName = withTimestamp(\"rule pkg\") // spaces in name intended to test uri path encoding\n    val actionName = withTimestamp(\"a1 to 1\")\n    val pkgActionName = s\"$pkgName/$actionName\"\n\n    assetHelper.withCleaner(wsk.pkg, pkgName) { (pkg, name) =>\n      pkg.create(name)\n    }\n\n    ruleSetup(Seq((ruleName, triggerName, (pkgActionName, pkgActionName, defaultAction))), assetHelper)\n\n    val now = Instant.now\n    val run = wsk.trigger.fire(triggerName, Map(\"payload\" -> testString.toJson))\n\n    withActivation(wsk.activation, run) { triggerActivation =>\n      triggerActivation.cause shouldBe None\n\n      val ruleActivations = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult])\n      ruleActivations should have size 1\n      val ruleActivation = ruleActivations.head\n      ruleActivation.success shouldBe true\n      ruleActivation.statusCode shouldBe 0\n\n      withActivation(wsk.activation, ruleActivation.activationId) { actionActivation =>\n        actionActivation.response.result shouldBe Some(testResult)\n      }\n    }\n  }\n\n  it should \"invoke the action from a package binding attached on trigger fire, creating an activation for each entity including the cause\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val ruleName = withTimestamp(\"pr1to1\")\n    val triggerName = withTimestamp(\"pt1to1\")\n    val pkgName = withTimestamp(\"rule pkg\") // spaces in name intended to test uri path encoding\n    val pkgBindingName = withTimestamp(\"rule pkg binding\")\n    val actionName = withTimestamp(\"a1 to 1\")\n    val pkgActionName = s\"$pkgName/$actionName\"\n\n    assetHelper.withCleaner(wsk.pkg, pkgName) { (pkg, name) =>\n      pkg.create(name)\n    }\n\n    assetHelper.withCleaner(wsk.pkg, pkgBindingName) { (pkg, name) =>\n      pkg.bind(pkgName, pkgBindingName)\n    }\n\n    ruleSetup(Seq((ruleName, triggerName, (pkgActionName, s\"$pkgBindingName/$actionName\", defaultAction))), assetHelper)\n\n    val run = wsk.trigger.fire(triggerName, Map(\"payload\" -> testString.toJson))\n\n    withActivation(wsk.activation, run) { triggerActivation =>\n      triggerActivation.cause shouldBe None\n\n      val ruleActivations = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult])\n      ruleActivations should have size 1\n      val ruleActivation = ruleActivations.head\n      ruleActivation.success shouldBe true\n      ruleActivation.statusCode shouldBe 0\n\n      withActivation(wsk.activation, ruleActivation.activationId) { actionActivation =>\n        actionActivation.response.result shouldBe Some(testResult)\n        actionActivation.cause shouldBe None\n      }\n    }\n  }\n\n  it should \"not activate an action if the rule is deleted when the trigger is fired\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val ruleName = withTimestamp(\"ruleDelete\")\n      val triggerName = withTimestamp(\"ruleDeleteTrigger\")\n      val actionName = withTimestamp(\"ruleDeleteAction\")\n\n      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, name) =>\n        trigger.create(name)\n      }\n      assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n        action.create(name, Some(defaultAction))\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName, confirmDelete = false) { (rule, name) =>\n        rule.create(name, triggerName, actionName)\n      }\n\n      val first = wsk.trigger.fire(triggerName, Map(\"payload\" -> \"bogus\".toJson))\n      wsk.rule.delete(ruleName)\n      val second = wsk.trigger.fire(triggerName, Map(\"payload\" -> \"bogus2\".toJson))\n\n      withActivation(wsk.activation, first)(activation => activation.logs.get should have size 1)\n    // there won't be an activation for the second fire since there is no rule\n  }\n\n  it should \"enable and disable a rule and check action is activated only when rule is enabled\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val ruleName = withTimestamp(\"ruleDisable\")\n    val triggerName = withTimestamp(\"ruleDisableTrigger\")\n    val actionName = withTimestamp(\"ruleDisableAction\")\n\n    ruleSetup(Seq((ruleName, triggerName, (actionName, actionName, defaultAction))), assetHelper)\n\n    val first = wsk.trigger.fire(triggerName, Map(\"payload\" -> testString.toJson))\n    wsk.rule.disable(ruleName)\n    val second = wsk.trigger.fire(triggerName, Map(\"payload\" -> s\"$testString with added words\".toJson))\n    wsk.rule.enable(ruleName)\n    val third = wsk.trigger.fire(triggerName, Map(\"payload\" -> testString.toJson))\n\n    withActivation(wsk.activation, first) { triggerActivation =>\n      val ruleActivations = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult])\n      ruleActivations should have size 1\n      val ruleActivation = ruleActivations.head\n      withActivation(wsk.activation, ruleActivation.activationId) { actionActivation =>\n        actionActivation.response.result shouldBe Some(testResult)\n      }\n    }\n\n    // second fire will not write an activation\n\n    withActivation(wsk.activation, third) { triggerActivation =>\n      val ruleActivations = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult])\n      ruleActivations should have size 1\n      val ruleActivation = ruleActivations.head\n      withActivation(wsk.activation, ruleActivation.activationId) { actionActivation =>\n        actionActivation.response.result shouldBe Some(testResult)\n      }\n    }\n  }\n\n  it should \"be able to recreate a rule with the same name and match it successfully\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val ruleName = withTimestamp(\"ruleRecreate\")\n      val triggerName1 = withTimestamp(\"ruleRecreateTrigger1\")\n      val triggerName2 = withTimestamp(\"ruleRecreateTrigger2\")\n      val actionName = withTimestamp(\"ruleRecreateAction\")\n\n      assetHelper.withCleaner(wsk.trigger, triggerName1) { (trigger, name) =>\n        trigger.create(name)\n      }\n      assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>\n        action.create(name, Some(defaultAction))\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName, confirmDelete = false) { (rule, name) =>\n        rule.create(name, triggerName1, actionName)\n      }\n\n      wsk.rule.delete(ruleName)\n\n      assetHelper.withCleaner(wsk.trigger, triggerName2) { (trigger, name) =>\n        trigger.create(name)\n      }\n      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n        rule.create(name, triggerName2, actionName)\n      }\n\n      val first = wsk.trigger.fire(triggerName2, Map(\"payload\" -> testString.toJson))\n      withActivation(wsk.activation, first) { triggerActivation =>\n        val ruleActivations = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult])\n        ruleActivations should have size 1\n        val ruleActivation = ruleActivations.head\n        withActivation(wsk.activation, ruleActivation.activationId) { actionActivation =>\n          actionActivation.response.result shouldBe Some(testResult)\n        }\n      }\n  }\n\n  it should \"connect two triggers via rules to one action and activate it accordingly\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val triggerName1 = withTimestamp(\"t2to1a\")\n      val triggerName2 = withTimestamp(\"t2to1b\")\n      val actionName = withTimestamp(\"a2to1\")\n\n      ruleSetup(\n        Seq(\n          (\"r2to1a\", triggerName1, (actionName, actionName, defaultAction)),\n          (\"r2to1b\", triggerName2, (actionName, actionName, defaultAction))),\n        assetHelper)\n\n      val testPayloads = Seq(\"got three words\", \"got four words, period\")\n      val runs = testPayloads.map(payload => wsk.trigger.fire(triggerName1, Map(\"payload\" -> payload.toJson)))\n\n      runs.zip(testPayloads).foreach {\n        case (run, payload) =>\n          withActivation(wsk.activation, run) { triggerActivation =>\n            val ruleActivations = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult])\n            ruleActivations should have size 1\n            val ruleActivation = ruleActivations.head\n            withActivation(wsk.activation, ruleActivation.activationId) { actionActivation =>\n              actionActivation.response.result shouldBe Some(JsObject(\"count\" -> payload.split(\" \").length.toJson))\n            }\n          }\n      }\n  }\n\n  it should \"connect one trigger to two different actions, invoking them both eventually\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val triggerName = withTimestamp(\"t1to2\")\n      val actionName1 = withTimestamp(\"a1to2a\")\n      val actionName2 = withTimestamp(\"a1to2b\")\n\n      ruleSetup(\n        Seq(\n          (\"r1to2a\", triggerName, (actionName1, actionName1, defaultAction)),\n          (\"r1to2b\", triggerName, (actionName2, actionName2, secondAction))),\n        assetHelper)\n\n      val run = wsk.trigger.fire(triggerName, Map(\"payload\" -> testString.toJson))\n\n      withActivation(wsk.activation, run) { triggerActivation =>\n        val ruleActivations = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult])\n        ruleActivations should have size 2\n\n        val action1Result = ruleActivations.find(_.action.contains(actionName1)).get\n        val action2Result = ruleActivations.find(_.action.contains(actionName2)).get\n\n        withActivation(wsk.activation, action1Result.activationId) { actionActivation =>\n          actionActivation.response.result shouldBe Some(testResult)\n        }\n        withActivation(wsk.activation, action2Result.activationId) { actionActivation =>\n          actionActivation.logs.get.mkString(\" \") should include(s\"hello, $testString\")\n        }\n      }\n  }\n\n  it should \"connect two triggers to two different actions, invoking them both eventually\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val triggerName1 = withTimestamp(\"t1to1a\")\n      val triggerName2 = withTimestamp(\"t1to1b\")\n      val actionName1 = withTimestamp(\"a1to1a\")\n      val actionName2 = withTimestamp(\"a1to1b\")\n\n      ruleSetup(\n        Seq(\n          (\"r2to2a\", triggerName1, (actionName1, actionName1, defaultAction)),\n          (\"r2to2b\", triggerName1, (actionName2, actionName2, secondAction)),\n          (\"r2to2c\", triggerName2, (actionName1, actionName1, defaultAction)),\n          (\"r2to2d\", triggerName2, (actionName2, actionName2, secondAction))),\n        assetHelper)\n\n      val testPayloads = Seq(\"got three words\", \"got four words, period\")\n      val runs = Seq(triggerName1, triggerName2).zip(testPayloads).map {\n        case (trigger, payload) =>\n          payload -> wsk.trigger.fire(trigger, Map(\"payload\" -> payload.toJson))\n      }\n\n      runs.foreach {\n        case (payload, run) =>\n          withActivation(wsk.activation, run) { triggerActivation =>\n            val ruleActivations = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult])\n            ruleActivations should have size 2 // each trigger has 2 actions attached\n\n            val action1Result = ruleActivations.find(_.action.contains(actionName1)).get\n            val action2Result = ruleActivations.find(_.action.contains(actionName2)).get\n\n            withActivation(wsk.activation, action1Result.activationId) { actionActivation =>\n              actionActivation.response.result shouldBe Some(JsObject(\"count\" -> payload.split(\" \").length.toJson))\n            }\n            withActivation(wsk.activation, action2Result.activationId) { actionActivation =>\n              actionActivation.logs.get.mkString(\" \") should include(s\"hello, $payload\")\n            }\n          }\n      }\n  }\n\n  it should \"disable a rule and check its status is displayed when listed\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val ruleName = withTimestamp(\"ruleDisable\")\n      val ruleName2 = withTimestamp(\"ruleEnable\")\n      val triggerName = withTimestamp(\"ruleDisableTrigger\")\n      val actionName = withTimestamp(\"ruleDisableAction\")\n\n      ruleSetup(\n        Seq(\n          (ruleName, triggerName, (actionName, actionName, defaultAction)),\n          (ruleName2, triggerName, (actionName, actionName, defaultAction))),\n        assetHelper)\n\n      wsk.rule.disable(ruleName)\n      val ruleListResult = wsk.rule.list()\n      verifyRuleList(ruleListResult, ruleName2, ruleName)\n  }\n\n  def verifyRuleList(ruleListResult: RunResult, ruleNameEnable: String, ruleName: String) = {\n    val ruleList = ruleListResult.stdout\n    val listOutput = ruleList.linesIterator\n    listOutput.find(_.contains(ruleNameEnable)).get should (include(ruleNameEnable) and include(\"active\"))\n    listOutput.find(_.contains(ruleName)).get should (include(ruleName) and include(\"inactive\"))\n    ruleList should not include \"Unknown\"\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskSequenceTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport java.time.Instant\nimport java.util.Date\n\nimport io.restassured.RestAssured\n\nimport scala.concurrent.duration.DurationInt\nimport scala.language.postfixOps\nimport scala.util.matching.Regex\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\nimport common._\nimport common.TestUtils._\nimport common.rest.WskRestOperations\nimport org.apache.openwhisk.core.entity.WhiskActivation\nimport spray.json._\nimport spray.json.DefaultJsonProtocol._\nimport system.rest.RestUtil\nimport org.apache.openwhisk.http.Messages._\n\n/**\n * Tests sequence execution\n */\n@RunWith(classOf[JUnitRunner])\nclass WskSequenceTests extends TestHelpers with WskTestHelpers with StreamLogging with RestUtil with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  val wsk: WskOperations = new WskRestOperations\n  val allowedActionDuration = 120 seconds\n  val shortDuration = 10 seconds\n\n  behavior of \"Wsk Sequence\"\n\n  it should \"invoke a sequence with normal payload and payload with error field\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val name = \"sequence\"\n      val actions = Seq(\"split\", \"sort\", \"head\", \"cat\")\n      for (actionName <- actions) {\n        val file = TestUtils.getTestActionFilename(s\"$actionName.js\")\n        assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n          action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))\n        }\n      }\n\n      assetHelper.withCleaner(wsk.action, name) {\n        val sequence = actions.mkString(\",\")\n        (action, _) =>\n          action.create(name, Some(sequence), kind = Some(\"sequence\"), timeout = Some(allowedActionDuration))\n      }\n\n      val now = \"it is now \" + new Date()\n      val args = Array(\"what time is it?\", now)\n      val run = wsk.action.invoke(name, Map(\"payload\" -> args.mkString(\"\\n\").toJson))\n      withActivation(wsk.activation, run, totalWait = 4 * allowedActionDuration) { activation =>\n        checkSequenceLogsAndAnnotations(activation, 4) // 4 activations in this sequence\n        activation.cause shouldBe None // topmost sequence\n        val result = activation.response.result.get\n        result.asJsObject.fields.get(\"payload\") shouldBe defined\n        result.asJsObject.fields.get(\"length\") should not be defined\n        result.asJsObject.fields.get(\"lines\") shouldBe Some(JsArray(Vector(now.toJson)))\n      }\n\n      // update action sequence and run it with normal payload\n      val newSequence = Seq(\"split\", \"sort\").mkString(\",\")\n      wsk.action.create(\n        name,\n        Some(newSequence),\n        kind = Some(\"sequence\"),\n        timeout = Some(allowedActionDuration),\n        update = true)\n      val secondrun = wsk.action.invoke(name, Map(\"payload\" -> args.mkString(\"\\n\").toJson))\n      withActivation(wsk.activation, secondrun, totalWait = 2 * allowedActionDuration) { activation =>\n        checkSequenceLogsAndAnnotations(activation, 2) // 2 activations in this sequence\n        val result = activation.response.result.get\n        result.asJsObject.fields.get(\"length\") shouldBe Some(2.toJson)\n        result.asJsObject.fields.get(\"lines\") shouldBe Some(args.sortWith(_.compareTo(_) < 0).toArray.toJson)\n      }\n\n      // run sequence with error in the payload\n      // sequence should run with no problems, error should be ignored in this test case\n      // result of sequence should be identical to previous invocation above\n      val payload = Map(\"error\" -> JsString(\"irrelevant error string\"), \"payload\" -> args.mkString(\"\\n\").toJson)\n      val thirdrun = wsk.action.invoke(name, payload)\n      withActivation(wsk.activation, thirdrun, totalWait = 2 * allowedActionDuration) { activation =>\n        checkSequenceLogsAndAnnotations(activation, 2) // 2 activations in this sequence\n        val result = activation.response.result.get\n        result.asJsObject.fields.get(\"length\") shouldBe Some(2.toJson)\n        result.asJsObject.fields.get(\"lines\") shouldBe Some(args.sortWith(_.compareTo(_) < 0).toArray.toJson)\n      }\n  }\n\n  it should \"invoke a sequence with an enclosing sequence action\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val inner_name = \"inner_sequence\"\n    val outer_name = \"outer_sequence\"\n    val inner_actions = Seq(\"sort\", \"head\")\n    val actions = Seq(\"split\") ++ inner_actions ++ Seq(\"cat\")\n    // create atomic actions\n    for (actionName <- actions) {\n      val file = TestUtils.getTestActionFilename(s\"$actionName.js\")\n      assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n        action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))\n      }\n    }\n\n    // create inner sequence\n    assetHelper.withCleaner(wsk.action, inner_name) {\n      val inner_sequence = inner_actions.mkString(\",\")\n      (action, _) =>\n        action.create(inner_name, Some(inner_sequence), kind = Some(\"sequence\"))\n    }\n\n    // create outer sequence\n    assetHelper.withCleaner(wsk.action, outer_name) {\n      val outer_sequence = Seq(\"split\", \"inner_sequence\", \"cat\").mkString(\",\")\n      (action, _) =>\n        action.create(outer_name, Some(outer_sequence), kind = Some(\"sequence\"))\n    }\n\n    val now = \"it is now \" + new Date()\n    val args = Array(\"what time is it?\", now)\n    val run = wsk.action.invoke(outer_name, Map(\"payload\" -> args.mkString(\"\\n\").toJson))\n    withActivation(wsk.activation, run, totalWait = 4 * allowedActionDuration) { activation =>\n      checkSequenceLogsAndAnnotations(activation, 3) // 3 activations in this sequence\n      activation.cause shouldBe None // topmost sequence\n      val result = activation.response.result.get\n      result.asJsObject.fields.get(\"payload\") shouldBe defined\n      result.asJsObject.fields.get(\"length\") should not be defined\n      result.asJsObject.fields.get(\"lines\") shouldBe Some(JsArray(Vector(now.toJson)))\n    }\n  }\n\n  /**\n   * s -> echo, x, echo\n   * x -> echo\n   *\n   * update x -> <limit-1> echo -- should work\n   * run s -> should stop after <limit> echo\n   *\n   * This confirms that a dynamic check on the sequence length holds within the system limit.\n   * This is different from creating a long sequence up front which will report a length error at create time.\n   */\n  it should \"replace atomic component in a sequence that is too long and report invoke error\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val xName = \"xSequence\"\n    val sName = \"sSequence\"\n    val echo = \"echo\"\n\n    // create echo action\n    val file = TestUtils.getTestActionFilename(s\"$echo.js\")\n    assetHelper.withCleaner(wsk.action, echo) { (action, actionName) =>\n      action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))\n    }\n    // create x\n    assetHelper.withCleaner(wsk.action, xName) { (action, seqName) =>\n      action.create(seqName, Some(echo), kind = Some(\"sequence\"))\n    }\n    // create s\n    assetHelper.withCleaner(wsk.action, sName) { (action, seqName) =>\n      action.create(seqName, Some(s\"$echo,$xName,$echo\"), kind = Some(\"sequence\"))\n    }\n\n    // invoke s\n    val now = \"it is now \" + new Date()\n    val args = Array(\"what time is it?\", now)\n    val argsJson = args.mkString(\"\\n\").toJson\n    val run = wsk.action.invoke(sName, Map(\"payload\" -> argsJson))\n\n    withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) { activation =>\n      checkSequenceLogsAndAnnotations(activation, 3) // 3 activations in this sequence\n      val result = activation.response.result.get\n      result.asJsObject.fields.get(\"payload\") shouldBe Some(argsJson)\n    }\n    // update x with limit echo\n    val limit: Int = {\n      val response = RestAssured.given.config(sslconfig).get(getServiceURL)\n      response.statusCode should be(200)\n      response.body.asString.parseJson.asJsObject.fields(\"limits\").asJsObject.fields(\"sequence_length\").convertTo[Int]\n    }\n    val manyEcho = for (i <- 1 to limit) yield echo\n\n    wsk.action.create(xName, Some(manyEcho.mkString(\",\")), kind = Some(\"sequence\"), update = true)\n\n    val updateRun = wsk.action.invoke(sName, Map(\"payload\" -> argsJson))\n    withActivation(wsk.activation, updateRun, totalWait = 2 * allowedActionDuration) { activation =>\n      activation.response.status shouldBe (\"application error\")\n      checkSequenceLogsAndAnnotations(activation, 2)\n      val result = activation.response.result.get\n      result.asJsObject.fields.get(\"error\") shouldBe Some(JsString(sequenceIsTooLong))\n      // check that inner sequence had only (limit - 1) activations\n      val innerSeq = activation.logs.get(1) // the id of the inner sequence activation\n      val getInnerSeq = wsk.activation.get(Some(innerSeq))\n      withActivation(wsk.activation, getInnerSeq, totalWait = allowedActionDuration) { innerSeqActivation =>\n        innerSeqActivation.logs.get.size shouldBe (limit - 1)\n        innerSeqActivation.cause shouldBe Some(activation.activationId)\n      }\n    }\n  }\n\n  it should \"create and run a sequence in a package with parameters\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val sName = \"sSequence\"\n\n      // create a package\n      val pkgName = \"echopackage\"\n      val pkgStr = \"LonelyPackage\"\n      assetHelper.withCleaner(wsk.pkg, pkgName) { (pkg, name) =>\n        pkg.create(name, Map(\"payload\" -> JsString(pkgStr)))\n      }\n      val helloName = \"hello\"\n      val helloWithPkg = s\"$pkgName/$helloName\"\n\n      // create hello action in package\n      val file = TestUtils.getTestActionFilename(s\"$helloName.js\")\n      val actionStr = \"AtomicAction\"\n      assetHelper.withCleaner(wsk.action, helloWithPkg) { (action, actionName) =>\n        action.create(\n          name = actionName,\n          artifact = Some(file),\n          timeout = Some(allowedActionDuration),\n          parameters = Map(\"payload\" -> JsString(actionStr)))\n      }\n      // create s\n      assetHelper.withCleaner(wsk.action, sName) { (action, seqName) =>\n        action.create(seqName, Some(helloWithPkg), kind = Some(\"sequence\"))\n      }\n      val run = wsk.action.invoke(sName)\n      // action params trump package params\n      checkLogsAtomicAction(0, run, new Regex(actionStr))\n      // run with some parameters\n      val sequenceStr = \"AlmightySequence\"\n      val sequenceParamRun = wsk.action.invoke(sName, parameters = Map(\"payload\" -> JsString(sequenceStr)))\n      // sequence param should be passed to the first atomic action and trump the action params\n      checkLogsAtomicAction(0, sequenceParamRun, new Regex(sequenceStr))\n      // update action and remove the params by sending an unused param that overrides previous params\n      wsk.action.create(\n        name = helloWithPkg,\n        artifact = Some(file),\n        timeout = Some(allowedActionDuration),\n        parameters = Map(\"param\" -> JsString(\"irrelevant\")),\n        update = true)\n      val sequenceParamSecondRun = wsk.action.invoke(sName, parameters = Map(\"payload\" -> JsString(sequenceStr)))\n      // sequence param should be passed to the first atomic action and trump the package params\n      checkLogsAtomicAction(0, sequenceParamSecondRun, new Regex(sequenceStr))\n      val pkgParamRun = wsk.action.invoke(sName)\n      // no sequence params, no atomic action params used, the pkg params should show up\n      checkLogsAtomicAction(0, pkgParamRun, new Regex(pkgStr))\n  }\n\n  it should \"run a sequence with an action in a package binding with parameters\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val packageName = \"package1\"\n      val bindName = \"package2\"\n      val actionName = \"print\"\n      val packageActionName = packageName + \"/\" + actionName\n      val bindActionName = bindName + \"/\" + actionName\n      val packageParams = Map(\"key1a\" -> \"value1a\".toJson, \"key1b\" -> \"value1b\".toJson)\n      val bindParams = Map(\"key2a\" -> \"value2a\".toJson, \"key1b\" -> \"value2b\".toJson)\n      val actionParams = Map(\"key0\" -> \"value0\".toJson)\n      val file = TestUtils.getTestActionFilename(\"printParams.js\")\n      assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n        pkg.create(packageName, packageParams)\n      }\n      assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>\n        action.create(packageActionName, Some(file), parameters = actionParams)\n      }\n      assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>\n        pkg.bind(packageName, bindName, bindParams)\n      }\n      // sequence\n      val sName = \"sequenceWithBindingParams\"\n      assetHelper.withCleaner(wsk.action, sName) { (action, seqName) =>\n        action.create(seqName, Some(bindActionName), kind = Some(\"sequence\"))\n      }\n      // Check that inherited parameters are passed to the action.\n      val now = new Date().toString()\n      val run = wsk.action.invoke(sName, Map(\"payload\" -> now.toJson))\n      // action params trump package params\n      checkLogsAtomicAction(\n        0,\n        run,\n        new Regex(String.format(\".*key0: value0.*key1a: value1a.*key1b: value2b.*key2a: value2a.*payload: %s\", now)))\n  }\n\n  it should \"contain an binding annotation if invoked action is in a package binding\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val ns = wsk.namespace.whois()\n      val packageName = \"package1\"\n      val bindName = \"package2\"\n      val actionName = \"print\"\n      val packageActionName = packageName + \"/\" + actionName\n      val bindActionName = bindName + \"/\" + actionName\n\n      val file = TestUtils.getTestActionFilename(\"echo.js\")\n      assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n        pkg.create(packageName)\n      }\n      assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>\n        action.create(packageActionName, Some(file))\n      }\n      assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>\n        pkg.bind(packageName, bindName)\n      }\n      // sequence\n      val sequenceActionName = \"sequenceWithBinding\"\n      val sName = packageName + \"/\" + sequenceActionName\n      val bName = bindName + \"/\" + sequenceActionName\n      assetHelper.withCleaner(wsk.action, sName) { (action, seqName) =>\n        action.create(seqName, Some(bindActionName), kind = Some(\"sequence\"))\n      }\n\n      val run = wsk.action.invoke(bName)\n      withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) { activation =>\n        val binding = activation.getAnnotationValue(WhiskActivation.bindingAnnotation)\n        binding shouldBe defined\n        binding.get shouldBe JsString(ns + \"/\" + bindName)\n\n        for (id <- activation.logs.get) {\n          withActivation(\n            wsk.activation,\n            id,\n            initialWait = 1 second,\n            pollPeriod = 60 seconds,\n            totalWait = allowedActionDuration) { componentActivation =>\n            val binding = componentActivation.getAnnotationValue(WhiskActivation.bindingAnnotation)\n            binding shouldBe defined\n            binding.get shouldBe JsString(ns + \"/\" + bindName)\n          }\n        }\n      }\n  }\n\n  /**\n   * s -> apperror, echo\n   * only apperror should run\n   */\n  it should \"stop execution of a sequence (with no payload) on error\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val sName = \"sSequence\"\n      val apperror = \"applicationError\"\n      val echo = \"echo\"\n\n      // create actions\n      val actions = Seq(apperror, echo)\n      for (actionName <- actions) {\n        val file = TestUtils.getTestActionFilename(s\"$actionName.js\")\n        assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>\n          action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))\n        }\n      }\n      // create sequence s\n      assetHelper.withCleaner(wsk.action, sName) { (action, seqName) =>\n        action.create(seqName, artifact = Some(actions.mkString(\",\")), kind = Some(\"sequence\"))\n      }\n      // run sequence s with no payload\n      val run = wsk.action.invoke(sName)\n      withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) { activation =>\n        checkSequenceLogsAndAnnotations(activation, 1) // only the first action should have run\n        activation.response.success shouldBe (false)\n        // the status should be error\n        activation.response.status shouldBe (\"application error\")\n        val result = activation.response.result.get\n        // the result of the activation should be the application error\n        result shouldBe (JsObject(\"error\" -> JsString(\"This error thrown on purpose by the action.\")))\n      }\n  }\n\n  /**\n   * s -> echo, initforever\n   * should run both, but error\n   */\n  it should \"propagate execution error (timeout) from atomic action to sequence\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val sName = \"sSequence\"\n      val initforever = \"initforever\"\n      val echo = \"echo\"\n\n      // create actions\n      val actions = Seq(echo, initforever)\n      // timeouts for the action; make the one for initforever short\n      val timeout = Map(echo -> allowedActionDuration, initforever -> shortDuration)\n      for (actionName <- actions) {\n        val file = TestUtils.getTestActionFilename(s\"$actionName.js\")\n        assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>\n          action.create(name = actionName, artifact = Some(file), timeout = Some(timeout(actionName)))\n        }\n      }\n      // create sequence s\n      assetHelper.withCleaner(wsk.action, sName) { (action, seqName) =>\n        action.create(seqName, artifact = Some(actions.mkString(\",\")), kind = Some(\"sequence\"))\n      }\n      // run sequence s with no payload\n      val run = wsk.action.invoke(sName)\n      withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) { activation =>\n        checkSequenceLogsAndAnnotations(activation, 2) // 2 actions\n        activation.response.success shouldBe (false)\n        // the status should be error\n        //activation.response.status shouldBe(\"application error\")\n        val result = activation.response.result.get\n        // the result of the activation should be timeout or an abnormal initialization\n        result should (be(JsObject(\"error\" -> timedoutActivation(shortDuration, true).toJson)) or be(\n          JsObject(\"error\" -> abnormalInitialization.toJson)))\n      }\n  }\n\n  /**\n   * s -> echo, sleep\n   * sleep sleeps for 90s, timeout set at 120s\n   * should run both, the blocking call should be transformed into a non-blocking call, but finish executing\n   */\n  it should \"execute a sequence in blocking fashion and finish execution even if longer than blocking response timeout\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val sName = \"sSequence\"\n    val sleep = \"sleep\"\n    val echo = \"echo\"\n\n    // create actions\n    val actions = Seq(echo, sleep)\n    for (actionName <- actions) {\n      val file = TestUtils.getTestActionFilename(s\"$actionName.js\")\n      assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>\n        action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))\n      }\n    }\n    // create sequence s\n    assetHelper.withCleaner(wsk.action, sName) { (action, seqName) =>\n      action.create(seqName, artifact = Some(actions.mkString(\",\")), kind = Some(\"sequence\"))\n    }\n    // run sequence s with sleep time\n    val sleepTime = 90 seconds\n    val run = wsk.action.invoke(\n      sName,\n      parameters = Map(\"sleepTimeInMs\" -> sleepTime.toMillis.toJson),\n      blocking = true,\n      expectedExitCode = ACCEPTED)\n    withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 3 * allowedActionDuration) { activation =>\n      checkSequenceLogsAndAnnotations(activation, 2) // 2 actions\n      activation.response.success shouldBe (true)\n      val result = activation.response.result.get\n      result.toString should include(\"\"\"Terminated successfully after around\"\"\")\n    }\n  }\n\n  /**\n   * sequence s -> echo\n   * t trigger with payload\n   * rule r: t -> s\n   */\n  it should \"execute a sequence that is part of a rule and pass the trigger parameters to the sequence\" in withAssetCleaner(\n    wskprops) { (wp, assetHelper) =>\n    val seqName = \"seqRule\"\n    val actionName = \"echo\"\n    val triggerName = \"trigSeq\"\n    val ruleName = \"ruleSeq\"\n\n    val itIsNow = \"it is now \" + new Date()\n    // set up all entities\n    // trigger\n    val triggerPayload: Map[String, JsValue] = Map(\"payload\" -> JsString(itIsNow))\n    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, name) =>\n      trigger.create(name, parameters = triggerPayload)\n    }\n    // action\n    val file = TestUtils.getTestActionFilename(s\"$actionName.js\")\n    assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>\n      action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))\n    }\n    // sequence\n    assetHelper.withCleaner(wsk.action, seqName) { (action, seqName) =>\n      action.create(seqName, artifact = Some(actionName), kind = Some(\"sequence\"))\n    }\n    // rule\n    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>\n      rule.create(name, triggerName, seqName)\n    }\n    // fire trigger\n    val run = wsk.trigger.fire(triggerName)\n    // check that the sequence was invoked and that the echo action produced the expected result\n    checkEchoSeqRuleResult(run, seqName, JsObject(triggerPayload))\n    // fire trigger with new payload\n    val now = \"this is now: \" + Instant.now\n    val newPayload = Map(\"payload\" -> JsString(now))\n    val newRun = wsk.trigger.fire(triggerName, newPayload)\n    checkEchoSeqRuleResult(newRun, seqName, JsObject(newPayload))\n  }\n\n  it should \"run a sub-action even if it is updated while the sequence action is running\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val seqName = \"sequence\"\n      val sleep = \"sleep\"\n      val echo = \"echo\"\n      val slowInvokeDuration = 5.seconds\n\n      // create echo action\n      val echoFile = TestUtils.getTestActionFilename(s\"$echo.js\")\n      assetHelper.withCleaner(wsk.action, echo) { (action, actionName) =>\n        action.create(name = actionName, artifact = Some(echoFile), timeout = Some(allowedActionDuration))\n      }\n      // create sleep action\n      val sleepFile = TestUtils.getTestActionFilename(s\"$sleep.js\")\n      assetHelper.withCleaner(wsk.action, sleep) { (action, actionName) =>\n        action.create(\n          name = sleep,\n          artifact = Some(sleepFile),\n          parameters = Map(\"sleepTimeInMs\" -> slowInvokeDuration.toMillis.toJson),\n          timeout = Some(allowedActionDuration))\n      }\n\n      // create sequence\n      assetHelper.withCleaner(wsk.action, seqName) { (action, seqName) =>\n        action.create(seqName, Some(s\"$sleep,$echo\"), kind = Some(\"sequence\"))\n      }\n      val run = wsk.action.invoke(seqName)\n\n      // update the sub-action before the sequence action invokes it\n      wsk.action.create(name = echo, artifact = None, annotations = Map(\"a\" -> JsString(\"A\")), update = true)\n      wsk.action.invoke(echo)\n\n      wsk.action.create(name = echo, artifact = None, annotations = Map(\"b\" -> JsString(\"B\")), update = true)\n      wsk.action.invoke(echo)\n\n      withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) { activation =>\n        activation.response.status shouldBe \"success\"\n      }\n  }\n\n  it should \"invoke a sequence which supports array result\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val name = \"sequence-array\"\n    val actions = Seq(\"split-array\", \"sort-array\")\n    for (actionName <- actions) {\n      val file = TestUtils.getTestActionFilename(s\"$actionName.js\")\n      assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>\n        action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))\n      }\n    }\n\n    assetHelper.withCleaner(wsk.action, name) {\n      val sequence = actions.mkString(\",\")\n      (action, _) =>\n        action.create(name, Some(sequence), kind = Some(\"sequence\"), timeout = Some(allowedActionDuration))\n    }\n\n    val args = Array(\"bbb\", \"aaa\", \"ccc\")\n    val run = wsk.action.invoke(name, Map(\"payload\" -> args.mkString(\"\\n\").toJson))\n    withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) { activation =>\n      checkSequenceLogsAndAnnotations(activation, 2) // 2 activations in this sequence\n      activation.cause shouldBe None // topmost sequence\n      activation.response.result shouldBe Some(JsArray(JsString(\"aaa\"), JsString(\"bbb\"), JsString(\"ccc\")))\n    }\n  }\n\n  /**\n   * checks the result of an echo sequence connected to a trigger through a rule\n   * @param triggerFireRun the run result of firing the trigger\n   * @param seqName the sequence name\n   * @param triggerPayload the payload used for the trigger (that should be reflected in the sequence result)\n   */\n  private def checkEchoSeqRuleResult(triggerFireRun: RunResult, seqName: String, triggerPayload: JsObject) = {\n    withActivation(wsk.activation, triggerFireRun) { triggerActivation =>\n      val ruleActivation = triggerActivation.logs.get.map(_.parseJson.convertTo[RuleActivationResult]).head\n      withActivation(wsk.activation, ruleActivation.activationId) { actionActivation =>\n        actionActivation.response.result match {\n          case Some(result) =>\n            val (_, part2) = result.asJsObject.fields partition (p => p._1 == \"__ow_headers\") // excluding headers\n            JsObject(part2) shouldBe triggerPayload\n          case others =>\n            fail(s\"no result found: $others\")\n\n        }\n        actionActivation.cause shouldBe None\n      }\n    }\n  }\n\n  /**\n   * checks logs for the activation of a sequence (length/size and ids)\n   * checks that the cause field for composing atomic actions is set properly\n   * checks duration\n   * checks memory\n   */\n  private def checkSequenceLogsAndAnnotations(activation: ActivationResult, size: Int) = {\n    activation.logs shouldBe defined\n    // check that the logs are what they are supposed to be (activation ids)\n    // check that the cause field is properly set for these activations\n    activation.logs.get.size shouldBe (size) // the number of activations in this sequence\n    var totalTime: Long = 0\n    var maxMemory: Long = 0\n    for (id <- activation.logs.get) {\n      withActivation(\n        wsk.activation,\n        id,\n        initialWait = 1 second,\n        pollPeriod = 60 seconds,\n        totalWait = allowedActionDuration) { componentActivation =>\n        componentActivation.cause shouldBe defined\n        componentActivation.cause.get shouldBe (activation.activationId)\n        // check waitTime\n        val waitTime = componentActivation.getAnnotationValue(\"waitTime\")\n        waitTime shouldBe defined\n        // check causedBy\n        val causedBy = componentActivation.getAnnotationValue(\"causedBy\")\n        causedBy shouldBe defined\n        causedBy.get shouldBe (JsString(\"sequence\"))\n        totalTime += componentActivation.duration\n        // extract memory\n        val mem = extractMemoryAnnotation(componentActivation)\n        maxMemory = maxMemory max mem\n      }\n    }\n    // extract duration\n    activation.duration shouldBe (totalTime)\n    // extract memory\n    activation.annotations shouldBe defined\n    val memory = extractMemoryAnnotation(activation)\n    memory shouldBe (maxMemory)\n  }\n\n  /** checks that the logs of the idx-th atomic action from a sequence contains logsStr */\n  private def checkLogsAtomicAction(atomicActionIdx: Int, run: RunResult, regex: Regex): Unit = {\n    withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) { activation =>\n      checkSequenceLogsAndAnnotations(activation, 1)\n      val componentId = activation.logs.get(atomicActionIdx)\n      val getComponentActivation = wsk.activation.get(Some(componentId))\n      withActivation(wsk.activation, getComponentActivation, totalWait = allowedActionDuration) { componentActivation =>\n        withClue(componentActivation) {\n          componentActivation.logs shouldBe defined\n          val logs = componentActivation.logs.get.mkString(\" \")\n          regex.findFirstIn(logs) shouldBe defined\n        }\n      }\n    }\n  }\n\n  private def extractMemoryAnnotation(activation: ActivationResult): Long = {\n    val limits = activation.getAnnotationValue(\"limits\")\n    limits shouldBe defined\n    limits.get.asJsObject.getFields(\"memory\")(0).convertTo[Long]\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/basic/WskUnicodeTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.basic\n\nimport java.io.File\nimport io.restassured.RestAssured\nimport org.junit.runner.RunWith\nimport org.scalatestplus.junit.JUnitRunner\n\nimport scala.concurrent.duration.DurationInt\nimport common._\nimport common.rest.WskRestOperations\nimport spray.json._\nimport system.rest.RestUtil\n\n@RunWith(classOf[JUnitRunner])\nclass WskUnicodeTests extends TestHelpers with WskTestHelpers with JsHelpers with WskActorSystem with RestUtil {\n\n  implicit val wskprops: common.WskProps = WskProps()\n  val wsk: WskOperations = new WskRestOperations\n\n  val activationMaxDuration = 2.minutes\n  val activationPollDuration = 3.minutes\n\n  import WskUnicodeTests._\n\n  val actionKinds: Iterable[Kind] = {\n    val response = RestAssured.given.config(sslconfig).get(getServiceURL)\n    response.statusCode should be(200)\n\n    val mf = response.body.asString.parseJson.asJsObject.fields(\"runtimes\").asJsObject\n    mf.fields.values.map(_.convertTo[Vector[Kind]]).flatten.filter(!_.deprecated)\n  }\n\n  println(s\"Kinds to test: ${actionKinds.map(_.kind).mkString(\", \")}\")\n\n  def main(kind: String): Option[String] = {\n    if (kind.startsWith(\"java\")) {\n      Some(\"Unicode\")\n    } else if (kind.contains(\"dotnet\")) {\n      Some(\"Apache.OpenWhisk.UnicodeTests.Dotnet::Apache.OpenWhisk.UnicodeTests.Dotnet.Unicode::Main\")\n    } else None\n  }\n\n  def getFileLocation(kind: String): Option[String] = {\n    // the test file is either named kind.txt or kind.bin\n    // one of the two must exist otherwise, fail the test.\n    val prefix = \"unicode.tests\" + File.separator + kind.replace(\":\", \"-\")\n    val txt = new File(TestUtils.getTestActionFilename(s\"$prefix.txt\"))\n    val bin = new File(TestUtils.getTestActionFilename(s\"$prefix.bin\"))\n    if (txt.exists) Some(txt.toString)\n    else if (bin.exists) Some(bin.toString)\n    else {\n      println(s\"WARNING: did not find text or binary action for kind $kind, skipping it\")\n      None\n    }\n  }\n\n  // tolerate missing files rather than throw an exception\n  actionKinds.map(k => (k.kind, getFileLocation(k.kind))).collect {\n    case (actionKind, file @ Some(_)) =>\n      s\"$actionKind action\" should \"Ensure that UTF-8 in supported in source files, input params, logs, and output results\" in withAssetCleaner(\n        wskprops) { (wp, assetHelper) =>\n        val name = s\"unicodeGalore.${actionKind.replace(\":\", \"\")}\"\n\n        assetHelper.withCleaner(wsk.action, name) { (action, _) =>\n          action\n            .create(name, file, main = main(actionKind), kind = Some(actionKind), timeout = Some(activationMaxDuration))\n        }\n\n        withActivation(\n          wsk.activation,\n          wsk.action.invoke(name, parameters = Map(\"delimiter\" -> JsString(\"❄\"))),\n          totalWait = activationPollDuration) { activation =>\n          val response = activation.response\n          response.result.get.asJsObject.fields.get(\"error\") shouldBe empty\n          response.result.get.asJsObject.fields.get(\"winter\") should be(Some(JsString(\"❄ ☃ ❄\")))\n\n          activation.logs.toList.flatten.mkString(\" \") should include(\"❄ ☃ ❄\")\n        }\n      }\n  }\n}\n\nprotected[basic] object WskUnicodeTests extends DefaultJsonProtocol {\n  case class Kind(kind: String, deprecated: Boolean)\n  implicit val serdes: RootJsonFormat[Kind] = jsonFormat2(Kind)\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/rest/ActionSchemaTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.rest\n\nimport scala.util.Success\nimport scala.util.Try\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\nimport io.restassured.RestAssured\nimport common._\nimport common.rest.WskRestOperations\nimport spray.json._\n\n/**\n * Basic tests of API calls for actions\n */\n@RunWith(classOf[JUnitRunner])\nclass ActionSchemaTests\n    extends AnyFlatSpec\n    with Matchers\n    with RestUtil\n    with JsonSchema\n    with WskTestHelpers\n    with WskActorSystem {\n\n  implicit val wskprops = WskProps()\n  val wsk = new WskRestOperations\n  val guestNamespace = wskprops.namespace\n\n  it should \"respond to GET /actions as documented in swagger\" in withAssetCleaner(wskprops) { (wp, assetHelper) =>\n    val packageName = \"samples\"\n    assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n      pkg.create(packageName, shared = Some(true))\n    }\n\n    val auth = WhiskProperties.getBasicAuth\n    val response = RestAssured\n      .given()\n      .config(sslconfig)\n      .auth()\n      .basic(auth.fst, auth.snd)\n      .get(getBaseURL() + s\"/namespaces/$guestNamespace/actions/$packageName/\")\n    assert(response.statusCode() == 200)\n\n    val body = Try { response.body().asString().parseJson }\n    val schema = getJsonSchema(\"EntityBrief\").compactPrint\n\n    body match {\n      case Success(JsArray(actions)) =>\n        // check that each collection result obeys the schema\n        actions.foreach { a =>\n          val aString = a.compactPrint\n          assert(check(aString, schema))\n        }\n\n      case Success(_) =>\n        assert(false, \"response is not an array of actions\")\n\n      case _ =>\n        assert(false, \"response failed to parse: \" + body)\n    }\n  }\n\n  it should \"respond to GET /actions/samples/wordCount as documented in swagger\" in withAssetCleaner(wskprops) {\n    (wp, assetHelper) =>\n      val packageName = \"samples\"\n      val actionName = \"wordCount\"\n      val fullActionName = s\"/$guestNamespace/$packageName/$actionName\"\n      assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>\n        pkg.create(packageName, shared = Some(true))\n      }\n\n      assetHelper.withCleaner(wsk.action, fullActionName) { (action, _) =>\n        action.create(fullActionName, Some(TestUtils.getTestActionFilename(\"wc.js\")))\n      }\n      val auth = WhiskProperties.getBasicAuth\n      val response = RestAssured\n        .given()\n        .config(sslconfig)\n        .auth()\n        .basic(auth.fst, auth.snd)\n        .get(getBaseURL() + s\"/namespaces/$guestNamespace/actions/$packageName/$actionName\")\n      assert(response.statusCode() == 200)\n\n      val body = Try { response.body().asString().parseJson }\n      val schema = getJsonSchema(\"Action\").compactPrint\n\n      body match {\n        case Success(action: JsObject) =>\n          // check that the action obeys the Action model schema\n          val aString = action.compactPrint\n          assert(check(aString, schema))\n\n        case Success(_) =>\n          assert(false, \"response is not a json object\")\n\n        case _ =>\n          assert(false, \"response failed to parse as JSON: \" + body)\n      }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/rest/GoCLINginxTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.rest\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport io.restassured.RestAssured\n\nimport spray.json._\nimport DefaultJsonProtocol._\n\n/**\n * Basic tests of the download link for Go CLI binaries\n */\n@RunWith(classOf[JUnitRunner])\nclass GoCLINginxTests extends AnyFlatSpec with Matchers with RestUtil {\n  val DownloadLinkGoCli = \"cli/go/download\"\n  val ServiceURL = getServiceURL()\n\n  it should s\"respond to all files in root directory\" in {\n    val response = RestAssured.given().config(sslconfig).get(s\"$ServiceURL/$DownloadLinkGoCli\")\n    response.statusCode should be(200)\n    val responseString = response.body.asString\n    responseString should include(\"\"\"<a href=\"content.json\">content.json</a>\"\"\")\n    val responseJSON = RestAssured.given().config(sslconfig).get(s\"$ServiceURL/$DownloadLinkGoCli/content.json\")\n    responseJSON.statusCode should be(200)\n    val cli = responseJSON.body.asString.parseJson.asJsObject\n      .fields(\"cli\")\n      .convertTo[Map[String, Map[String, Map[String, String]]]]\n    cli.foreach {\n      case (os, arch) => responseString should include(s\"\"\"<a href=\"$os/\">$os/</a>\"\"\")\n    }\n  }\n\n  it should \"respond to all operating systems and architectures in HTML index\" in {\n    val responseJSON = RestAssured.given().config(sslconfig).get(s\"$ServiceURL/$DownloadLinkGoCli/content.json\")\n    responseJSON.statusCode should be(200)\n    val cli = responseJSON.body.asString.parseJson.asJsObject\n      .fields(\"cli\")\n      .convertTo[Map[String, Map[String, Map[String, String]]]]\n    cli.foreach {\n      case (os, arch) =>\n        val response = RestAssured.given().config(sslconfig).get(s\"$ServiceURL/$DownloadLinkGoCli/$os\")\n        response.statusCode should be(200)\n        val responseString = response.body.asString\n        arch.foreach {\n          case (arch, path) =>\n            if (arch != \"default\") {\n              responseString should include(s\"\"\"<a href=\"$arch/\">$arch/</a>\"\"\")\n            }\n        }\n    }\n  }\n\n  it should \"respond to the download paths in content.json\" in {\n    val response = RestAssured.given().config(sslconfig).get(s\"$ServiceURL/$DownloadLinkGoCli/content.json\")\n    response.statusCode should be(200)\n    val cli =\n      response.body.asString.parseJson.asJsObject.fields(\"cli\").convertTo[Map[String, Map[String, Map[String, String]]]]\n    cli.values.flatMap(_.values).flatMap(_.values).foreach { path =>\n      RestAssured.given().config(sslconfig).get(s\"$ServiceURL/$DownloadLinkGoCli/$path\").statusCode should be(200)\n    }\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/rest/JsonSchema.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.rest\n\nimport com.github.fge.jsonschema.main.JsonSchemaFactory\nimport com.fasterxml.jackson.databind.ObjectMapper\n\n/**\n * Utilities for dealing with JSON schema\n *\n */\ntrait JsonSchema {\n\n  /**\n   * Check whether a JSON document (represented as a String) conforms to a JSON schema (also a String).\n   *\n   * @return true if the document is valid, false otherwise\n   */\n  def check(doc: String, schema: String): Boolean = {\n    val mapper = new ObjectMapper()\n    val docNode = mapper.readTree(doc)\n    val schemaNode = mapper.readTree(schema)\n\n    val validator = JsonSchemaFactory.byDefault().getValidator\n    validator.validate(schemaNode, docNode).isSuccess\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/rest/JsonSchemaTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.rest\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\n/**\n * Basic tests of API calls for actions\n */\n@RunWith(classOf[JUnitRunner])\nclass JsonSchemaTests extends AnyFlatSpec with Matchers with JsonSchema with RestUtil {\n\n  def TEST_SCHEMA = \"\"\"{\n      \"type\" : \"object\",\n      \"properties\" : {\n        \"price\" : {\"type\" : \"number\"},\n        \"name\" :  {\"type\" : \"string\"}\n       }\n    }\"\"\"\n\n  \"JSON schema validator\" should \"accept a correct object\" in {\n    def GOOD = \"\"\" {\"name\" : \"Eggs\", \"price\" : 34.99} \"\"\"\n    assert(check(GOOD, TEST_SCHEMA))\n  }\n  it should \"reject a bad object\" in {\n    def BAD = \"\"\" {\"name\" : \"Eggs\", \"price\" : \"Invalid\"} \"\"\"\n    assert(!check(BAD, TEST_SCHEMA))\n  }\n\n  it should \"accept a properly structured Action\" in {\n    val schema = getJsonSchema(\"Action\").compactPrint\n    def ACTION = \"\"\" {\"namespace\":\"_\",\n                       | \"name\":\"foo\",\n                       | \"version\":\"1.1.1\",\n                       | \"publish\":false,\n                       | \"exec\":{ \"code\": \"foo\", \"kind\": \"nodejs:20\" },\n                       | \"parameters\":[\"key1\",\"value1\"],\n                       | \"limits\":{ \"timeout\":1000, \"memory\":200 } }\"\"\".stripMargin\n    assert(check(ACTION, schema))\n  }\n\n  it should \"reject an improperly structured Action\" in {\n    val schema = getJsonSchema(\"Action\").compactPrint\n    def ACTION = \"\"\" {\"sname\" : \"foo\", \"spublish\" : false} \"\"\"\n    assert(!check(ACTION, schema))\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/rest/RestUtil.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.rest\n\nimport org.apache.pekko.http.scaladsl.model.Uri\n\nimport scala.util.Try\nimport io.restassured.RestAssured\nimport io.restassured.config.RestAssuredConfig\nimport io.restassured.config.SSLConfig\nimport common.WhiskProperties\nimport spray.json._\n\n/**\n * Utilities for REST tests\n */\ntrait RestUtil {\n\n  private val skipKeyStore = WhiskProperties.isSSLCheckRelaxed\n  private val trustStorePassword = WhiskProperties.getSslCertificateChallenge\n\n  // force RestAssured to allow all hosts in SSL certificates\n  val sslconfig = {\n    val inner = new SSLConfig().allowAllHostnames()\n    val config = if (!skipKeyStore && trustStorePassword != null) {\n      inner.keyStore(\"keystore\", trustStorePassword)\n      inner.trustStore(\"keystore\", trustStorePassword)\n    } else {\n      inner.relaxedHTTPSValidation()\n    }\n    new RestAssuredConfig().sslConfig(config)\n  }\n\n  /**\n   * @return the URL for the whisk service as a hostname (this is the edge/router as a hostname)\n   */\n  def getServiceApiHost(subdomain: String, withProtocol: Boolean): String = {\n    WhiskProperties.getApiHostForClient(subdomain, withProtocol)\n  }\n\n  /**\n   * @return the URL and port for the whisk service using the main router or the edge router ip address\n   */\n  def getServiceURL(): String = {\n    val host = WhiskProperties.getEdgeHost\n    val uri = Uri(host)\n    //Ensure that port is explicitly include in the returned URL\n    val absolute = if (uri.isAbsolute) {\n      uri.withPort(uri.effectivePort)\n    } else {\n      val apiPort = WhiskProperties.getEdgeHostApiPort\n      val protocol = if (apiPort == 443) \"https\" else \"http\"\n      Uri.from(scheme = protocol, host = host, port = apiPort)\n    }\n    absolute.toString()\n  }\n\n  /**\n   * @return the base URL for the whisk REST API\n   */\n  def getBaseURL(path: String = \"/api/v1\"): String = {\n    getServiceURL() + path\n  }\n\n  /**\n   * construct the Json schema for a particular model type in the swagger model,\n   * and return it as a string.\n   */\n  def getJsonSchema(model: String, path: String = \"/api/v1\"): JsValue = {\n    val response = RestAssured\n      .given()\n      .config(sslconfig)\n      .get(getServiceURL() + s\"${if (path.endsWith(\"/\")) path else path + \"/\"}api-docs\")\n\n    assert(response.statusCode() == 200)\n\n    val body = Try { response.body().asString().parseJson.asJsObject }\n    val schema = body map { _.fields(\"definitions\").asJsObject }\n    val t = schema map { _.fields(model).asJsObject } getOrElse JsObject.empty\n    val d = JsObject(\"definitions\" -> (schema getOrElse JsObject.empty))\n    JsObject(t.fields ++ d.fields)\n  }\n}\n"
  },
  {
    "path": "tests/src/test/scala/system/rest/SwaggerTests.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage system.rest\n\nimport org.junit.runner.RunWith\nimport org.scalatest.flatspec.AnyFlatSpec\nimport org.scalatest.matchers.should.Matchers\nimport org.scalatestplus.junit.JUnitRunner\n\nimport io.restassured.RestAssured\n\nimport common.WhiskProperties\n\n/**\n * Basic tests of Swagger support\n */\n@RunWith(classOf[JUnitRunner])\nclass SwaggerTests extends AnyFlatSpec with Matchers with RestUtil {\n\n  \"Whisk API service\" should \"respond to /docs with Swagger UI\" in {\n    val response = RestAssured.given().config(sslconfig).get(getServiceURL() + \"/api/v1/docs/index.html\")\n\n    response.statusCode() should be(200)\n    response.body().asString().contains(\"<title>Swagger UI</title>\") should be(true)\n  }\n\n  it should \"respond to /api-docs with Swagger XML\" in {\n    val response = RestAssured.given().config(sslconfig).get(getServiceURL() + \"/api/v1/api-docs\")\n\n    response.statusCode() should be(200)\n    response.body().asString().contains(\"\\\"swagger\\\":\") should be(true)\n  }\n\n  it should \"respond to /api-docs including ActionMeta/ActionExecMeta/RuleMeta/TriggerMeta\" in {\n    val response = RestAssured.given().config(sslconfig).get(getServiceURL() + \"/api/v1/api-docs\")\n\n    response.statusCode() should be(200)\n    response.body().asString().contains(\"\\\"#/definitions/ActionMeta\\\"\") should be(true)\n    response.body().asString().contains(\"\\\"#/definitions/ActionExecMeta\\\"\") should be(true)\n    response.body().asString().contains(\"\\\"#/definitions/RuleMeta\\\"\") should be(true)\n    response.body().asString().contains(\"\\\"#/definitions/TriggerMeta\\\"\") should be(true)\n    response.body().asString().contains(\"\\\"ActionMeta\\\"\") should be(true)\n    response.body().asString().contains(\"\\\"ActionExecMeta\\\"\") should be(true)\n    response.body().asString().contains(\"\\\"RuleMeta\\\"\") should be(true)\n    response.body().asString().contains(\"\\\"TriggerMeta\\\"\") should be(true)\n  }\n\n  it should \"respond to invalid URI with status code 404\" in {\n    val auth = WhiskProperties.getBasicAuth\n    val response =\n      RestAssured.given().config(sslconfig).auth().basic(auth.fst, auth.snd).get(getServiceURL() + \"/api/v1/docs/dummy\")\n    response.statusCode() should be(404)\n  }\n}\n"
  },
  {
    "path": "tools/actionProxy/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Dockerfile for docker skeleton (useful for running blackbox binaries, scripts, or Python 3 actions) .\nFROM openwhisk/dockerskeleton\n"
  },
  {
    "path": "tools/actionProxy/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nSkeleton for \"docker actions\"\n================\n\nThe `dockerskeleton` base image is useful for actions that run scripts (e.g., bash, perl, python)\nand compiled binaries or, more generally, any native executable. It provides a proxy service\n(using Flask, a Python web microframework) that implements the required `/init` and `/run` routes\nto interact with the OpenWhisk invoker service. The implementation of these routes is encapsulated\nin a class named `ActionRunner` which provides a basic framework for receiving code from an invoker,\npreparing it for execution, and then running the code when required.\n\nThe initialization of the `ActionRunner` is done via `init()` which receives a JSON object containing\na `code` property whose value is the source code to execute. It writes the source to a `source` file.\nThis method also provides a hook to optionally augment the received code via an `epilogue()` method,\nand then performs a `build()` to generate an executable. The last step of the initialization applies\n`verify()` to confirm the executable has the proper permissions to run the code. The action runner\nis ready to run the action if `verify()` is true.\n\nThe default implementations of `epilogue()` and `build()` are no-ops and should be overridden as needed.\nThe base image contains a stub added which is already executable by construction via `docker build`.\nFor language runtimes (e.g., C) that require compiling the source, the extending class should run the\nrequired source compiler during `build()`.\n\nThe `run()` method runs the action via the executable generated during `init()`. This method is only called\nby the proxy service if `verify()` is true. `ActionRunner` subclasses are encouraged to override this method\nif they have additional logic that should cause `run()` to never execute. The `run()` method calls the executable\nvia a process and sends the received input parameters (from the invoker) to the action via the command line\n(as a JSON string argument). Additional properties received from the invoker are passed on to the action via\nenvironment variables as well. To augment the action environment, override `env()`.\n\nBy convention the action executable may log messages to `stdout` and `stderr`. The proxy requires that the last\nline of output to `stdout` is a valid JSON object serialized to string if the action returns a JSON result.\nA return value is optional but must be a JSON object (properly serialized) if present.\n\nFor an example implementation of an `ActionRunner` that overrides `epilogue()` and `build()` see the\n[Swift 3.x](https://github.com/apache/openwhisk-runtime-swift/blob/master/core/swift3Action/swift3runner.py) action proxy. An implementation of the runner for Python actions\nis available [here](https://github.com/apache/openwhisk-runtime-python/blob/master/core/pythonAction/pythonrunner.py). Lastly, an example Docker action that uses `C` is\navailable in this [example](https://github.com/apache/openwhisk-runtime-docker/blob/master/sdk/docker/Dockerfile).\n"
  },
  {
    "path": "tools/actionProxy/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\next.dockerImageName = 'actionproxy'\napply from: '../../gradle/docker.gradle'\n"
  },
  {
    "path": "tools/actionProxy/invoke.py",
    "content": "#!/usr/bin/env python\n\"\"\"Executable Python script for testing the action proxy.\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n  This script is useful for testing the action proxy (or its derivatives)\n  by simulating invoker interactions. Use it in combination with\n  docker run <image> which starts up the action proxy.\n  Example:\n     docker run -i -t -p 8080:8080 dockerskeleton # locally built images may be referenced without a tag\n     ./invoke.py init <action source file>\n     ./invoke.py run '{\"some\":\"json object as a string\"}'\n\n  For additional help, try ./invoke.py -h\n\"\"\"\n\nimport os\nimport re\nimport sys\nimport json\nimport base64\nimport requests\nimport codecs\nimport argparse\ntry:\n    import argcomplete\nexcept ImportError:\n    argcomplete = False\n\ndef main():\n    try:\n        args = parseArgs()\n        exitCode = {\n            'init' : init,\n            'run'   : run\n        }[args.cmd](args)\n    except Exception as e:\n        print(e)\n        exitCode = 1\n    sys.exit(exitCode)\n\ndef dockerHost():\n    dockerHost = 'localhost'\n    if 'DOCKER_HOST' in os.environ:\n        try:\n            dockerHost = re.compile('tcp://(.*):[\\d]+').findall(os.environ['DOCKER_HOST'])[0]\n        except Exception:\n            print('cannot determine docker host from %s' % os.environ['DOCKER_HOST'])\n            sys.exit(-1)\n    return dockerHost\n\ndef containerRoute(args, path):\n    return 'http://%s:%s/%s' % (args.host, args.port, path)\n\nclass objectify(object):\n    def __init__(self, d):\n        self.__dict__ = d\n\ndef parseArgs():\n    parser = argparse.ArgumentParser(description='initialize and run an OpenWhisk action container')\n    parser.add_argument('-v', '--verbose', help='verbose output', action='store_true')\n    parser.add_argument('--host', help='action container host', default=dockerHost())\n    parser.add_argument('-p', '--port', help='action container port number', default=8080, type=int)\n\n    subparsers = parser.add_subparsers(title='available commands', dest='cmd')\n\n    initmenu = subparsers.add_parser('init', help='initialize container with src or zip/tgz file')\n    initmenu.add_argument('-b', '--binary', help='treat artifact as binary', action='store_true')\n    initmenu.add_argument('-r', '--run', nargs='?', default=None, help='run after init')\n    initmenu.add_argument('main', nargs='?', default='main', help='name of the \"main\" entry method for the action')\n    initmenu.add_argument('artifact', help='a source file or zip/tgz archive')\n    initmenu.add_argument('env', nargs='?', help='the environment variables to export to the action, either a reference to a file or an inline JSON object', default=None)\n\n    runmenu = subparsers.add_parser('run', help='send arguments to container to run action')\n    runmenu.add_argument('payload', nargs='?', help='the arguments to send to the action, either a reference to a file or an inline JSON object', default=None)\n\n    if argcomplete:\n        argcomplete.autocomplete(parser)\n    return parser.parse_args()\n\ndef init(args):\n    main = args.main\n    artifact = args.artifact\n\n    if artifact and (args.binary or artifact.endswith('.zip') or artifact.endswith('tgz') or artifact.endswith('jar')):\n        with open(artifact, 'rb') as fp:\n            contents = fp.read()\n        contents = str(base64.b64encode(contents), 'utf-8')\n        binary = True\n    elif artifact != '':\n        with(codecs.open(artifact, 'r', 'utf-8')) as fp:\n            contents = fp.read()\n        binary = False\n    else:\n        contents = None\n        binary = False\n\n    r = requests.post(\n        containerRoute(args, 'init'),\n        json = {\n            \"value\": {\n                \"code\": contents,\n                \"binary\": binary,\n                \"main\": main,\n                \"env\": processPayload(args.env)\n            }\n        })\n\n    print(r.text)\n\n    if r.status_code == 200 and args.run != None:\n        runArgs = objectify({})\n        runArgs.__dict__ = args.__dict__.copy()\n        runArgs.payload = args.run\n        run(runArgs)\n\ndef run(args):\n    value = processPayload(args.payload)\n    if args.verbose:\n        print('Sending value: %s...' % json.dumps(value)[0:40])\n    r = requests.post(containerRoute(args, 'run'), json = {\"value\": value})\n    print(str(r.content, 'utf-8'))\n\ndef processPayload(payload):\n    if payload and os.path.exists(payload):\n        with open(payload) as fp:\n            return json.load(fp)\n    try:\n        d = json.loads(payload if payload else '{}')\n        if isinstance(d, dict):\n            return d\n        else:\n            raise\n    except:\n        print('payload must be a JSON object.')\n        sys.exit(-1)\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "tools/admin/README-NEXT.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Administrative Operations\n\nThe `wskadmin-next` utility is handy for performing various administrative operations against an OpenWhisk deployment.\nIt allows you to create a new subject, manage their namespaces, to block a subject or delete their record entirely.\n\nThis is a Scala based implementation of `wskadmin` utility and is meant to be DB agnostic.\n\n### Build\n\nTo build the tool run\n\n    $./gradlew :tools:admin:build\n\nThis creates a jar at `tools/admin/build/libs/openwhisk-admin-tools-1.0.0-SNAPSHOT-cli.jar` and install it as an executable script at\n`bin/wskadmin-next`.\n\n### Setup\n\nBuild task creates an executable at `bin/wskadmin-next`. By default, the config related to `ArtifactStore` for accessing database will read the `$OPENWHISK_HOME/whisk.conf`, which was generated by Ansible `properties` deployment. Alternatively, the required config can be also passed by an overwritten config file. For example to access user details from default CouchDB setup create a file `application-cli.conf`.\n\n    include classpath(\"application.conf\")\n\n    whisk {\n      couchdb {\n        protocol = \"http\"\n        host     = \"172.17.0.1\"\n        port     = \"5984\"\n        username = \"whisk_admin\"\n        password = \"some_passw0rd\"\n        provider = \"CouchDB\"\n        databases {\n          WhiskAuth       = \"whisk_local_subjects\"\n          WhiskEntity     = \"whisk_local_whisks\"\n          WhiskActivation = \"whisk_local_activations\"\n        }\n      }\n    }\n\nAnd pass that to command via `-c` option.\n\n    $./wskadmin-next -c application-cli.conf user get guest\n\n\n### Managing Users (subjects)\n\nThe `wskadmin-next user -h` command prints the help message for working with subject records. You can create and delete a\nnew user, list all their namespaces or keys for a specific namespace, identify a user by their key, block/unblock a subject,\nand list all keys that have access to a particular namespace.\n\nSome examples:\n```bash\n# create a new user\n$ wskadmin-next user create userA\n<prints key>\n\n# add user to a specific namespace\n$ wskadmin-next user create --namespace space1 userA\n<prints new key specific to userA and space1>\n\n# add second user to same space\n$ wskadmin-next user create --namespace space1 userB\n<prints new key specific to userB and space1>\n\n# force update a user with new uuid:key\n$ wskadmin-next user create -f userA\n<prints new UUID and new key>\n\n# revoke auth key of a user and regenerate a new key\n$ wskadmin-next user create -r userA\n<prints old UUID and new key>\n\n# list all users sharing a space\n$ wskadmin-next user list -a space1\n<key for userA>   userA\n<key for userB>   userB\n\n# remove user access to a namespace\n$ wskadmin-next user delete --namespace space1 userB\nNamespace deleted\n\n# get key for userA default namespaces\n$ wskadmin-next user get userA\n<prints key specific to userA default namespace>\n\n# block a user\n$ wskadmin-next user block userA\n\"userA\" blocked successfully\n\n# unblock a user\n$ wskadmin-next user unblock userA\n\"userA\" unblocked successfully\n\n# delete user\n$ wskadmin-next user delete userB\nSubject deleted\n```\n\nThe `wskadmin-next limits` commands allow you set action and trigger throttles per namespace.\n\n```bash\n# see if custom limits are set for a namespace\n$ wskadmin-next limits get space1\nNo limits found, default system limits apply\n\n# set limits on invocationsPerMinute\n$ wskadmin-next limits set --invocationsPerMinute 1 space1\nLimits successfully set for \"space1\"\n\n# set limits on allowedKinds\n$ wskadmin-next limits set --allowedKinds nodejs:6 python space1\nLimits successfully set for \"space1\"\n\n# set limits to disable saving of activations in activationstore\n$ wskadmin-next limits set --storeActivations false space1\nLimits successfully set for \"space1\"\n```\n\nNote that limits apply to a namespace and will survive even if all users that share a namespace are deleted. You must manually delete them.\n```bash\n$ wskadmin-next limits delete space1\nLimits deleted\n```\n"
  },
  {
    "path": "tools/admin/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n## Administrative Operations\n\nThe `wskadmin` utility is handy for performing various administrative operations against an OpenWhisk deployment.\nIt allows you to create a new subject, manage their namespaces, to block a subject or delete their record entirely.\nIt also offers a convenient way to dump the asset, activation or subject databases and query their views. This is useful for debugging local deployments.\nLastly, it is a convenient way to inspect the system logs, or retrieve logs specific to a component or transaction id.\n\n### Managing Users (subjects)\n\nThe `wskadmin user -h` command will print the help message for working with subject records. You can create and delete a new user, list all their namespaces or keys for a specific namespace, identify a user by their key, block/unblock a subject, and list all keys that have access to a particular namespace.\n\nSome examples:\n```bash\n# create a new user\n$ wskadmin user create userA\n<prints key>\n\n# add user to a specific namespace\n$ wskadmin user create userA -ns space1\n<prints new key specific to userA and space1>\n\n# add second user to same space\n$ wskadmin user create userB -ns space1\n<prints new key specific to userB and space1>\n\n# list all users sharing a space\n$ wskadmin user list space1 -a\n<key for userA>   userA\n<key for userB>   userB\n\n# remove user access to a namespace\n$ wskadmin user delete userB -ns space1\nNamespace deleted\n\n# get key for userA default namespaces\n$ wskadmin user get userA\n<prints key specific to userA default namespace>\n\n# block a user\n$ wskadmin user block userA\n\"userA\" blocked successfully\n\n# unblock a user\n$ wskadmin user unblock userA\n\"userA\" unblocked successfully\n\n# delete user\n$ wskadmin user delete userB\nSubject deleted\n```\n\nThe `wskadmin limits` commands allow you set action and trigger throttles per namespace.\n\n```bash\n# see if custom limits are set for a namespace\n$ wskadmin limits get space1\nNo limits found, default system limits apply\n\n# set limits on invocationsPerMinute\n$ wskadmin limits set space1 --invocationsPerMinute 1\nLimits successfully set for \"space1\"\n\n# set limits on allowedKinds\n$ wskadmin limits set space1 --allowedKinds nodejs:6 python\nLimits successfully set for \"space1\"\n\n# set limits to disable saving of activations in activationstore\n$ wskadmin limits set space1 --storeActivations false\nLimits successfully set for \"space1\"\n```\n\nNote that limits apply to a namespace and will survive even if all users that share a namespace are deleted. You must manually delete them.\n```bash\n$ wskadmin limits delete space1\nLimits deleted\n```\n\n### Inspecting the Databases\n\nIt is sometimes handy to inspect the database form the command line. The `wskadmin db get -h` command will print the help message for available utilities.\nYou can read the entire database with `wskadmin db get whisks`. Add `--docs` to include the full documents. To list specific views, use the `--view` option.\nFor example `wskadmin db get whisks --view whisks.v2/actions` will list the actions view only.\n\n### Inspecting System Logs\n\nFor debugging a local deployment, `wskadmin syslog get` will show you the controller and invoker logs available. You can use `--grep` to grep the logs for specific patterns, or `--tid` to isolate logs specific to a specific transaction in the system. It is possible to isolate logs to a specific component (e.g., `controller0`). By default, logs are fetched from all available components.\n"
  },
  {
    "path": "tools/admin/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'org.springframework.boot' version '2.0.2.RELEASE'\n    id 'scala'\n    id 'maven-publish'\n    id 'org.scoverage'\n}\n\nproject.archivesBaseName = \"openwhisk-admin-tools\"\n\nscoverage {\n    scoverageVersion.set(\"${gradle.scala.scoverageVersion}\")\n    scoverageScalaVersion.set(\"${gradle.scala.scoverageScalaVersion}\")\n}\n\njar {\n    enabled = true\n}\n\ntask copyBootJarToBin(type:Copy){\n    from (\"${buildDir}/libs\")\n    into file(\"${project.rootProject.projectDir}/bin\")\n    rename(\"${project.archivesBaseName}-$version-cli.jar\", \"wskadmin-next\")\n}\n\nbootJar {\n    classifier = 'cli'\n    mainClassName = 'org.apache.openwhisk.core.cli.Main'\n    launchScript()\n    finalizedBy copyBootJarToBin\n}\n\ndependencies {\n    implementation \"org.scala-lang:scala-library:${gradle.scala.version}\"\n    implementation project(':common:scala')\n    implementation \"org.rogach:scallop_${gradle.scala.depVersion}:3.3.2\"\n}\n"
  },
  {
    "path": "tools/admin/src/main/resources/application.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nwhisk{\n  # tracing configuration\n  tracing {\n    component = \"cli\"\n  }\n}\n"
  },
  {
    "path": "tools/admin/src/main/scala/org/apache/openwhisk/core/cli/CommandMessages.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli\n\nobject CommandMessages {\n  val subjectBlocked = \"The subject you want to edit is blocked\"\n  val namespaceExists = \"Namespace already exists\"\n  val shortName = \"Subject name must be at least 5 characters\"\n  val invalidUUID = \"authorization id is not a valid UUID\"\n  val shortKey = \"authorization key must be at least 64 characters long\"\n\n  val subjectMissing = \"Subject to delete not found\"\n\n  def namespaceMissing(ns: String, u: String) = s\"Namespace '$ns' does not exist for '$u'\"\n  val namespaceDeleted = \"Namespace deleted\"\n  val subjectDeleted = \"Subject deleted\"\n\n  def namespaceMissing(ns: String) = s\"no identities found for namespace  '$ns'\"\n  def blocked(subject: String) = s\"'$subject' blocked successfully\"\n  def unblocked(subject: String) = s\"'$subject' unblocked successfully\"\n  def subjectMissing(subject: String) = s\"'$subject missing\"\n\n  def limitsSuccessfullyUpdated(namespace: String) = s\"Limits successfully updated for '$namespace'\"\n  def limitsSuccessfullySet(namespace: String) = s\"Limits successfully set for '$namespace'\"\n  val defaultLimits = \"No limits found, default system limits apply\"\n\n  def limitsNotFound(namespace: String) = s\"Limits not found for '$namespace'\"\n  val limitsDeleted = s\"Limits deleted\"\n\n}\n"
  },
  {
    "path": "tools/admin/src/main/scala/org/apache/openwhisk/core/cli/Main.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.cli\n\nimport java.io.File\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.http.scaladsl.Http\nimport ch.qos.logback.classic.{Level, LoggerContext}\nimport org.rogach.scallop._\nimport org.slf4j.LoggerFactory\nimport pureconfig.error.ConfigReaderException\nimport org.apache.openwhisk.common.{Logging, PekkoLogging, TransactionId}\nimport org.apache.openwhisk.core.database.{LimitsCommand, UserCommand}\n\nimport scala.concurrent.duration.{Duration, DurationInt}\nimport scala.concurrent.{Await, Future}\nimport scala.util.{Failure, Success, Try}\n\nclass Conf(arguments: Seq[String]) extends ScallopConf(arguments) {\n  banner(\"OpenWhisk admin command line tool\")\n  val durationConverter = singleArgConverter[Duration](Duration(_))\n\n  //Spring boot launch script changes the working directory to one where jar file is present\n  //So invocation like ./bin/wskadmin-next -c config.conf would fail to resolve file as it would\n  //be looked in directory where jar is present. This convertor makes use of `OLDPWD` to also\n  //do a fallback check in that directory\n  val fileConverter = singleArgConverter[File] { f =>\n    val f1 = new File(f)\n    val oldpwd = System.getenv(\"OLDPWD\")\n    if (f1.exists())\n      f1\n    else if (oldpwd != null) {\n      val f2 = new File(oldpwd, f)\n      if (f2.exists()) f2 else f1\n    } else {\n      f1\n    }\n  }\n  val verbose = tally()\n  val configFile = opt[File](descr = \"application.conf which overwrites the default whisk.conf\")(fileConverter)\n  val timeout =\n    opt[Duration](descr = \"time to wait for asynchronous task to finish\", default = Some(30.seconds))(durationConverter)\n  printedName = Main.printedName\n\n  def verboseEnabled: Boolean = verbose() > 0\n\n  addSubcommand(new UserCommand)\n  addSubcommand(new LimitsCommand)\n  shortSubcommandsHelp()\n\n  requireSubcommand()\n  validateFileExists(configFile)\n  verify()\n}\n\nobject Main {\n  val printedName = \"wskadmin\"\n\n  def main(args: Array[String]): Unit = {\n    //Parse conf before instantiating actorSystem to ensure fast pre check of config\n    val conf = new Conf(args)\n    initLogging(conf)\n    initConfig(conf)\n\n    conf.subcommands match {\n      case List(c: WhiskCommand) => c.failNoSubCommand()\n      case _                     =>\n    }\n    val exitCode = execute(conf)\n    System.exit(exitCode)\n  }\n\n  private def execute(conf: Conf): Int = {\n    implicit val actorSystem = ActorSystem(\"admin-cli\")\n    try {\n      executeWithSystem(conf)\n    } finally {\n      Await.result(Http().shutdownAllConnectionPools(), 60.seconds)\n      actorSystem.terminate()\n      Await.result(actorSystem.whenTerminated, 60.seconds)\n    }\n  }\n\n  private def executeWithSystem(conf: Conf)(implicit actorSystem: ActorSystem): Int = {\n    implicit val logger = new PekkoLogging(org.apache.pekko.event.Logging.getLogger(actorSystem, this))\n\n    val admin = new WhiskAdmin(conf)\n    val result = Try {\n      val f = admin.executeCommand()\n      Await.result(f, admin.timeout)\n    }\n    result match {\n      case Success(r) =>\n        r match {\n          case Right(msg) =>\n            println(msg)\n            0\n          case Left(e) =>\n            printErr(e.message)\n            e.code\n        }\n      case Failure(e) =>\n        e match {\n          case _: ConfigReaderException[_] =>\n            printErr(\"Incomplete config. Provide application.conf via '-c' option\")\n            if (conf.verboseEnabled) {\n              e.printStackTrace()\n            }\n          case _ =>\n            e.printStackTrace()\n        }\n        3\n    }\n  }\n\n  private def initConfig(conf: Conf): Unit = {\n    val file = conf.configFile.getOrElse {\n      new File(\"../whisk.conf\")\n    }\n    if (file.exists()) {\n      System.setProperty(\"config.file\", file.getAbsolutePath)\n    }\n  }\n\n  private def initLogging(conf: Conf): Unit = {\n    val ctx = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]\n    ctx.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).setLevel(toLevel(conf.verbose()))\n  }\n\n  private def toLevel(v: Int) = {\n    v match {\n      case 0 => Level.WARN\n      case 1 => Level.INFO\n      case 2 => Level.DEBUG\n      case _ => Level.ALL\n    }\n  }\n\n  private def printErr(message: String): Unit = {\n    //Taken from ScallopConf\n    if (overrideColorOutput.value.getOrElse(System.console() != null)) {\n      Console.err.println(\"[\\u001b[31m%s\\u001b[0m] Error: %s\" format (printedName, message))\n    } else {\n      // no colors on output\n      Console.err.println(\"[%s] Error: %s\" format (printedName, message))\n    }\n  }\n}\n\nclass CommandError(val message: String, val code: Int)\ncase class IllegalState(override val message: String) extends CommandError(message, 1)\ncase class IllegalArg(override val message: String) extends CommandError(message, 2)\n\ntrait WhiskCommand {\n  this: ScallopConfBase =>\n\n  shortSubcommandsHelp()\n\n  def failNoSubCommand(): Unit = {\n    val s = parentConfig.builder.findSubbuilder(commandNameAndAliases.head).get\n    println(s.help)\n    sys.exit(0)\n  }\n}\n\ncase class WhiskAdmin(conf: Conf)(implicit val actorSystem: ActorSystem, implicit val logging: Logging) {\n  implicit val tid = TransactionId(TransactionId.systemPrefix + \"cli\")\n  def executeCommand(): Future[Either[CommandError, String]] = {\n    conf.subcommands match {\n      case List(cmd: UserCommand, x)   => cmd.exec(x)\n      case List(cmd: LimitsCommand, x) => cmd.exec(x)\n    }\n  }\n\n  def timeout: Duration = {\n    conf.subcommands match {\n      case _ => conf.timeout()\n    }\n  }\n}\n"
  },
  {
    "path": "tools/admin/src/main/scala/org/apache/openwhisk/core/database/LimitsCommand.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.cli.{CommandError, CommandMessages, IllegalState, WhiskCommand}\nimport org.apache.openwhisk.core.database.LimitsCommand.LimitEntity\nimport org.apache.openwhisk.core.entity.types.AuthStore\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.spi.SpiLoader\nimport org.rogach.scallop.{ScallopConfBase, Subcommand}\nimport spray.json.{JsObject, JsString, JsValue, RootJsonFormat}\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.language.reflectiveCalls\nimport scala.reflect.classTag\nimport scala.util.{Properties, Try}\n\nclass LimitsCommand extends Subcommand(\"limits\") with WhiskCommand {\n  descr(\"manage namespace-specific limits\")\n\n  val set = new Subcommand(\"set\") {\n    descr(\"set limits for a given namespace\")\n\n    val namespace = trailArg[String](descr = \"the namespace to set limits for\")\n\n    //name is explicitly mentioned for backward compatibility\n    //otherwise scallop would convert it to - separated names\n    val invocationsPerMinute =\n      opt[Int](\n        descr = \"invocations per minute allowed\",\n        argName = \"INVOCATIONSPERMINUTE\",\n        validate = _ >= 0,\n        name = \"invocationsPerMinute\",\n        noshort = true)\n    val firesPerMinute =\n      opt[Int](\n        descr = \"trigger fires per minute allowed\",\n        argName = \"FIRESPERMINUTE\",\n        validate = _ >= 0,\n        name = \"firesPerMinute\",\n        noshort = true)\n    val concurrentInvocations =\n      opt[Int](\n        descr = \"concurrent invocations allowed for this namespace\",\n        argName = \"CONCURRENTINVOCATIONS\",\n        validate = _ >= 0,\n        name = \"concurrentInvocations\",\n        noshort = true)\n    val allowedKinds =\n      opt[List[String]](\n        descr = \"list of runtime kinds allowed in this namespace\",\n        argName = \"ALLOWEDKINDS\",\n        name = \"allowedKinds\",\n        noshort = true,\n        default = None)\n    val storeActivations =\n      opt[String](\n        descr = \"enable or disable storing of activations to datastore for this namespace\",\n        argName = \"STOREACTIVATIONS\",\n        name = \"storeActivations\",\n        noshort = true,\n        default = None)\n\n    lazy val limits: LimitEntity =\n      new LimitEntity(\n        EntityName(namespace()),\n        UserLimits(\n          invocationsPerMinute.toOption,\n          concurrentInvocations.toOption,\n          firesPerMinute.toOption,\n          allowedKinds.toOption.map(_.toSet),\n          storeActivations.toOption.map(_.toBoolean)))\n  }\n  addSubcommand(set)\n\n  val get = new Subcommand(\"get\") {\n    descr(\"get limits for a given namespace (if none exist, system defaults apply)\")\n    val namespace = trailArg[String](descr = \"the namespace to get limits for`\")\n  }\n  addSubcommand(get)\n\n  val delete = new Subcommand(\"delete\") {\n    descr(\"delete limits for a given namespace (system defaults apply)\")\n    val namespace = trailArg[String](descr = \"the namespace to delete limits for\")\n\n  }\n  addSubcommand(delete)\n\n  def exec(cmd: ScallopConfBase)(implicit system: ActorSystem,\n                                 logging: Logging,\n                                 transid: TransactionId): Future[Either[CommandError, String]] = {\n    implicit val executionContext = system.dispatcher\n    val authStore = LimitsCommand.createDataStore()\n    val result = cmd match {\n      case `set`    => setLimits(authStore)\n      case `get`    => getLimits(authStore)\n      case `delete` => delLimits(authStore)\n    }\n    result.onComplete { _ =>\n      authStore.shutdown()\n    }\n    result\n  }\n\n  def setLimits(authStore: AuthStore)(implicit transid: TransactionId,\n                                      ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    authStore\n      .get[LimitEntity](set.limits.docinfo)\n      .flatMap { limits =>\n        val newLimits = set.limits.revision[LimitEntity](limits.rev)\n        authStore.put(newLimits).map(_ => Right(CommandMessages.limitsSuccessfullyUpdated(limits.name.asString)))\n      }\n      .recoverWith {\n        case _: NoDocumentException =>\n          authStore.put(set.limits).map(_ => Right(CommandMessages.limitsSuccessfullySet(set.limits.name.asString)))\n      }\n  }\n\n  def getLimits(authStore: AuthStore)(implicit transid: TransactionId,\n                                      ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    val info = DocInfo(LimitsCommand.limitIdOf(EntityName(get.namespace())))\n    authStore\n      .get[LimitEntity](info)\n      .map { le =>\n        val l = le.limits\n        val msg = Seq(\n          l.concurrentInvocations.map(ci => s\"concurrentInvocations =  $ci\"),\n          l.invocationsPerMinute.map(i => s\"invocationsPerMinute = $i\"),\n          l.firesPerMinute.map(i => s\"firesPerMinute = $i\"),\n          l.allowedKinds.map(k => s\"allowedKinds = ${k.mkString(\", \")}\"),\n          l.storeActivations.map(sa => s\"storeActivations = $sa\")).flatten.mkString(Properties.lineSeparator)\n        Right(msg)\n      }\n      .recover {\n        case _: NoDocumentException =>\n          Right(CommandMessages.defaultLimits)\n      }\n  }\n\n  def delLimits(authStore: AuthStore)(implicit transid: TransactionId,\n                                      ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    val info = DocInfo(LimitsCommand.limitIdOf(EntityName(delete.namespace())))\n    authStore\n      .get[LimitEntity](info)\n      .flatMap { l =>\n        authStore.del(l.docinfo).map(_ => Right(CommandMessages.limitsDeleted))\n      }\n      .recover {\n        case _: NoDocumentException =>\n          Left(IllegalState(CommandMessages.limitsNotFound(delete.namespace())))\n      }\n  }\n}\n\nobject LimitsCommand {\n  def limitIdOf(name: EntityName) = DocId(s\"${name.name}/limits\")\n\n  def createDataStore()(implicit system: ActorSystem, logging: Logging): ArtifactStore[WhiskAuth] =\n    SpiLoader\n      .get[ArtifactStoreProvider]\n      .makeStore[WhiskAuth]()(classTag[WhiskAuth], LimitsFormat, WhiskDocumentReader, system, logging)\n\n  class LimitEntity(val name: EntityName, val limits: UserLimits) extends WhiskAuth(Subject(), Set.empty) {\n    override def docid: DocId = limitIdOf(name)\n\n    //There is no api to write limits. So piggy back on WhiskAuth but replace auth json\n    //with limits!\n    override def toJson: JsObject = UserLimits.serdes.write(limits).asJsObject\n  }\n\n  private object LimitsFormat extends RootJsonFormat[WhiskAuth] {\n    override def read(json: JsValue): WhiskAuth = {\n      val r = Try[LimitEntity] {\n        val limits = UserLimits.serdes.read(json)\n        val JsString(id) = json.asJsObject.fields(\"_id\")\n        val JsString(rev) = json.asJsObject.fields(\"_rev\")\n        val Array(name, _) = id.split('/')\n        new LimitEntity(EntityName(name), limits).revision[LimitEntity](DocRevision(rev))\n      }\n      if (r.isSuccess) r.get else throw DocumentUnreadable(Messages.corruptedEntity)\n    }\n\n    override def write(obj: WhiskAuth): JsValue = obj.toDocumentRecord\n  }\n}\n"
  },
  {
    "path": "tools/admin/src/main/scala/org/apache/openwhisk/core/database/UserCommand.scala",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage org.apache.openwhisk.core.database\n\nimport java.util.UUID\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.{Sink, Source}\nimport org.apache.openwhisk.common.{Logging, TransactionId}\nimport org.apache.openwhisk.core.cli.{CommandError, CommandMessages, IllegalState, WhiskCommand}\nimport org.apache.openwhisk.core.database.UserCommand.ExtendedAuth\nimport org.apache.openwhisk.core.entity._\nimport org.apache.openwhisk.core.entity.types._\nimport org.apache.openwhisk.http.Messages\nimport org.apache.openwhisk.spi.SpiLoader\nimport org.rogach.scallop.{ScallopConfBase, Subcommand}\nimport spray.json.{JsBoolean, JsObject, JsString, JsValue, RootJsonFormat}\n\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.language.reflectiveCalls\nimport scala.reflect.classTag\nimport scala.util.{Properties, Try}\n\nclass UserCommand extends Subcommand(\"user\") with WhiskCommand {\n  descr(\"manage users\")\n\n  class CreateUserCmd extends Subcommand(\"create\") {\n    descr(\"create a user and show authorization key\")\n    val auth =\n      opt[String](\n        descr = \"the uuid:key to initialize the subject authorization key with\",\n        argName = \"AUTH\",\n        short = 'u')\n    val namespace =\n      opt[String](descr = \"create key for given namespace instead (defaults to subject id)\", argName = \"NAMESPACE\")\n    val revoke =\n      opt[Boolean](descr = \"revoke the current authorization key and generate a new key\", short = 'r')\n    val force =\n      opt[Boolean](descr = \"force update an existing subject authorization uuid:key\", short = 'f')\n    val subject = trailArg[String](descr = \"the subject to create\")\n\n    validate(subject) { s =>\n      if (s.length < 5) {\n        Left(CommandMessages.shortName)\n      } else {\n        Right(())\n      }\n    }\n\n    validate(auth) { a =>\n      a.split(\":\") match {\n        case Array(uuid, key) =>\n          if (key.length < 64) {\n            Left(CommandMessages.shortKey)\n          } else if (!isUUID(uuid)) {\n            Left(CommandMessages.invalidUUID)\n          } else {\n            Right(())\n          }\n        case _ => Left(s\"failed to determine authorization id and key: $a\")\n      }\n\n    }\n\n    def isUUID(u: String) = Try(UUID.fromString(u)).isSuccess\n\n    def desiredNamespace(authKey: BasicAuthenticationAuthKey) =\n      Namespace(EntityName(namespace.getOrElse(subject()).trim), authKey.uuid)\n  }\n\n  val create = new CreateUserCmd\n\n  addSubcommand(create)\n\n  val delete = new Subcommand(\"delete\") {\n    descr(\"delete a user\")\n    val subject = trailArg[String](descr = \"the subject to delete\")\n    val namespace =\n      opt[String](descr = \"delete key for given namespace only\", argName = \"NAMESPACE\")\n  }\n  addSubcommand(delete)\n\n  val get = new Subcommand(\"get\") {\n    descr(\"get authorization key for user\")\n\n    val subject = trailArg[String](descr = \"the subject to get key for\")\n    val namespace =\n      opt[String](descr = \"the namespace to get the key for, defaults to subject id\", argName = \"NAMESPACE\")\n\n    val all = opt[Boolean](descr = \"list all namespaces and their keys\")\n  }\n  addSubcommand(get)\n\n  val whois = new Subcommand(\"whois\") {\n    descr(\"identify user from an authorization key\")\n    val authkey = trailArg[String](descr = \"the credentials to look up 'uuid:key'\")\n  }\n  addSubcommand(whois)\n\n  val list = new Subcommand(\"list\") {\n    descr(\"list authorization keys associated with a namespace\")\n    val namespace = trailArg[String](descr = \"the namespace to lookup\")\n\n    val pick = opt[Int](descr = \"show no more than N identities\", argName = \"N\", validate = _ > 0)\n    val key = opt[Boolean](descr = \"show only the keys\")\n    val all = opt[Boolean](descr = \"show all identities\")\n\n    def limit: Int = {\n      if (all.isSupplied) 0\n      else pick.getOrElse(0)\n    }\n\n    def showOnlyKeys = key.isSupplied\n  }\n  addSubcommand(list)\n\n  val block = new Subcommand(\"block\") {\n    descr(\"block one or more users\")\n    val subjects = trailArg[List[String]](descr = \"one or more users to block\")\n  }\n  addSubcommand(block)\n\n  val unblock = new Subcommand(\"unblock\") {\n    descr(\"unblock one or more users\")\n    val subjects = trailArg[List[String]](descr = \"one or more users to unblock\")\n  }\n  addSubcommand(unblock)\n\n  def exec(cmd: ScallopConfBase)(implicit system: ActorSystem,\n                                 logging: Logging,\n                                 transid: TransactionId): Future[Either[CommandError, String]] = {\n    implicit val executionContext = system.dispatcher\n    val authStore = UserCommand.createDataStore()\n    val result = cmd match {\n      case `create`  => createUser(authStore)\n      case `delete`  => deleteUser(authStore)\n      case `get`     => getKey(authStore)\n      case `whois`   => whoIs(authStore)\n      case `list`    => list(authStore)\n      case `block`   => changeUserState(authStore, block.subjects(), blocked = true)\n      case `unblock` => changeUserState(authStore, unblock.subjects(), blocked = false)\n    }\n    result.onComplete { _ =>\n      authStore.shutdown()\n    }\n    result\n  }\n\n  def createUser(authStore: AuthStore)(implicit transid: TransactionId,\n                                       ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    val authKey = create.auth.map(BasicAuthenticationAuthKey(_)).getOrElse(BasicAuthenticationAuthKey())\n    authStore\n      .get[ExtendedAuth](DocInfo(create.subject()))\n      .flatMap { auth =>\n        val nsToUpdate = create.desiredNamespace(authKey).name\n        val existingNS = auth.namespaces.filter(_.namespace.name != nsToUpdate)\n        if (auth.isBlocked) {\n          Future.successful(Left(IllegalState(CommandMessages.subjectBlocked)))\n        } else if (!auth.namespaces.exists(_.namespace.name == nsToUpdate) || create.force.isSupplied) {\n          val newNS = existingNS + WhiskNamespace(create.desiredNamespace(authKey), authKey)\n          val newAuth = WhiskAuth(auth.subject, newNS).revision[WhiskAuth](auth.rev)\n          authStore.put(newAuth).map(_ => Right(authKey.compact))\n        } else if (create.revoke.isSupplied) {\n          val updatedAuthKey = auth.namespaces.find(_.namespace.name == nsToUpdate).get.authkey\n          val newAuthKey = new BasicAuthenticationAuthKey(updatedAuthKey.uuid, Secret())\n\n          val newNS = existingNS + WhiskNamespace(create.desiredNamespace(newAuthKey), newAuthKey)\n          val newAuth = WhiskAuth(auth.subject, newNS).revision[WhiskAuth](auth.rev)\n          authStore.put(newAuth).map(_ => Right(newAuthKey.compact))\n        } else {\n          Future.successful(Left(IllegalState(CommandMessages.namespaceExists)))\n        }\n      }\n      .recoverWith {\n        case _: NoDocumentException =>\n          val auth =\n            WhiskAuth(Subject(create.subject()), Set(WhiskNamespace(create.desiredNamespace(authKey), authKey)))\n          authStore.put(auth).map(_ => Right(authKey.compact))\n      }\n  }\n\n  def deleteUser(authStore: AuthStore)(implicit transid: TransactionId,\n                                       ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    authStore\n      .get[ExtendedAuth](DocInfo(delete.subject()))\n      .flatMap { auth =>\n        delete.namespace\n          .map { namespaceToDelete =>\n            val newNS = auth.namespaces.filter(_.namespace.name.asString != namespaceToDelete)\n            if (newNS == auth.namespaces) {\n              Future.successful(\n                Left(IllegalState(CommandMessages.namespaceMissing(namespaceToDelete, delete.subject()))))\n            } else {\n              val newAuth = WhiskAuth(auth.subject, newNS).revision[WhiskAuth](auth.rev)\n              authStore.put(newAuth).map(_ => Right(CommandMessages.namespaceDeleted))\n            }\n          }\n          .getOrElse {\n            authStore.del(auth.docinfo).map(_ => Right(CommandMessages.subjectDeleted))\n          }\n      }\n      .recover {\n        case _: NoDocumentException =>\n          Left(IllegalState(CommandMessages.subjectMissing))\n      }\n  }\n\n  def getKey(authStore: AuthStore)(implicit transid: TransactionId,\n                                   ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    authStore\n      .get[ExtendedAuth](DocInfo(get.subject()))\n      .map { auth =>\n        if (get.all.isSupplied) {\n          val msg =\n            auth.namespaces.map(ns => s\"${ns.namespace.name}\\t${ns.authkey.compact}\").mkString(Properties.lineSeparator)\n          Right(msg)\n        } else {\n          val ns = get.namespace.getOrElse(get.subject())\n          auth.namespaces\n            .find(_.namespace.name.asString == ns)\n            .map(n => Right(n.authkey.compact))\n            .getOrElse(Left(IllegalState(CommandMessages.namespaceMissing(ns, get.subject()))))\n        }\n      } recover {\n      case _: NoDocumentException =>\n        Left(IllegalState(CommandMessages.subjectMissing))\n    }\n  }\n\n  def whoIs(authStore: AuthStore)(implicit transid: TransactionId,\n                                  ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    Identity\n      .get(authStore, BasicAuthenticationAuthKey(whois.authkey()))\n      .map { i =>\n        val msg = Seq(s\"subject: ${i.subject}\", s\"namespace: ${i.namespace}\").mkString(Properties.lineSeparator)\n        Right(msg)\n      }\n      .recover {\n        case _: NoDocumentException =>\n          Left(IllegalState(CommandMessages.subjectMissing))\n      }\n  }\n\n  def list(authStore: AuthStore)(implicit transid: TransactionId,\n                                 ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    Identity\n      .list(authStore, List(list.namespace()), limit = list.limit)\n      .map { rows =>\n        if (rows.isEmpty) Left(IllegalState(CommandMessages.namespaceMissing(list.namespace())))\n        else {\n          val msg = rows\n            .map { row =>\n              row.getFields(\"id\", \"value\") match {\n                case Seq(JsString(subject), JsObject(value)) =>\n                  val JsString(uuid) = value(\"uuid\")\n                  val JsString(secret) = value(\"key\")\n                  s\"$uuid:$secret${if (list.showOnlyKeys) \"\" else s\"\\t$subject\"}\"\n                case _ => throw new IllegalStateException(\"identities view malformed\")\n              }\n            }\n            .mkString(Properties.lineSeparator)\n          Right(msg)\n        }\n      }\n  }\n\n  def changeUserState(authStore: AuthStore, subjects: List[String], blocked: Boolean)(\n    implicit transid: TransactionId,\n    system: ActorSystem,\n    ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    Source(subjects)\n      .mapAsync(1)(changeUserState(authStore, _, blocked))\n      .runWith(Sink.seq[Either[CommandError, String]])\n      .map { rows =>\n        val lefts = rows.count(_.isLeft)\n        val msg = rows\n          .map {\n            case Left(x)  => x.message\n            case Right(x) => x\n          }\n          .mkString(Properties.lineSeparator)\n\n        if (lefts > 0) Left(new CommandError(msg, lefts)) else Right(msg)\n      }\n  }\n\n  private def changeUserState(authStore: AuthStore, subject: String, blocked: Boolean)(\n    implicit transid: TransactionId,\n    ec: ExecutionContext): Future[Either[CommandError, String]] = {\n    authStore\n      .get[ExtendedAuth](DocInfo(subject))\n      .flatMap { auth =>\n        val newAuth = new ExtendedAuth(auth.subject, auth.namespaces, Some(blocked))\n        newAuth.revision[ExtendedAuth](auth.rev)\n        val msg = if (blocked) CommandMessages.blocked(subject) else CommandMessages.unblocked(subject)\n        authStore.put(newAuth).map(_ => Right(msg))\n      }\n      .recover {\n        case _: NoDocumentException =>\n          Left(IllegalState(CommandMessages.subjectMissing(subject)))\n      }\n  }\n}\n\nobject UserCommand {\n  def createDataStore()(implicit system: ActorSystem, logging: Logging): ArtifactStore[WhiskAuth] =\n    SpiLoader\n      .get[ArtifactStoreProvider]\n      .makeStore[WhiskAuth]()(classTag[WhiskAuth], ExtendedAuthFormat, WhiskDocumentReader, system, logging)\n\n  class ExtendedAuth(subject: Subject, namespaces: Set[WhiskNamespace], blocked: Option[Boolean])\n      extends WhiskAuth(subject, namespaces) {\n    override def toJson: JsObject =\n      blocked.map(b => JsObject(super.toJson.fields + (\"blocked\" -> JsBoolean(b)))).getOrElse(super.toJson)\n\n    def isBlocked: Boolean = blocked.getOrElse(false)\n  }\n\n  private object ExtendedAuthFormat extends RootJsonFormat[WhiskAuth] {\n    override def write(obj: WhiskAuth): JsValue = {\n      obj.toDocumentRecord\n    }\n\n    override def read(json: JsValue): WhiskAuth = {\n      val r = Try[ExtendedAuth] {\n        val auth = WhiskAuth.serdes.read(json)\n        val blocked = json.asJsObject.fields.get(\"blocked\") match {\n          case Some(b: JsBoolean) => Some(b.value)\n          case _                  => None\n        }\n        new ExtendedAuth(auth.subject, auth.namespaces, blocked).revision[ExtendedAuth](auth.rev)\n      }\n      if (r.isSuccess) r.get else throw DocumentUnreadable(Messages.corruptedEntity)\n    }\n  }\n}\n"
  },
  {
    "path": "tools/admin/wskadmin",
    "content": "#!/usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##\n# Whisk Admin command line interface\n##\n\nimport argparse\nimport json\nimport os\nimport random\nimport re\nfrom subprocess import Popen, PIPE, STDOUT\nimport string\nimport sys\nimport traceback\nimport uuid\nimport wskprop\nif sys.version_info.major >= 3:\n    from urllib.parse import quote_plus\nelse:\n    from urllib import quote_plus\ntry:\n    import argcomplete\nexcept ImportError:\n    argcomplete = False\nfrom wskutil import request\n\nDB_PROTOCOL = 'DB_PROTOCOL'\nDB_HOST     = 'DB_HOST'\nDB_PORT     = 'DB_PORT'\nDB_USERNAME = 'DB_USERNAME'\nDB_PASSWORD = 'DB_PASSWORD'\n\nDB_WHISK_AUTHS   = 'DB_WHISK_AUTHS'\nDB_WHISK_ACTIONS = 'DB_WHISK_ACTIONS'\nDB_WHISK_ACTIVATIONS = 'DB_WHISK_ACTIVATIONS'\n\nLOGS_DIR = 'WHISK_LOGS_DIR'\n\n# SCRIPT_DIR is going to be traversing all links and point to tools/cli/wsk\nCLI_DIR = os.path.dirname(os.path.realpath(sys.argv[0]))\n# ROOT_DIR is the repository root\nROOT_DIR = os.path.join(os.path.join(CLI_DIR, os.pardir), os.pardir)\n\ndef main():\n    requiredprops = [\n        DB_PROTOCOL, DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD,\n        DB_WHISK_AUTHS, DB_WHISK_ACTIONS, DB_WHISK_ACTIVATIONS,\n        LOGS_DIR ]\n    whiskprops = wskprop.importPropsIfAvailable(wskprop.propfile(ROOT_DIR))\n    (valid, props, deferredInfo) = wskprop.checkRequiredProperties(requiredprops, whiskprops)\n\n    exitCode = 0 if valid else 2\n    if valid:\n        try:\n            args = parseArgs()\n            if (args.verbose):\n                print(deferredInfo)\n            exitCode = {\n              'user' : userCmd,\n              'db'   : dbCmd,\n              'syslog' : syslogCmd,\n              'limits': limitsCmd\n            }[args.cmd](args, props)\n        except Exception as e:\n            print('Exception: ', e)\n            print('Informative: ', deferredInfo)\n            traceback.print_exc()\n            exitCode = 1\n    sys.exit(exitCode)\n\ndef str_to_bool(value):\n    if value.lower() in (\"yes\", \"true\"):\n        return True\n    elif value.lower() in (\"no\", \"false\"):\n        return False\n    else:\n        raise argparse.ArgumentTypeError(\"%s is not a valid boolean.\" % value)\n\ndef parseArgs():\n    parser = argparse.ArgumentParser(description='OpenWhisk admin command line tool')\n    parser.add_argument('-v', '--verbose', help='verbose output', action='store_true')\n    subparsers = parser.add_subparsers(title='available commands', dest='cmd')\n    subparsers.required = True\n\n    propmenu = subparsers.add_parser('user', help='manage users')\n    propmenu.add_argument('-w', '--view', help='the subject view to query', default='subjects.v2.0.0')\n    subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')\n    subparser.required = True\n\n    subcmd = subparser.add_parser('create', help='create a user and show authorization key')\n    subcmd.add_argument('subject', help='the subject to create')\n    subcmd.add_argument('-u', '--auth', help='the uuid:key to initialize the subject authorization key with')\n    subcmd.add_argument('-ns', '--namespace', help='create key for given namespace instead (defaults to subject id')\n    subcmd.add_argument('-r', '--revoke', help='revoke existing key and create a new one', action='store_true')\n    subcmd.add_argument('-g', '--genonly', help='generate a uuid and key but do not store them in the database', action='store_true')\n    subcmd.add_argument('-s', '--silent', help='do not should the new key on the console', action='store_true')\n\n    subcmd = subparser.add_parser('delete', help='delete a user')\n    subcmd.add_argument('subject', help='the subject to delete')\n    subcmd.add_argument('-ns', '--namespace', help='delete key for given namespace only')\n\n    subcmd = subparser.add_parser('get', help='get authorization key for user')\n    subcmd.add_argument('subject', help='the subject to get key for')\n    subcmd.add_argument('-ns', '--namespace', help='the namespace to get the key for, defaults to subject id')\n    subcmd.add_argument('-a', '--all', help='list all namespaces and their keys', action='store_true')\n\n    subcmd = subparser.add_parser('whois', help='identify user from an authorization key')\n    subcmd.add_argument('authkey', help='the credentials to look up')\n\n    subcmd = subparser.add_parser('block', help='block one or more users')\n    subcmd.add_argument('subjects', nargs='+', help='one or more users to block')\n\n    subcmd = subparser.add_parser('unblock', help='unblock one or more users')\n    subcmd.add_argument('subjects', nargs='+', help='one or more users to unblock')\n\n    subcmd = subparser.add_parser('list', help='list authorization keys associated with a namespace')\n    subcmd.add_argument('namespace', help='the namespace to lookup')\n    subcmd.add_argument('-p', '--pick', metavar='N', help='show no more than N identities', type=int, default=1)\n    subcmd.add_argument('-a', '--all', help='show all identities', action='store_true')\n    subcmd.add_argument('-k', '--key', help='show only the keys', action='store_true')\n\n    propmenu = subparsers.add_parser('limits', help='manage namespace-specific limits')\n    subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')\n    subparser.required = True\n\n    subcmd = subparser.add_parser('set', help='set limits for a given namespace')\n    subcmd.add_argument('namespace', help='the namespace to set limits for')\n    subcmd.add_argument('--invocationsPerMinute', help='invocations per minute allowed', type=int)\n    subcmd.add_argument('--firesPerMinute', help='trigger fires per minute allowed', type=int)\n    subcmd.add_argument('--concurrentInvocations', help='concurrent invocations allowed for this namespace', type=int)\n    subcmd.add_argument('--allowedKinds', help='list of runtime kinds allowed in this namespace', nargs='+', type=str)\n    subcmd.add_argument('--storeActivations', help='enable or disable storing of activations to datastore for this namespace', default=None, type=str_to_bool)\n    subcmd.add_argument('--minActionMemory', help='minimum action memory size for this namespace', default=None, type=int)\n    subcmd.add_argument('--maxActionMemory', help='maximum action memory size for this namespace', default=None, type=int)\n    subcmd.add_argument('--minActionLogs', help='minimum activation log size for this namespace', default=None, type=int)\n    subcmd.add_argument('--maxActionLogs', help='maximum activation log size for this namespace', default=None, type=int)\n    subcmd.add_argument('--minActionTimeout', help='minimum action time limit for this namespace', default=None, type=int)\n    subcmd.add_argument('--maxActionTimeout', help='maximum action time limit for this namespace', default=None, type=int)\n    subcmd.add_argument('--minActionConcurrency', help='minimum action concurrency limit for this namespace', default=None, type=int)\n    subcmd.add_argument('--maxActionConcurrency', help='maximum action concurrency limit for this namespace', default=None, type=int)\n    subcmd.add_argument('--maxParameterSize', help='maximum parameter size for this namespace', default=None, type=str)\n    subcmd.add_argument('--maxPayloadSize', help='maximum payload size for this namespace', default=None, type=str)\n    subcmd.add_argument('--truncationSize', help='activation truncation size for this namespace', default=None, type=str)\n\n    subcmd = subparser.add_parser('get', help='get limits for a given namespace (if none exist, system defaults apply)')\n    subcmd.add_argument('namespace', help='the namespace to get limits for')\n\n    subcmd = subparser.add_parser('delete', help='delete limits for a given namespace (system defaults apply)')\n    subcmd.add_argument('namespace', help='the namespace to delete limits for')\n\n    propmenu = subparsers.add_parser('db', help='work with dbs')\n    subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')\n    subparser.required = True\n\n    subcmd = subparser.add_parser('get', help='get contents of database')\n    subcmd.add_argument('database', help='the database name')\n    subcmd.add_argument('-w', '--view', help='the view in the database to query')\n    subcmd.add_argument('--docs', help='include document contents', action='store_true')\n\n    propmenu = subparsers.add_parser('syslog', help='work with system logs')\n    subparser = propmenu.add_subparsers(title='available commands', dest='subcmd')\n    subparser.required = True\n\n    subcmd = subparser.add_parser('get', help='get logs')\n    subcmd.add_argument('components', help='components, one or more of [controllerN, schedulerN, invokerN] where N is the instance', nargs='*', default=['controller0', 'scheduler0', 'invoker0'])\n    subcmd.add_argument('-t', '--tid', help='retrieve logs for the transaction id')\n    subcmd.add_argument('-g', '--grep', help='retrieve logs that match grep expression')\n\n    if argcomplete:\n        argcomplete.autocomplete(parser)\n    return parser.parse_args()\n\ndef userCmd(args, props):\n    if args.subcmd == 'create':\n        return createUserCmd(args, props)\n    elif args.subcmd == 'delete':\n        return deleteUserCmd(args, props)\n    elif args.subcmd == 'get':\n        return getUserCmd(args, props)\n    elif args.subcmd == 'whois':\n        return whoisUserCmd(args, props)\n    elif args.subcmd == 'list':\n        return listUserCmd(args, props)\n    elif args.subcmd == 'block':\n        return blockUserCmd(args, props)\n    elif args.subcmd == 'unblock':\n        return unblockUserCmd(args, props)\n    else:\n        print('unknown command')\n        return 2\n\ndef dbCmd(args, props):\n    if args.subcmd == 'get':\n        return getDbCmd(args, props)\n    else:\n        print('unknown command')\n        return 2\n\ndef syslogCmd(args, props):\n    if args.subcmd == 'get':\n        return getLogsCmd(args, props)\n    else:\n        print('unknown command')\n        return 2\n\ndef limitsCmd(args, props):\n    if args.subcmd == 'set':\n        return setLimitsCmd(args, props)\n    elif args.subcmd == 'get':\n        return getLimitsCmd(args, props)\n    elif args.subcmd == 'delete':\n        return deleteLimitsCmd(args, props)\n    else:\n        print('unknown command')\n        return 2\n\ndef createUserCmd(args, props):\n    subject = args.subject.strip()\n    if len(subject) < 5:\n        print('Subject name must be at least 5 characters')\n        return 2\n\n    if args.namespace and args.namespace.strip() == '':\n        print('Namespace must not be empty')\n        return 2\n    else:\n        desiredNamespace = subject if not args.namespace else args.namespace.strip()\n\n    if args.auth:\n        try:\n            parts = args.auth.split(':')\n            try:\n                uid = str(uuid.UUID(parts[0], version = 4))\n            except ValueError:\n                print('authorization id is not a valid UUID')\n                return 2\n\n            key = parts[1]\n            if len(key) < 64:\n                print('authorization key must be at least 64 characters long')\n                return 2\n        except Exception as e:\n            print('failed to determine authorization id and key: %s' % e)\n            return 2\n    else:\n        uid = str(uuid.uuid4())\n        key = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(64))\n\n    if args.genonly:\n        print('%s:%s' % (uid, key))\n        return 0\n\n    (doc, res) = getDocumentFromDb(props, args.subject, args.verbose)\n    if doc is None:\n        doc = {\n            '_id': subject,\n            'subject': subject,\n            'namespaces': [\n                {\n                    'name': desiredNamespace,\n                    'uuid': uid,\n                    'key': key\n                }\n            ]\n        }\n    else:\n        if not doc.get('blocked'):\n            namespaces = [ns for ns in doc['namespaces'] if ns['name'] == desiredNamespace]\n            if len(namespaces) == 0:\n                doc['namespaces'].append({\n                    'name': desiredNamespace,\n                    'uuid': uid,\n                    'key': key\n                })\n            elif args.revoke:\n                if len(namespaces) == 1:\n                    namespaces[0]['uuid'] = uid\n                    namespaces[0]['key'] = key\n                else:\n                    print('Namespace is not unique')\n                    return 1\n            else:\n                print('Namespace already exists')\n                return 1\n        else:\n            print('The subject you want to edit is blocked')\n            return 1\n\n    res = insertIntoDatabase(props, doc, args.verbose)\n    if res.status in [201, 202]:\n        if not args.silent:\n            print('%s:%s' % (uid, key))\n    else:\n        print('Failed to create subject (%s)' % res.read().strip())\n        return 1\n\ndef getUserCmd(args, props):\n    (doc, res) = getDocumentFromDb(props, args.subject, args.verbose)\n\n    if doc is not None:\n        if args.all is True:\n            # tabulate name of each space and its key\n            for ns in doc['namespaces']:\n                print('%s\\t%s:%s' % (ns['name'], ns['uuid'], ns['key']))\n            return 0\n        else:\n          # if requesting key for specific namespace, report only that key;\n          # use default namespace if no namespace provided\n          namespaceName = args.namespace if args.namespace is not None else args.subject\n          namespaces = [ns for ns in doc['namespaces'] if ns['name'] == namespaceName]\n          if len(namespaces) == 1:\n              ns = namespaces[0]\n              print('%s:%s' % (ns['uuid'], ns['key']))\n              return 0\n          else:\n              print('namespace \"%s\" not found for \"%s\"' % (namespaceName, args.subject))\n              return 1\n    else:\n        print('Failed to get subject (%s)' % res.read().strip())\n        return 1\n\ndef listUserCmd(args, props):\n    (nslist, res) = getIdentitiesFromNamespace(args, props)\n\n    if args.pick < 1:\n        print('pick at least 1 identity to show')\n        return 2\n\n    if nslist is not None:\n        nslist = nslist if args.all is True else nslist[:args.pick]\n        if len(nslist) > 0:\n            for p in nslist:\n                print('%s:%s%s' % (p['uuid'], p['key'], \"\\t%s\" % p['subject'] if not args.key else \"\"))\n            return 0\n        else:\n            print('no identities found for namespace \"%s\"' % args.namespace)\n            return 0\n    else:\n        print('Failed to get namespace key (%s)' % res.read().strip())\n        return 1\n\ndef getDocumentFromDb(props, doc, verbose):\n    protocol = props[DB_PROTOCOL]\n    host     = props[DB_HOST]\n    port     = props[DB_PORT]\n    username = props[DB_USERNAME]\n    password = props[DB_PASSWORD]\n    database = props[DB_WHISK_AUTHS]\n\n    url = '%(protocol)s://%(host)s:%(port)s/%(database)s/%(subject)s' % {\n        'protocol': protocol,\n        'host'    : host,\n        'port'    : port,\n        'database': database,\n        'subject' : doc\n    }\n\n    headers = {\n        'Content-Type': 'application/json',\n    }\n\n    res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=verbose)\n    if res.status == 200:\n        doc = json.loads(res.read())\n        return (doc, res)\n    else:\n        return (None, res)\n\ndef getIdentitiesFromNamespace(args, props):\n    protocol = props[DB_PROTOCOL]\n    host     = props[DB_HOST]\n    port     = props[DB_PORT]\n    username = props[DB_USERNAME]\n    password = props[DB_PASSWORD]\n    database = props[DB_WHISK_AUTHS]\n\n    url = '%(protocol)s://%(host)s:%(port)s/%(database)s/_design/%(view)s/_view/identities?key=[\"%(ns)s\"]' % {\n        'protocol': protocol,\n        'host'    : host,\n        'port'    : port,\n        'username': username,\n        'database': database,\n        'view'    : args.view,\n        'ns'      : args.namespace\n    }\n\n    headers = {\n        'Content-Type': 'application/json',\n    }\n\n    res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)\n    nslist = None\n    if res.status == 200:\n        doc = json.loads(res.read())\n        nslist = []\n        if 'rows' in doc and len(doc['rows']) > 0:\n            for row in doc['rows']:\n                if 'id' in row:\n                    nslist.append({\"subject\": row[\"id\"], \"uuid\": row['value']['uuid'], \"key\": row['value']['key']})\n    return (nslist, res)\n\ndef deleteUserCmd(args, props):\n    protocol = props[DB_PROTOCOL]\n    host     = props[DB_HOST]\n    port     = props[DB_PORT]\n    username = props[DB_USERNAME]\n    password = props[DB_PASSWORD]\n    database = props[DB_WHISK_AUTHS]\n\n    if args.subject.strip() == '':\n        print('Subject must not be empty')\n        return 2\n\n    if args.namespace and args.namespace.strip() == '':\n        print('Namespace must not be empty')\n        return 2\n\n    (prev, res) = getDocumentFromDb(props, args.subject, args.verbose)\n    if prev is None:\n        print('Failed to delete subject (%s)' % res.read().strip())\n        return 1\n\n    if not args.namespace:\n        url = '%(protocol)s://%(host)s:%(port)s/%(database)s/%(subject)s?rev=%(rev)s' % {\n            'protocol': protocol,\n            'host'    : host,\n            'port'    : port,\n            'database': database,\n            'subject' : args.subject.strip(),\n            'rev'     : prev['_rev']\n        }\n\n        headers = {\n            'Content-Type': 'application/json',\n        }\n\n        res = request('DELETE', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)\n        if res.status in [200, 202]:\n            print('Subject deleted')\n        else:\n            print('Failed to delete subject (%s)' % res.read().strip())\n            return 1\n    else:\n        namespaceToDelete = args.namespace.strip()\n        namespaces = [ns for ns in prev['namespaces'] if ns['name'] != namespaceToDelete]\n        if len(prev['namespaces']) == len(namespaces):\n            print('Namespace \"%s\" does not exist for \"%s\"' % (namespaceToDelete, prev['_id']))\n            return 1\n        else:\n            prev['namespaces'] = namespaces\n            res = insertIntoDatabase(props, prev, args.verbose)\n            if res.status in [201, 202]:\n                print('Namespace deleted')\n            else:\n                print('Failed to remove namespace (%s)' % res.read().strip())\n                return 1\n\ndef whoisUserCmd(args, props):\n    protocol = props[DB_PROTOCOL]\n    host     = props[DB_HOST]\n    port     = props[DB_PORT]\n    username = props[DB_USERNAME]\n    password = props[DB_PASSWORD]\n    database = props[DB_WHISK_AUTHS]\n\n    authParts = args.authkey.split(':')\n    uuid      = authParts[0]\n    key       = authParts[1]\n\n    url = '%(protocol)s://%(host)s:%(port)s/%(database)s/_design/%(view)s/_view/identities?key=[\"%(uuid)s\",\"%(key)s\"]' % {\n        'protocol': protocol,\n        'host'    : host,\n        'port'    : port,\n        'username': username,\n        'database': database,\n        'view'    : args.view,\n        'uuid'    : uuid,\n        'key'     : key\n    }\n\n    headers = {\n        'Content-Type': 'application/json',\n    }\n\n    res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)\n    if res.status == 200:\n        doc = json.loads(res.read())\n        if 'rows' in doc and len(doc['rows']) > 0:\n            for row in doc['rows']:\n                if 'id' in row:\n                    print('subject: %s' % row['id'])\n                    print('namespace: %s' % row['value']['namespace'])\n        else:\n            print('Subject id is not recognized')\n        return 0\n    print('Failed to get subject (%s)' % res.read().strip())\n    return 1\n\ndef blockUserCmd(args, props):\n    failed = 0\n    for subject in args.subjects:\n        subject = subject.strip()\n        if len(subject) > 0:\n            (doc, res) = getDocumentFromDb(props, subject, args.verbose)\n\n            if doc is not None:\n                doc['blocked'] = True\n                insertRes = insertIntoDatabase(props, doc, args.verbose)\n                if insertRes.status in [201, 202]:\n                    print('\"%s\" blocked successfully' % subject)\n                else:\n                    print('Failed to block \"%s\" (%s)' % (subject, res.read().strip()))\n                    failed += 1\n            else:\n                print('Failed to block \"%s\" (%s)' % (subject, res.read().strip()))\n                failed += 1\n    return failed\n\ndef unblockUserCmd(args, props):\n    failed = 0\n    for subject in args.subjects:\n        subject = subject.strip()\n        if len(subject) > 0:\n            (doc, res) = getDocumentFromDb(props, subject, args.verbose)\n\n            if doc is not None:\n                doc['blocked'] = False\n                insertRes = insertIntoDatabase(props, doc, args.verbose)\n                if insertRes.status in [201, 202]:\n                    print('\"%s\" unblocked successfully' % subject)\n                else:\n                    print('Failed to unblock \"%s\" (%s)' % (subject, res.read().strip()))\n                    failed += 1\n            else:\n                print('Failed to unblock \"%s\" (%s)' % (subject, res.read().strip()))\n                failed += 1\n    return failed\n\ndef setLimitsCmd(args, props):\n    argsDict = vars(args)\n    docId = args.namespace + \"/limits\"\n    (dbDoc, res) = getDocumentFromDb(props, quote_plus(docId), args.verbose)\n    doc = dbDoc or {'_id': docId}\n\n    limits = [\n        'invocationsPerMinute',\n        'firesPerMinute',\n        'concurrentInvocations',\n        'allowedKinds',\n        'storeActivations',\n        'minActionMemory',\n        'maxActionMemory',\n        'minActionLogs',\n        'maxActionLogs',\n        'minActionTimeout',\n        'maxActionTimeout',\n        'minActionConcurrency',\n        'maxActionConcurrency',\n        'maxParameterSize',\n        'maxPayloadSize',\n        'truncationSize'\n        ]\n    for limit in limits:\n        givenLimit = argsDict.get(limit)\n        toSet = givenLimit if givenLimit != None else doc.get(limit)\n        if toSet != None:\n            doc[limit] = toSet\n\n    res = insertIntoDatabase(props, doc, args.verbose)\n    if res.status in [201, 202]:\n        print('Limits successfully set for \"%s\"' % args.namespace)\n    else:\n        print('Failed to set limits (%s)' % res.read().strip())\n        return 1\n\ndef getLimitsCmd(args, props):\n    docId = args.namespace + \"/limits\"\n    (dbDoc, res) = getDocumentFromDb(props, quote_plus(docId), args.verbose)\n\n    if dbDoc is not None:\n        limits = [\n            'invocationsPerMinute',\n            'firesPerMinute',\n            'concurrentInvocations',\n            'allowedKinds',\n            'storeActivations',\n            'minActionMemory',\n            'maxActionMemory',\n            'minActionLogs',\n            'maxActionLogs',\n            'minActionTimeout',\n            'maxActionTimeout',\n            'minActionConcurrency',\n            'maxActionConcurrency',\n            'maxParameterSize',\n            'maxPayloadSize',\n            'truncationSize'\n            ]\n        for limit in limits:\n            givenLimit = dbDoc.get(limit)\n            if givenLimit != None:\n                print('%s = %s' % (limit, givenLimit))\n    else:\n        error = json.loads(res.read())\n        if error['reason'] == 'missing' or error['reason'] == 'deleted':\n            print('No limits found, default system limits apply')\n            return 0\n        else:\n            print('Failed to get limits (%s)' % res.read().strip())\n        return 1\n\ndef deleteLimitsCmd(args, props):\n    docId = quote_plus(args.namespace + \"/limits\")\n    (dbDoc, res) = getDocumentFromDb(props, docId, args.verbose)\n\n    if dbDoc is None:\n        print('Failed to delete limits (%s)' % res.read().strip())\n        return 1\n\n    protocol = props[DB_PROTOCOL]\n    host     = props[DB_HOST]\n    port     = props[DB_PORT]\n    username = props[DB_USERNAME]\n    password = props[DB_PASSWORD]\n    database = props[DB_WHISK_AUTHS]\n\n    url = '%(protocol)s://%(host)s:%(port)s/%(database)s/%(docid)s?rev=%(rev)s' % {\n        'protocol': protocol,\n        'host'    : host,\n        'port'    : port,\n        'database': database,\n        'docid'   : docId,\n        'rev'     : dbDoc['_rev']\n    }\n\n    headers = {\n        'Content-Type': 'application/json',\n    }\n\n    res = request('DELETE', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)\n    if res.status in [200, 202]:\n        print('Limits deleted')\n    else:\n        print('Failed to delete limits (%s)' % res.read().strip())\n        return 1\n\ndef getDbCmd(args, props):\n    protocol = props[DB_PROTOCOL]\n    host     = props[DB_HOST]\n    port     = props[DB_PORT]\n    username = props[DB_USERNAME]\n    password = props[DB_PASSWORD]\n\n    if args.database == 'subjects':\n        database = props[DB_WHISK_AUTHS]\n    elif args.database == 'whisks':\n        database = props[DB_WHISK_ACTIONS]\n    elif args.database == 'activations':\n        database = props[DB_WHISK_ACTIVATIONS]\n    else:\n        database = args.database\n\n    if args.view:\n        try:\n            parts = args.view.split('/')\n            designdoc = parts[0]\n            viewname  = parts[1]\n        except:\n            print('view name \"%s\" is not formatted correctly, should be design/view' % args.view)\n            return 2\n\n    url = '%(protocol)s://%(host)s:%(port)s/%(database)s%(design)s/%(index)s?reduce=false&include_docs=%(docs)s' % {\n        'protocol': protocol,\n        'host'    : host,\n        'port'    : port,\n        'database': database,\n        'design'  : '/_design/' + designdoc +'/_view' if args.view else '',\n        'index'   : viewname if args.view else '_all_docs',\n        'docs'    : 'true' if args.docs else 'false'\n    }\n\n    headers = {\n        'Content-Type': 'application/json',\n    }\n\n    print('getting contents for %s (%s)' % (database, args.view if args.view else 'primary index'))\n    res = request('GET', url, headers=headers, auth='%s:%s' % (username, password), verbose=args.verbose)\n    if res.status == 200:\n        table = json.loads(res.read())\n        print(json.dumps(table, sort_keys=True, indent=4, separators=(',', ': ')))\n        return 0\n    print('Failed to get database (%s)' % res.read().strip())\n    return 1\n\ndef insertIntoDatabase(props, doc, verbose = False):\n    protocol = props[DB_PROTOCOL]\n    host     = props[DB_HOST]\n    port     = props[DB_PORT]\n    username = props[DB_USERNAME]\n    password = props[DB_PASSWORD]\n    database = props[DB_WHISK_AUTHS]\n\n    url = '%(protocol)s://%(host)s:%(port)s/%(database)s' % {\n        'protocol': protocol,\n        'host'    : host,\n        'port'    : port,\n        'database': database\n    }\n    body = json.dumps(doc)\n    headers = {\n        'Content-Type': 'application/json',\n    }\n\n    res = request('POST', url, body, headers, auth='%s:%s' % (username, password), verbose=verbose)\n    return res\n\ndef getLogsCmd(args, props):\n    def getComponentLogs(component):\n        path = '%s/%s/%s_logs.log' % (props[LOGS_DIR], component, component)\n        if args.tid:\n            cmd = 'grep \"\\[#tid_%s\\]\" %s' % (args.tid, path)\n        elif args.grep:\n            cmd = 'grep \"%s\" %s' % (args.grep, path)\n        else:\n            cmd = 'cat %s' % path\n        (output, error) = shell(cmd, verbose = args.verbose)\n\n        if output:\n            return output.decode('utf-8')\n        if error:\n            sys.stderr.write(error)\n        return ''\n\n    logs = map(getComponentLogs, args.components)\n    joined = ''.join(logs)\n\n    if joined:\n        output = joined.strip()\n        parts = output.split('\\n')\n        filter = [p for p in parts if p != '']\n        date = map(extractDate, filter)\n        keyed = zip(date, parts)\n        sort = sorted(keyed, key=lambda t: t[1])\n        msgs = list(unzip(sort))[1]\n        print('\\n'.join(msgs))\n    return 0\n\ndef shell(cmd, data=None, verbose=False):\n    if verbose:\n        print(cmd)\n    if input:\n        p = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT, stdin=PIPE)\n        out, err = p.communicate(input=data)\n    else:\n        out, err = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT)\n    p.wait()\n    return (out, err)\n\ndef unzip(iterable):\n    return zip(*iterable)\n\ndef extractDate(line):\n    matches = re.search(r'\\d{4}-[01]{1}\\d{1}-[0-3]{1}\\d{1}T[0-2]{1}\\d{1}:[0-6]{1}\\d{1}:[0-6]{1}\\d{1}.\\d{3}Z', line)\n    if matches is not None:\n        date = matches.group(0)\n        return date\n    else:\n        return None\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "tools/admin/wskprop.py",
    "content": "#!/usr/bin/env python\n\"\"\"Helper methods for whisk properties.\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\nimport os\n\n\ndef propfile(base):\n    if base != '':\n        filename = '%s/whisk.properties' % base\n        if os.path.isfile(filename) and os.path.exists(filename):\n            return filename\n        else:\n            parent = os.path.dirname(base)\n            return propfile(parent) if parent != base else ''\n    else:\n        return ''\n\n\ndef importPropsIfAvailable(filename):\n    thefile = (open(filename, 'r') if os.path.isfile(filename) and\n               os.path.exists(filename) else [])\n    return importProps(thefile)\n\n\ndef importProps(stream):\n    props = {}\n    for line in stream:\n        parts = line.split('=')\n        if len(parts) >= 1:\n            key = parts[0].strip()\n        if len(parts) >= 2:\n            val = parts[1].strip()\n        if key != '' and val != '':\n            props[key.upper().replace('.', '_')] = val\n        elif key != '':\n            props[key.upper().replace('.', '_')] = ''\n    return props\n\n\n# Returns a triple of (length(invalidProperties), requiredProperties,\n# deferredInfo) prints a message if a required property is not found\ndef checkRequiredProperties(requiredPropertiesByName, properties):\n    \"\"\"Return a tuple describing the requested required properties.\"\"\"\n    requiredPropertiesByValue = [getPropertyValue(key, properties) for key\n                                 in requiredPropertiesByName]\n    requiredProperties = dict(zip(requiredPropertiesByName,\n                                  requiredPropertiesByValue))\n    invalidProperties = [key for key in requiredPropertiesByName if\n                         requiredProperties[key] is None]\n    deferredInfo = ''\n    for key, value in requiredProperties.items():\n        if value in (None, ''):\n            print('property \"%s\" not found in environment or '\n                  'property file' % key)\n        else:\n            deferredInfo += 'using %(key)s = %(value)s\\n' % {'key': key,\n                                                             'value': value}\n    return (len(invalidProperties) == 0, requiredProperties, deferredInfo)\n\n\ndef getPropertyValue(key, properties):\n    evalue = os.environ.get(key)\n    value = (evalue if evalue != None and evalue != ''\n             else properties[key] if key in properties else None)\n    return value\n"
  },
  {
    "path": "tools/admin/wskutil.py",
    "content": "\"\"\"Whisk Utility methods.\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\n\nimport os\nimport json\nimport sys\nif sys.version_info.major >= 3:\n    from http.client import HTTPConnection, HTTPSConnection, IncompleteRead\n    from urllib.parse import urlparse\nelse:\n    from httplib import HTTPConnection, HTTPSConnection, IncompleteRead\n    from urlparse import urlparse\nimport ssl\nimport base64\nimport socket\n\n\n# global configurations, can control whether to allow untrusted certificates\n# on HTTPS connections\n\nverify_cert = os.getenv('DB_VERIFY_CERT') is None or os.getenv('DB_VERIFY_CERT').lower() != 'false'\nhttpRequestProps = {'secure': verify_cert}\n\ndef request(method, urlString, body = '', headers = {}, auth = None, verbose = False, https_proxy = os.getenv('https_proxy', None), timeout = 60):\n    url = urlparse(urlString)\n    if url.scheme == 'http':\n        conn = HTTPConnection(url.netloc, timeout = timeout)\n    else:\n        if httpRequestProps['secure'] or not hasattr(ssl, '_create_unverified_context'):\n            conn = HTTPSConnection(url.netloc if https_proxy is None else https_proxy, timeout = timeout)\n        else:\n            conn = HTTPSConnection(url.netloc if https_proxy is None else https_proxy, context=ssl._create_unverified_context(), timeout = timeout)\n        if https_proxy:\n            conn.set_tunnel(url.netloc)\n\n    if auth is not None:\n        auth = base64.b64encode(auth.encode()).decode()\n        headers['Authorization'] = 'Basic %s' % auth\n\n    if verbose:\n        print('========')\n        print('REQUEST:')\n        print('%s %s' % (method, urlString))\n        print('Headers sent:')\n        print(getPrettyJson(headers))\n        if body != '':\n            print('Body sent:')\n            print(body)\n\n    try:\n        conn.request(method, urlString, body, headers)\n        res = conn.getresponse()\n        body = ''\n        try:\n            body = res.read()\n        except IncompleteRead as e:\n            body = e.partial\n\n        # patch the read to return just the body since the normal read\n        # can only be done once\n        res.read = lambda: body\n\n        if verbose:\n            print('--------')\n            print('RESPONSE:')\n            print('Got response with code %s' % res.status)\n            print('Body received:')\n            print(res.read())\n            print('========')\n        return res\n    except socket.timeout:\n        return ErrorResponse(status = 500, error = 'request timed out at %d seconds' % timeout)\n    except Exception as e:\n        return ErrorResponse(status = 500, error = str(e))\n\n\ndef getPrettyJson(obj):\n    return json.dumps(obj, sort_keys=True, indent=4, separators=(',', ': '))\n\n\n# class to normalize responses for exceptions with no HTTP response for canonical error handling\nclass ErrorResponse:\n    def __init__(self, status, error):\n        self.status = status\n        self.error = error\n\n    def read(self):\n        return self.error\n"
  },
  {
    "path": "tools/build/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Build helper scripts\n\nThis directory contains the following utilities.\n- `redo`: a wrapper around Ansible and Gradle commands, for which examples are given below,\n- `citool`: allows for command line monitoring of Jenkins and Travis CI builds.\n\n## How to use `redo`\n\nThe script is called `redo` because for most development, one will want to \"redo\" the compilation and deployment.\n\n- usage information: `redo -h`\n- initialize environment and `docker-machine` (for mac): `redo setup prereq`\n- start CouchDB container and initialize DB with system and guest keys: `redo couchdb initdb`\n- start ElasticSearch container to store activations: `redo elasticsearch`\n- start MongoDB container to as database backend: `redo mongodb`\n- build and deploy system: `redo deploy`\n- run tests: `redo props tests`\n\nTo do a fresh build and deploy all with one line for a first time run `redo setup prereq couchdb initdb deploy tests` as each of these is executed sequentially.\n\nIndividual components such as the `controller` may be rebuilt and redeployed as well.\n\n  * To only build: `redo controller -b`.\n  * To only teardown: `redo controller -x`.\n  * To redeploy only: `redo controller -d`.\n  * To do all at once: `redo controller -bxd` which is the default.\n\nAdditional arguments may be passed to underlying shell commands for Gradle and Ansible using `-a`.\nFor example, the following is handy to run a subset of all tests from the command line.\n\n  * `redo tests -a '--tests package.name.TestClass.evenMethodName'`\n\nSome components are dynamically generated. This is supported by a generic component name\nwhich specifies a regex. The `runtime:([\\w]+)` is one such component, useful for rebuilding\naction runtime images.\n\n  * `redo --dir /path/to/openwhisk-runtime-nodejs runtime:nodejs6action`\n\n## How to use `citool`\n\nThis script allows for monitoring of ongoing Jenkins and Travis builds.\nThe script assumes by default that the monitored job is a Travis CI build hosted here `https://api.travis-ci.org/`.\nTo change the Travis (or Jenkins) host URL, use `-u`.\n\n- usage information: `citool -h`\n- monitor a Travis CI build with job number `N`: `citool monitor N`\n- monitor same job `N` until completion: `citool monitor -p N`\n- save job output to a file: `citool -o monitor N`\n- for Travis CI matrix builds, use the matrix index after the job number as in `citool monitor N.i` where 1 <= i <= matrix builds.\n\nTo monitor a Jenkins build `B` with job number `N` on host `https://jenkins.host:port`:\n```\ncitool -u https://jenkins.host:port -b B monitor N\n```\n\nThe script also allows for gathering controller and invoker log artifacts from a Jenkins build job. For example,\nto retrieve logs for a deployment with 1 controller and 1 invoker for build `B` with job number `N` on\nhost `https://jenkins.host:port` with the artifacts are stored in `whisk/logs` relative to the job URL:\n\n```\ncitool -u https://jenkins.host:port -b B cat whisk/logs N\n```\n\nIt is sometimes convenient to save the logs locally (via `citool -o ...`) to avoid fetching them repeatedly if one wishes\nto inspect the logs and extract a specific transaction. Logs statements may be sorted according to their timestamps using `cat -s`.\nAdditionally to grep for a specific expression, use `cat -g`.\n\n```\ncitool -o -u https://jenkins.host:port -b B cat -s -g \"tid_123\" whisk/logs N\n```\n\nThe logs are saved to `./B-build.log` and can be reprocessed using `citool` with `-i`.\n\n```\ncitool -i -b B cat -s -g \"tid_124\" whisk/logs N\n```\n\n## Gradle Build Scan Integration\n\nOpenWhisk builds on CI setups have [Gradle Build Scan](https://gradle.com/build-scans) integrated. Each build on Travis pushes scan reports to\n[Gradle Scan Community Hosted Server](https://scans.gradle.com). To see the scan report you need to check the Travis build logs for lines like\nbelow\n\n```\nPublishing build scan...\nhttps://gradle.com/s/reldo4qqlg3ka\n```\n\nThe url above is the scan report url and is unique per build\n\n## Troubleshooting\n\nIf you encounter an error `ImportError: No module named pkg_resources` while running `redo`, try the workaround below\nor see [these instructions](https://pypi.python.org/pypi/setuptools/0.9.8#installation-instructions) for upgrading `setuptools`.\n\n```\npip install --upgrade setuptools\n```\n"
  },
  {
    "path": "tools/build/checkLogs.py",
    "content": "#!/usr/bin/env python\n\"\"\"Executable Python script for checking log (entries) sizes.\n\nCI/CD tool to assert that logs and databases are in certain bounds.\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\n##\n# CI/CD tool to assert that logs and databases are in certain bounds\n##\n\nimport collections\nimport itertools\nimport os\nimport platform\nimport sys\nimport json\nfrom functools import partial\n\ndef file_has_at_most_x_bytes(x, file):\n    size = os.path.getsize(file)\n    if(size > x):\n        return [ (0, \"file has %d bytes, expected %d bytes\" % (size, x)) ]\n    else:\n        return [ ]\n\n# Checks that the database dump contains at most x entries\ndef database_has_at_most_x_entries(x, file):\n    with open(file) as db_file:\n        data = json.load(db_file)\n        entries = len(data['rows'])\n        if(entries > x):\n            return [ (0, \"found %d database entries, expected %d entries\" % (entries, x)) ]\n        else:\n            return [ ]\n\n# Runs a series of file-by-file checks.\ndef run_file_checks(file_path, checks):\n    errors = []\n\n    for check in checks:\n        errs = check(file_path)\n        if errs:\n            errors += errs\n\n    return errors\n\n# Helpers, rather than non-standard modules.\ndef colors():\n    ansi = hasattr(sys.stderr, \"isatty\") and platform.system() != \"Windows\"\n\n    def colorize(code, string):\n        return \"%s%s%s\" % (code, string, '\\033[0m') if ansi else string\n\n    blue  = lambda s: colorize('\\033[94m', s)\n    green = lambda s: colorize('\\033[92m', s)\n    red   = lambda s: colorize('\\033[91m', s)\n\n    return collections.namedtuple(\"Colorizer\", \"blue green red\")(blue, green, red)\n\n# Script entrypoint.\nif __name__ == \"__main__\":\n    if len(sys.argv) > 3:\n        sys.stderr.write(\"Usage: %s logs_directory.\\n\" % sys.argv[0])\n        sys.exit(1)\n\n    root_dir = sys.argv[1]\n\n    tags_to_check = []\n    if len(sys.argv) == 3:\n        tags_to_check = {x.strip() for x in sys.argv[2].split(',')}\n\n    col = colors()\n\n    if not os.path.isdir(root_dir):\n        sys.stderr.write(\"%s: %s is not a directory.\\n\" % (sys.argv[0], root_dir))\n\n    file_checks = [\n        (\"db-rules.log\", {\"db\"}, [ partial(database_has_at_most_x_entries, 0) ]),\n        (\"db-triggers.log\", {\"db\"}, [ partial(database_has_at_most_x_entries, 0) ]),\n        # Assert that stdout of the container is correctly piped and empty\n        (\"controller0.log\", {\"system\"}, [ partial(file_has_at_most_x_bytes, 0) ]),\n        (\"scheduler0.log\", {\"system\"}, [ partial(file_has_at_most_x_bytes, 0) ]),\n        (\"invoker0.log\", {\"system\"}, [ partial(file_has_at_most_x_bytes, 0) ])\n    ]\n\n    all_errors = []\n\n    # Runs all relevant checks on all relevant files.\n    for file_name, tags, checks in file_checks:\n        if not tags_to_check or any(t in tags for t in tags_to_check):\n            file_path = root_dir + \"/\" + file_name\n            errors = run_file_checks(file_path, checks)\n            all_errors += map(lambda p: (file_path, p[0], p[1]), errors)\n\n    sort_key = lambda p: p[0]\n\n    if all_errors:\n        files_with_errors = 0\n        for path, triples in itertools.groupby(sorted(all_errors, key=sort_key), key=sort_key):\n            files_with_errors += 1\n            sys.stderr.write(\"%s:\\n\" % col.blue(path))\n\n            pairs = sorted(map(lambda t: (t[1],t[2]), triples), key=lambda p: p[0])\n            for line, msg in pairs:\n                sys.stderr.write(\"    %4d: %s\\n\" % (line, msg))\n\n        # There's no reason not to pluralize properly.\n        if len(all_errors) == 1:\n            message = \"There was an error.\"\n        else:\n            if files_with_errors == 1:\n                message = \"There were %d errors in a file.\" % len(all_errors)\n            else:\n                message = \"There were %d errors in %d files.\" % (len(all_errors), files_with_errors)\n\n        sys.stderr.write(col.red(message) + \"\\n\")\n        sys.exit(1)\n    else:\n        print(col.green(\"All checks passed.\"))\n        sys.exit(0)\n"
  },
  {
    "path": "tools/build/citool",
    "content": "#!/usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##\n# Jenkins/Travis tools script, for monitoring and analyzing jobs from CI.\n#\n# Example use to monitor a travis build, job number N:\n# > citool monitor N\n#\n# To monitor same job until completion:\n# > citool monitor -p N\n#\n# To save job output to a file:\n# > citool -o monitor N\n#\n# Example use to monitor a jenkins build B with job number N:\n# > citool -u https://jenkins.host:port -b B monitor N\n##\n\nimport sys\nimport time\nimport traceback\nimport argparse\nif sys.version_info.major >= 3:\n    from http.client import HTTPConnection, HTTPSConnection, TEMPORARY_REDIRECT, OK\n    from urllib.parse import urlparse\nelse:\n    from httplib import HTTPConnection, HTTPSConnection, TEMPORARY_REDIRECT, OK\n    from urlparse import urlparse\nimport re\nimport threading\nimport json\nfrom xml.dom import minidom\nfrom subprocess import Popen, PIPE, STDOUT\n\ndef main():\n    exitCode = 0\n    try:\n        args = parseArgs()\n        exitCode = {\n            'monitor' : monitor,\n            'cat': cat\n        }[args.cmd](args)\n    except Exception as e:\n        print('Exception: ', e)\n        if args.verbose:\n            traceback.print_exc()\n        exitCode = 1\n    sys.exit(exitCode)\n\ndef parseArgs():\n    parser = argparse.ArgumentParser(description='tool for analyzing logs from CI')\n    subparsers = parser.add_subparsers(title='available commands', dest='cmd')\n\n    parser.add_argument('job', help='job number; for matrix jobs add the matrix index after a period (e.g., 401881768.2)')\n\n    parser.add_argument('-b', '--build', help='build name', default='travis')\n    parser.add_argument('-v', '--verbose', help='verbose output', action='store_true')\n    parser.add_argument('-i', '--input-file', help='read logs from file rather than CI', action='store_true', dest='ifile')\n    parser.add_argument('-o', '--output-file', help='store intermediate buffer to a file (e.g., jenkins console or component logs)', action='store_true', dest='ofile')\n    parser.add_argument('-u', '--url', help='URL for CI build job (default is Travis CI)', default='https://api.travis-ci.com')\n\n    subparser = subparsers.add_parser('monitor', help='report passing or failing tests (only failing tests by default)')\n    subparser.add_argument('-a', '--all', help='show all tests suites, passing and failing', action='store_true')\n    subparser.add_argument('-r', '--relax', help='relax regex match to include failed ansible tasks', action='store_true')\n    subparser.add_argument('-p', '--poll', help='repeat monitor every 10 seconds', action='store_true')\n\n    subparser = subparsers.add_parser('cat', help='concatenate logs from build (limited to Jenkins)')\n    subparser.add_argument('artifactPath', help='path to artifacts store')\n    subparser.add_argument('-g', '--grep', help='run grep against logs using provided value')\n    subparser.add_argument('-s', '--sort', help='sort logs by timestamp', action='store_true')\n    subparser.add_argument('-n', '--invokers', help='number of invokers', type=int, default=3)\n    subparser.add_argument('-c', '--controllers', help='number of controllers', type=int, default=1)\n    subparser.add_argument('-c', '--schedulers', help='number of schedulers', type=int, default=1)\n\n    return parser.parse_args()\n\ndef request(method, urlString, body = \"\", headers = {}, auth = None, verbose = False):\n    url = urlparse(urlString)\n    if url.scheme == 'http':\n        conn = HTTPConnection(url.netloc)\n    else:\n        conn = HTTPSConnection(url.netloc)\n\n    if verbose:\n        print(\"%s %s\" % (method, urlString))\n\n    conn.request(method.upper(), urlString, body, headers = headers)\n    res = conn.getresponse()\n\n    if verbose:\n        print('Got response with code %s' % res.status)\n    return res\n\ndef shell(cmd, data = None, verbose = False):\n    start = time.time()\n\n    if verbose:\n        print('%s%s' % (cmd, ' <stdin>' if (data) else ''))\n\n    p = Popen(cmd, shell = True, stdout = PIPE, stderr = STDOUT, stdin = PIPE)\n    out, err = p.communicate(input = data)\n\n    p.wait()\n    # stdout/stderr may be either text or bytes, depending on Python\n    # version.   In the latter case, decode to text.\n    if isinstance(out, bytes):\n        out = out.decode('utf-8')\n    if isinstance(err, bytes):\n        err = err.decode('utf-8')\n\n    end = time.time()\n    delta = end - start\n    return (delta, out, err)\n\ndef getTravisHeaders():\n    return {'User-Agent': 'wsk citool/0.0.1',\n            'Travis-API-Version': 3,\n            'Accept': 'application/vnd.travis-ci.2+json'\n    }\n\ndef getTravisMatrixId(parts, values):\n    N = len(values)\n    if len(parts) == 1:\n        return -1\n    else:\n        try:\n            matrix = int(parts[1]) -1\n            if matrix < 0:\n                print('Matrix id must be positive. Valid values are [1..%s].' % N)\n                exit(-1)\n            if matrix >= N:\n                print('Matrix id is out of bounds. Valid values are [1..%s].' % N)\n                exit(-1)\n            return matrix\n        except Exception:\n            print('Matrix id is not an integer as expected. Valid values are [1..%s].' % N)\n            exit(-1)\n\ndef getJobUrl(args):\n    if args.build.lower() == 'travis':\n        # Get build information\n        parts = args.job.split('.')\n        jobid = parts[0]\n        if len(parts) > 2:\n            print('Job is malformed')\n            exit(-1)\n\n        buildUrl = '%s/build/%s' % (args.url, jobid)\n        buildRes = request('get', buildUrl, headers = getTravisHeaders(), verbose = args.verbose)\n        body = validateResponse(buildRes)\n        try:\n            body = json.loads(body)\n            index = getTravisMatrixId(parts, body['jobs'])\n            job = body['jobs'][index]['id']\n        except Exception:\n            print('Expected response to contain build and job-ids properties in %s' % body)\n            exit(-1)\n        url = '%s/job/%s' % (args.url, job)\n    else: # assume jenkins\n        url = '%s/job/%s/%s' % (args.url, args.build, args.job)\n    return url\n\ndef monitor(args):\n    def poll():\n        (ex, finished) = monitorOnce(args)\n        if not finished:\n            threading.Timer(10.0, poll).start()\n\n    if args.poll:\n        poll()\n    else:\n        (ex, finished) = monitorOnce(args)\n        return ex\n\ndef monitorOnce(args):\n    if args.ifile:\n        file = open('%s' % args.job, 'r')\n        body = file.read()\n        file.close()\n    else:\n        if args.build.lower() == 'travis':\n            url = '%s/log.txt' % getJobUrl(args)\n            res = request('get', url, headers = getTravisHeaders(), verbose = args.verbose)\n            if res.status == TEMPORARY_REDIRECT:\n                url = res.getheader('location')\n                res = request('get', url, headers = getTravisHeaders(), verbose = args.verbose)\n        else: # assume jenkins\n            url = '%s/logText/progressiveHtml' % getJobUrl(args)\n            res = request('get', url, verbose = args.verbose)\n\n        body = validateResponse(res)\n\n    if args.ofile:\n        file = open('%s-console.log' % args.job, 'wb')\n        file.write(body)\n        file.close()\n    if args.ifile or res.status == OK:\n        grepForFailingTests(args, body)\n        return reportBuildStatus(args, body)\n    elif args.ofile is False:\n        print(body)\n        return res.status\n\ndef validateResponse(res):\n    body = res.read()\n\n    if res.status != OK:\n        body = body.decode('utf-8')\n        if body.startswith('<'):\n            dom = minidom.parseString(body)\n            print(dom.toprettyxml()),\n        else:\n            print(body)\n        exit(res.status)\n    elif not body:\n        print('Build log is empty.')\n        exit(-1)\n    else:\n        return body\n\ndef grepForFailingTests(args, body):\n    cmd = 'grep :tests:test'\n    # check that tests ran\n    (time, output, error) = shell(cmd, body, args.verbose)\n    if output == '':\n        print('No tests detected.')\n    # no tests: either build failure or task not yet reached, skip further check\n    else:\n        if args.relax:\n            # this will match failed ansible tasks as well\n            cmd = 'grep -E \"^> Task *.* FAILED|^\\w+\\.*.*[&gt;|>] \\w*.* FAILED%s\"' % (\"|PASSED\" if args.all else \"\")\n        else:\n            cmd = 'grep -E \"^> Task *.* FAILED|^[\\w.]+\\s*[&gt;|>] \\w*.* FAILED%s\"' % (\"|PASSED\" if args.all else \"\")\n        (time, output, error) = shell(cmd, body, args.verbose)\n        if output == '':\n            print('All tests passing.')\n        else:\n            print(output.replace('&gt;', '>')),\n\ndef reportBuildStatus(args, body):\n    lines = body.decode('utf8').rstrip('\\n').rsplit('\\n', 1)\n\n    if len(lines) == 2:\n        output = lines[1]\n        output = re.sub('<[^<]+?>', '', output).strip()\n    else:\n        output = None\n\n    if output and ('Finished: ' in output or output.startswith('Done.') or ('exceeded' in output and 'terminated' in output)):\n        print(output)\n        return (0, True)\n    else:\n        print('Build: ONGOING')\n        if output:\n            print(output)\n        return (0, False)\n\ndef cat(args):\n    def getComponentList(components):\n        list = []\n        for k,v in components.items():\n            if v > 1:\n                for i in range(v):\n                    list.append('%s%d' % (k, i))\n            else:\n                list.append(k)\n        return list\n\n    def getComponentLogs(component):\n        url = '%s/artifact/%s/%s/%s_logs.log' % (getJobUrl(args), args.artifactPath, component, component)\n        res = request('get', url, verbose = args.verbose)\n        body = res.read()\n        if res.status == OK:\n            return body\n        else:\n            return ''\n\n    def unzip(iterable):\n        return zip(*iterable)\n\n    def extractDate(line):\n        matches = re.search(r'\\d{4}-[01]{1}\\d{1}-[0-3]{1}\\d{1}T[0-2]{1}\\d{1}:[0-6]{1}\\d{1}:[0-6]{1}\\d{1}.\\d{3}Z', line)\n        if matches is not None:\n            date = matches.group(0)\n            return date\n        else:\n            return None\n\n    if args.ifile:\n        file = open('%s-build.log' % args.job, 'r')\n        joined = file.read()\n        file.close()\n    elif args.build.lower == 'travis':\n        print('Feature not yet supported for Travis builds.')\n        return 2\n    else:\n        components = {\n            'controller': args.controllers,\n            'scheduler': args.schedulers,\n            'invoker': args.invokers\n        }\n        logs = map(getComponentLogs, getComponentList(components))\n        joined = ''.join(logs)\n    if args.ofile:\n        file = open('%s-build.log' % args.job, 'w')\n        file.write(joined)\n        file.close()\n\n    if args.grep is not None:\n        cmd = 'grep \"%s\"' % args.grep\n        (time, output, error) = shell(cmd, joined, args.verbose)\n        output = output.strip()\n        if args.sort:\n            parts = output.split('\\n')\n            filter = [p for p in parts if p != '']\n            date = map(extractDate, filter)\n            keyed = zip(date, parts)\n            sort = sorted(keyed, key=lambda t: t[1])\n            msgs = unzip(sort)[1]\n            print('\\n'.join(msgs))\n            return 0\n        else:\n            print(output)\n        return 0\n    elif args.ofile is False:\n        print(joined)\n        return 0\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "tools/build/redo",
    "content": "#!/usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nimport os\nimport sys\nimport platform\nimport argparse\nimport shlex\nimport subprocess\nimport re\n\n# the default openwhisk location in openwhisk checkouts\ndefaultOpenwhisk = os.path.dirname(os.path.realpath(__file__)) + '/../../'\n# the openwhisk env var overrides if it exists\nwhiskHome = os.getenv('WHISK_HOME', defaultOpenwhisk)\n\ndef main():\n    args = getArgs()\n\n    if (not args.build and\n        not args.teardown and\n        not args.deploy):\n        args.build = True\n        args.teardown = True\n        args.deploy = True\n\n    # args.dir is either explicitly set or default to openwhisk home per environment\n    wskhome = args.dir\n    # change to wsk home to use build/deploy scripts\n    os.chdir(wskhome)\n\n    props = {}\n    props['ENV'] = args.target\n    props['WSK_HOME'] = wskhome\n    props['MAIN_DOCKER_ENDPOINT'] = getDockerHost()\n    doComponentSequence(props, args, args.components)\n\ndef doComponentSequence(props, args, components):\n    for c in components:\n        component = getComponent(c)\n        if not component:\n            if args.yaml:\n                file = c if c.endswith('.yml') else '%s.yml' % c\n                component = makeComponent('custom deployment', 'deploying using %s' % file, yaml = file, modes = 'clean')\n            elif args.gradle:\n                file = c if c.endswith('.gradle') else '%s.gradle' % c\n                component = makeComponent('custom build target', 'building using %s' % file, yaml = False, gradle = True, tasks = c)\n            else:\n                print('unknown component %s' % c)\n                exit(1)\n\n        if component['steps']:\n            doComponentSequence(props, args, component['steps'])\n        else:\n            doOne(component, args, props)\n\ndef getArgs():\n    def detectDeployTarget():\n        osname = platform.system()\n        if osname == 'Linux':\n            return 'local'\n        elif osname == 'Darwin':\n            if os.getenv('DOCKER_HOST', None) is not None:\n                # docker-machine typically has docker host set in the environment\n                return 'docker-machine'\n            else:\n                # otherwise assume docker-for-mac\n                return 'local'\n        else:\n            return None\n\n    parser = argparse.ArgumentParser(description='[re]build and [re]deploy a whisk component if no args are given, otherwise do what is instructed')\n    parser.add_argument('-b', '--build', help='build component', action='store_const', const=True, default=False)\n    parser.add_argument('-x', '--teardown', help='teardown component', action='store_const', const=True, default=False)\n    parser.add_argument('-d', '--deploy', help='deploy component', action='store_const', const=True, default=False)\n    parser.add_argument('-t', '--target', help='deploy target (one of [docker-machine, local])', default=detectDeployTarget())\n    parser.add_argument('-y', '--yaml', help='deploy target using inferred YAML file if component is not one of known targets', action='store_const', const=True, default=False)\n    parser.add_argument('-g', '--gradle', help='use target using inferred gradle file if component is not one of known targets', action='store_const', const=True, default=False)\n    parser.add_argument('-n', '--just-print', help='prints the component configuration but does not run any targets', action='store_const', const=True, default=False, dest='skiprun')\n    parser.add_argument('-c', '--list-components', help='list known component names and exit', action='store_const', const=True, default=False, dest='list')\n    parser.add_argument('-a', '--additional-task-arguments', dest='extraArgs', action='append', help='pass additional arguments to gradle build')\n    parser.add_argument('-e', '--extra-ansible-vars', dest='extraAnsibleVars', action='append', help='pass extra vars to ansible-playbook')\n    parser.add_argument('components', nargs = '*', help='component name(s) to run (in order specified if more than one)')\n    parser.add_argument('--dir', help='whisk home directory')\n\n    args = parser.parse_args()\n\n    if args.target is None:\n        print('Use \"--target\" to specify a deployment target because one '\n              'could not be determined automatically for your platform '\n              '(supported platforms are GNU/Linux (Ubuntu) and Mac OS X.')\n        exit(-1)\n\n    if args.dir is None:\n        if whiskHome is None:\n            print('Must specify whisk home directory with \"--dir\".')\n            exit(-1)\n        else:\n            args.dir = whiskHome\n\n    if args.list:\n        print(\"{:<27}{:<40}\".format(bold('component'), bold('description')))\n        for c in Components:\n            print(\"{:<30}{:<40}\".format(hilite(c['name']), c['description']))\n        exit(0)\n    elif not args.components:\n        parser.print_usage()\n        exit(0)\n    else:\n        return args\n\nclass Playbook:\n    cmd = 'ansible-playbook'\n\n    dir = False\n    file = False\n    modes = False\n    env = False\n\n    # dir: the ansible directory containing the yaml files, roles, etc.\n    # file: the yml file for the playbook\n    # modes: the modes supported for the playbook as a comma separated string (e.g., 'clean')\n    # env: the environments directory (e.g., 'environment) or None if not environment specific playbook\n    def __init__(self, dir, file, modes, env):\n        self.dir = dir\n        self.file = file\n        self.modes = modes.split(',')\n        self.env = env\n\n    def path(self, basedir):\n        return basedir + '/' + self.dir\n\n    def execcmd(self, props, mode = False, extraAnsibleVars = []):\n        if self.dir and self.file and (mode is False or mode in self.modes):\n            cmd = [ self.cmd ]\n            if self.env:\n                cmd.append('-i %s/%s' % (self.env, props['ENV']))\n            cmd.append(self.file)\n            if mode:\n                cmd.append('-e mode=%s' % mode)\n            if extraAnsibleVars:\n                cmd.append(' '.join(map(lambda x: \"-e '\" + str(x) + \"'\", extraAnsibleVars)))\n            return ' '.join(cmd)\n\nclass Gradle:\n    cmd = 'gradlew'\n\n    tasks = False\n    components = False\n\n    def __init__(self, tasks, components = False):\n        self.tasks = tasks.split(',')\n        self.components = components\n\n    def execcmd(self, props, task, extraArgs = ''):\n        if task:\n            if self.components and self.components is not True:\n                parts = map(lambda c: '%s:%s' % (c, task), self.components.split(','))\n                parts = ' '.join(parts)\n            else:\n                parts = task\n\n            dh = props['MAIN_DOCKER_ENDPOINT']\n            return '%s %s %s --parallel %s' % (\n                   props['WSK_HOME'] + '/' + self.cmd,\n                   parts,\n                   extraArgs,\n                   ('-PdockerHost=%s' % dh) if dh else '')\n\ndef getDockerHost():\n    dh = os.getenv('DOCKER_HOST')\n    if dh is not None and dh.startswith('tcp://'):\n        return dh[6:]\n\ndef makeComponent(name,                   # component name, implies playbook default and gradle tasks roots\n                  description,\n                  yaml = True,            # true for default file name else the file name\n                  modes = '',\n                  env = 'environments',\n                  dir = 'ansible',\n                  gradle = False,         # gradle buildable iff true\n                  tasks = 'distDocker',\n                  steps = None):          # comma separated, runs these steps in sequence, each step is a reference to another component (yaml/gradle not allowed)\n    yaml = ('%s.yml' % name) if yaml is True else yaml\n    playbook = Playbook(dir, yaml, modes, env) if yaml is not False else None\n    gradle = Gradle(tasks, gradle) if gradle is not False else None\n    if steps and (playbook is not None or gradle is not None):\n        print('Cannot create component \"%s\" with a sequence of steps and also '\n              'a playbook with gradle build target' % name)\n        exit(-1)\n    elif steps:\n        steps = map(lambda c: c.strip(), steps.split(','))\n    return { 'name': name, 'description': description, 'playbook': playbook, 'gradle': gradle, 'steps': steps }\n\nComponents = [\n    makeComponent('fresh',\n                  'setup, build, and deploy a fresh whisk system using couchdb',\n                  yaml = False,\n                  steps = 'setup, couchdb, initdb, wipedb, deploy, catalog'),\n\n    makeComponent('fmt',\n                  'apply source code formats',\n                  gradle = True,\n                  yaml = False,\n                  tasks = 'scalafmtAll'),\n\n    makeComponent('setup',\n                  'system setup'),\n\n    makeComponent('prereq',\n                  'install requisites'),\n\n    makeComponent('couchdb',\n                  'deploy couchdb',\n                  modes = 'clean'),\n\n    makeComponent('initdb',\n                  'initialize db with guest/system keys'),\n\n    makeComponent('wipedb',\n                  'recreate main db for entities',\n                  yaml = 'wipe.yml'),\n\n    makeComponent('elasticsearch',\n                  'deploy elasticsearch',\n                  modes = 'clean'),\n\n    makeComponent('mongodb',\n                  'deploy mongodb',\n                  modes = 'clean'),\n\n    makeComponent('initMongoDB',\n                  'initialize mongodb with guest/system keys'),\n\n    makeComponent('build',\n                  'build system',\n                  yaml = False,\n                  gradle = True),\n\n    makeComponent('deploy',\n                  'build/deploy system',\n                  yaml = 'openwhisk.yml',\n                  modes = 'clean',\n                  gradle = True),\n\n    makeComponent('teardown',\n                  'teardown all deployed containers',\n                  yaml = 'teardown.yml'),\n\n    makeComponent('kafka',\n                  'build/deploy kafka',\n                  modes = 'clean'),\n\n    makeComponent('controller',\n                  'build/deploy controller',\n                  modes = 'clean',\n                  gradle = 'core:controller'),\n\n    makeComponent('scheduler',\n                  'build/deploy scheduler',\n                  modes = 'clean',\n                  gradle = 'core:scheduler'),\n\n    makeComponent('invoker',\n                  'build/deploy invoker',\n                  modes = 'clean',\n                  gradle = ':core:invoker'),\n\n    makeComponent('edge',\n                  'deploy edge'),\n\n    makeComponent('cli',\n                  'download cli from api host',\n                  modes = 'clean',\n                  yaml = 'downloadcli.yml'),\n\n    makeComponent('catalog',\n                  'install catalog',\n                  yaml = 'postdeploy.yml'),\n\n    makeComponent('apigw',\n                  'deploy api gateway',\n                  gradle = False,\n                  modes = 'clean',\n                  yaml = 'routemgmt.yml apigateway.yml'),\n\n    # the following (re)build images via gradle\n    makeComponent('runtime:([\\w.-]+)',\n                  'build a runtime action container, matching name using the regex; NOTE: must use --dir for path to runtime directory',\n                  yaml = False,\n                  gradle = 'core:$1:distDocker'),\n\n    makeComponent('actionproxy',\n                  'build action proxy container',\n                  yaml = False,\n                  gradle = 'tools:actionProxy'),\n\n    # required for tests\n    makeComponent('props',\n                  'build whisk.properties file (required for tests)',\n                  yaml = 'properties.yml'),\n\n    # convenient to run all tests\n    makeComponent('tests',\n                  'run all tests',\n                  yaml = False,\n                  gradle = True,\n                  tasks = 'test'),\n\n    makeComponent('unit-tests',\n                  'run units tests',\n                  yaml = False,\n                  tasks = 'testUnit',\n                  gradle = 'tests'),\n\n    makeComponent('standalone',\n                  'run standalone server',\n                  yaml = False,\n                  tasks = 'bootRun',\n                  gradle = 'core:standalone')\n]\n\ndef getComponent(component):\n    for c in Components:\n        if c['name'] == component:\n            return c\n        else:\n            parts = re.match(c['name'], component)\n            if parts:\n                name = parts.group(1)\n                return makeComponent('runtime:' + name,\n                          'build a ' + name + ' runtime action container',\n                          yaml = False,\n                          gradle = 'core:' + name)\n\n    return False\n\ndef bold(string):\n    if sys.stdin.isatty():\n        attr = []\n        attr.append('1')\n        return '\\x1b[%sm%s\\x1b[0m' % (';'.join(attr), string)\n    else:\n        return string\n\ndef hilite(string, isError = False):\n    if sys.stdin.isatty():\n        attr = []\n        attr.append('34' if not isError else '31')  # blue or red if isError\n        attr.append('1')\n        return '\\x1b[%sm%s\\x1b[0m' % (';'.join(attr), string)\n    else:\n        return string\n\ndef run(cmd, dir, skiprun, allowToFail = False):\n    if cmd is not None:\n        print(hilite(cmd))\n        if not skiprun:\n            args = shlex.split(cmd)\n            p = subprocess.Popen(args, cwd = dir)\n            p.wait()\n            if p.returncode and not allowToFail:\n               abort('command failed', p.returncode)\n\ndef runAndGetStdout(cmd):\n    print(hilite(cmd))\n    args = shlex.split(cmd)\n    p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n    out, err = p.communicate()\n    # stdout/stderr may be either text or bytes, depending on Python\n    # version.   In the latter case, decode to text.\n    if isinstance(out, bytes):\n        out = out.decode('utf-8')\n    if isinstance(err, bytes):\n        err = err.decode('utf-8')\n    if p.returncode:\n        print(hilite(out))\n        print(hilite(err, True))\n        abort('command failed', p.returncode)\n    return out\n\ndef abort(msg, code = -1):\n    print(hilite(msg, True))\n    exit(code)\n\ndef doOne(component, args, props):\n    basedir = props['WSK_HOME']\n    playbook = component['playbook']\n    gradle = component['gradle']\n    print(bold(component['description']))\n\n    extraArgs = '' if args.extraArgs is None or [] else ' '.join(map(str, args.extraArgs))\n\n    if args.build and gradle is not None:\n        cmd = gradle.execcmd(props, gradle.tasks[0], extraArgs)\n        run(cmd, basedir, args.skiprun)\n\n    if args.teardown and playbook is not None:\n        cmd = playbook.execcmd(props, 'clean', extraAnsibleVars = args.extraAnsibleVars)\n        run(cmd, playbook.path(basedir), args.skiprun)\n\n    if args.deploy and playbook is not None:\n        cmd = playbook.execcmd(props, extraAnsibleVars = args.extraAnsibleVars)\n        run(cmd, playbook.path(basedir), args.skiprun)\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "tools/db/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Configure data store\n\nBefore you can build and deploy OpenWhisk, you must configure a backing data store. The system supports any self-managed [CouchDB](#using-couchdb) instance or [Cloudant](#using-cloudant) as a cloud-based database service.\n\n## Using CouchDB\n\nIf you are using your own installation of CouchDB, make a note of the host, port, username and password, and adjust the properties in `openwhisk/ansible/db_local.ini` accordingly. If you do not find `db_local.ini`, refer to [Setup](../../ansible/README.md#setup) to create it. Note that:\n\n   * the username must have administrative rights\n   * the CouchDB instance must be accessible over `http` or `https` (the latter requires a valid certificate)\n\n### Using an ephemeral CouchDB container\n\nTo try out OpenWhisk without managing your own CouchDB installation, you can start a CouchDB instance in a container as part of the OpenWhisk deployment. We advise that you use this method only as a temporary measure. Please note that:\n\n  * no data will persist between two creations of the container\n  * you will need to run `ansible-playbook couchdb.yml` every time you `clean` or `teardown` the system (see below)\n  * you will need to initialize the data store each time (`ansible-playbook initdb.yml`, see below)\n\nDetailed instructions are found in the [Ansible README](../../ansible/README.md).\n\n## Using Cloudant\n\nAs an alternative to a self-managed CouchDB, you may want to try [Cloudant](https://cloudant.com) which is a cloud-based database service.\n\n### Create a Cloudant account via IBM Cloud\nSign up for an account via [IBM Cloud](https://cloud.ibm.com). IBM Cloud offers trial accounts and its signup process is straightforward so it is not described here in detail. Using IBM Cloud, the most convenient way to create a Cloudant instance is via the `cf` command-line tool. See [here](https://cloud.ibm.com/docs/starters/install_cli.html) for instructions on how to download and configure `cf` to work with your IBM Cloud account.\n\nWhen `cf` is set up, issue the following commands to create a Cloudant database.\n\n  ```\n  # Create a Cloudant service\n  cf create-service cloudantNoSQLDB Shared cloudant-for-openwhisk\n\n  # Create Cloudant service keys\n  cf create-service-key cloudant-for-openwhisk openwhisk\n\n  # Get Cloudant username and password\n  cf service-key cloudant-for-openwhisk openwhisk\n  ```\n\nMake note of the Cloudant `username` and `password` from the last `cf` command so you can create the required `db_local.ini`.\n\n### Setting the Cloudant credentials\n\nEdit the file `openwhisk/ansible/db_local.ini` to provide the required database properties.\n\nNote that:\n\n   * the protocol for Cloudant is always HTTPS\n   * the port is always 443\n   * the host has the schema `<your cloudant user>.cloudant.com`\n\nMore details on customizing `db_local.ini` are described in the [ansible readme](../../ansible/README.md).\n\n## Initializing database for authorization keys\n\nThe system requires certain authorization keys to install standard assets (i.e., samples) and provide guest access for running unit tests.\nThese are called immortal keys. If you are using a persisted data store (e.g., Cloudant), you only need to perform this operation **once**.\nIf you are [using an ephemeral CouchDB container](#using-an-ephemeral-couchdb-container), you need to run this script every time you tear down and deploy the system.\n\n  ```\n  # Work out of your openwhisk directory\n  cd /your/path/to/openwhisk/ansible\n\n  # Initialize data store containing authorization keys\n  ansible-playbook initdb.yml\n  ```\n\nThe playbook will create the required data structures to prepare the account to be used.\nDon't worry if you are unsure whether or not the db has already been initialized. The playbook won't perform any action on a db that is already prepared.\n\nThe output of the playbook will look similar to this (using CouchDB in this example):\n\n  ```\n  PLAY [ansible] *****************************************************************\n\n  TASK [setup] *******************************************************************\n  Tuesday 14 June 2016  16:33:51 +0200 (0:00:00.017)       0:00:00.017 **********\n  ok: [ansible]\n\n  TASK [include] *****************************************************************\n  Tuesday 14 June 2016  16:33:51 +0200 (0:00:00.262)       0:00:00.280 **********\n  included: /your/path/to/openwhisk/ansible/tasks/initdb.yml for ansible\n\n  TASK [check if the immortal subjects db with CouchDB exists?] ******************\n  Tuesday 14 June 2016  16:33:51 +0200 (0:00:00.060)       0:00:00.340 **********\n  ok: [ansible]\n\n  TASK [create immortal subjects db with CouchDB] ********************************\n  Tuesday 14 June 2016  16:33:51 +0200 (0:00:00.329)       0:00:00.670 **********\n  ok: [ansible]\n\n  TASK [recreate the \"full\" index on the \"auth\" database] ************************\n  Tuesday 14 June 2016  16:33:52 +0200 (0:00:00.166)       0:00:00.837 **********\n  ok: [ansible]\n\n  TASK [recreate necessary \"auth\" keys] ******************************************\n  Tuesday 14 June 2016  16:33:52 +0200 (0:00:00.162)       0:00:01.000 **********\n  ok: [ansible] => (item=guest)\n  ok: [ansible] => (item=whisk.system)\n\n  PLAY RECAP *********************************************************************\n  ansible                    : ok=6    changed=0    unreachable=0    failed=0\n  ```\n\n## Database backups\n\nBackups are essential for running a production system of any sort and size. `replicateDbs.py` provides an easy to use interface that uses [CouchDBs replication mechanism](https://wiki.apache.org/couchdb/Replication) to create *snapshot replications*, *continuous replications* and a mechanism to play a snapshot back into the production system.\n\nAll commands for `replicateDbs.py` take two standard parameters:\n\n* `--sourceDbUrl`: Server URL of the source database, that has to be backed up. E.g. 'https://xxx:yyy@domain.couch.com:443'.\n* `--targetDbUrl`: Server URL of the target database, where the backup is stored. Like sourceDbUrl.\n\n### Creating a snapshot\n\nTo create a snapshot, call `replicateDbs.py` with the `replicate` command. It takes 3 parameters:\n\n* `--dbPrefix`: The prefix of all databases that should be backed up.\n* `--expires`: Removes all snapshots older than the provided amount of seconds.\n* `--continuous`: If specified, the created replication will be continuous.\n\nUsing that command will result in a replication for every database that matches the `--dbPrefix` flag, which is then prefixed with `backup_${TIMESTAMP_IN_SECONDS}_`. `TIMESTAMP_IN_SECONDS` is the date of generation, which is also used to determine expired snapshots that should be deleted.\n\n**Note:** Replications are created asynchronously. The script will exit very fast while the replication could take a while.\n\n### Replaying a snapshot\n\nTo replay a snapshot, swap `--sourceDbUrl` and `--targetDbUrl` and call the script with the `replay` command. That command takes only 1 parameter: `--dbPrefix` to determine which backup to play back. Matching databases will be replicated back to the target database with the `backup_${TIMESTAMP_IN_SECONDS}_` removed, so they'd look just like the original database.\n\n## Database migration to new schema\n\nTo reduce the memory consumption in the OpenWhisk controller, all code inlined in action documents has been moved to attachments. This change allows only metadata for actions to be fetched instead of the entire action. Though the OpenWhisk controller supports both mentioned schemas, it is ideal to update existing databases to use the new schema for memory consumption relief.\n\nRun `moveCodeToAttachment.py` to update actions in an existing database to the new action schema. Two parameters are required:\n\n* `--dbUrl`: Server URL of the database. E.g. 'https://xxx:yyy@domain.couch.com:443'.\n* `--dbName`: Name of the Database to update.\n"
  },
  {
    "path": "tools/db/cleanUpActivations.py",
    "content": "#!/usr/bin/env python\n\"\"\"Python script to delete old Activations.\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\nimport argparse\nimport time\nimport couchdb.client\n\ntry:\n    long        # Python 2\nexcept NameError:\n    long = int  # Python 3\n\nDAY = 1000 * 60 * 60 * 24\n\n#\n# Delete activations\n#\ndef deleteOldActivations(args):\n    db = couchdb.client.Server(args.dbUrl)[args.dbName]\n    endkey = long(time.time() * 1000) - args.days * DAY\n    while True:\n        activationIds = db.view(\"activations/byDate\", limit=args.docsPerRequest, start_key=0, end_key=endkey)\n        if activationIds:\n            documentsToDelete = [couchdb.client.Document(_id=entry.value[0], _rev=entry.value[1], _deleted=True) for entry in activationIds]\n            db.update(documentsToDelete)\n        else:\n            return\n\nparser = argparse.ArgumentParser(description=\"Utility to delete old activations older than x days in given database.\")\nparser.add_argument(\"--dbUrl\", required=True, help=\"Server URL of the database, that has to be cleaned of old activations. E.g. 'https://xxx:yyy@domain.couch.com:443'\")\nparser.add_argument(\"--dbName\", required=True, help=\"Name of the Database of the activations to be truncated.\")\nparser.add_argument(\"--days\", required=True, type=int, help=\"How many days of the activations to be kept.\")\nparser.add_argument(\"--docsPerRequest\", type=int, default=200, help=\"Number of documents handled on each CouchDb Request. Default is 200.\")\nargs = parser.parse_args()\n\ndeleteOldActivations(args)\n"
  },
  {
    "path": "tools/db/cosmosDbUtil.py",
    "content": "#!/usr/bin/env python\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nfrom collections import namedtuple\nimport glob\nimport sys\nimport os\nimport argparse\nimport traceback\nimport pydocumentdb.documents as documents\nimport pydocumentdb.errors as document_errors\nimport pydocumentdb.document_client as document_client\n\ntry:\n    import argcomplete\nexcept ImportError:\n    argcomplete = False\n\nCLI_DIR = os.path.dirname(os.path.realpath(sys.argv[0]))\n# ROOT_DIR is the OpenWhisk repository root\nROOT_DIR = os.path.join(os.path.join(CLI_DIR, os.pardir), os.pardir)\n\nDbContext = namedtuple('DbContext', ['client', 'db', 'whisks', 'subjects', 'activations'])\nverbose = False\n\n\ndef main():\n    global verbose\n    exit_code = 0\n    try:\n        args = parse_args()\n        verbose = args.verbose\n        client = init_client(args)\n        exit_code = {\n            'init': init_cmd,\n            'prune': prune_cmd,\n            'drop': drop_cmd\n        }[args.cmd](args, client)\n    except Exception as e:\n        print('Exception: ', e)\n        traceback.print_exc()\n        exit_code = 1\n\n    sys.exit(exit_code)\n\n\ndef parse_args():\n    parser = argparse.ArgumentParser(description='OpenWhisk CosmosDB bootstrap tool')\n    parser.add_argument('--endpoint', help='DB Endpoint url like https://example.documents.azure.com:443/',\n                        required=True)\n    parser.add_argument('--key', help='DB access key', required=True)\n    parser.add_argument('-v', '--verbose', help='Verbose mode', action=\"store_true\")\n\n    subparsers = parser.add_subparsers(title='available commands', dest='cmd')\n\n    propmenu = subparsers.add_parser('init', help='initialize database')\n    propmenu.add_argument('db', help='Database name under which the collections would be created')\n    propmenu.add_argument('--dir', help='Directory under which auth files are stored')\n\n    propmenu = subparsers.add_parser('prune', help='remove stale databases created by test')\n    propmenu.add_argument('--prefix', help='Database name prefix which are matched for removal', default=\"travis-\")\n\n    propmenu = subparsers.add_parser('drop', help='drop database')\n    propmenu.add_argument('db', help='Database name to be removed')\n\n    if argcomplete:\n        argcomplete.autocomplete(parser)\n    return parser.parse_args()\n\n\ndef init_cmd(args, client):\n    db = get_or_create_db(client, args.db)\n\n    whisks = init_coll(client, db, \"whisks\")\n    subjects = init_coll(client, db, \"subjects\")\n    activations = init_coll(client, db, \"activations\")\n\n    db_ctx = DbContext(client, db, whisks, subjects, activations)\n    init_auth(db_ctx)\n    return 0\n\n\ndef prune_cmd(args, client):\n    # Remove database which are one day old\n    pass\n\n\ndef drop_cmd(args, client):\n    db = get_db(client, args.db)\n    if db is not None:\n        client.DeleteDatabase(db['_self'])\n        log(\"Removed database : %s\" % args.db)\n    else:\n        log(\"Database %s not found\" % args.db)\n\n\ndef init_auth(ctx):\n    for subject in find_default_subjects():\n        link = create_link(ctx.db, ctx.subjects, subject['id'])\n        options = {'partitionKey': subject.get('id')}\n        try:\n            ctx.client.ReadDocument(link, options)\n            log('Subject already exists : ' + subject['id'])\n        except document_errors.HTTPFailure as e:\n            if e.status_code == 404:\n                ctx.client.CreateDocument(ctx.subjects['_self'], subject, options)\n                log('Created subject : ' + subject['id'])\n            else:\n                raise e\n\n\ndef create_link(db, coll, doc_id):\n    return 'dbs/' + db['id'] + '/colls/' + coll['id'] + '/docs/' + doc_id\n\n\ndef find_default_subjects():\n    files_dir = os.path.join(ROOT_DIR, \"ansible/files\")\n    for name in glob.glob1(files_dir, \"auth.*\"):\n        auth_file = open(os.path.join(files_dir, name), 'r')\n        uuid, key = auth_file.read().strip().split(\":\")\n        subject = name[name.index('.') + 1:]\n        doc = {\n            'id': subject,\n            'subject': subject,\n            'namespaces': [\n                {\n                    'name': subject,\n                    'uuid': uuid,\n                    'key': key\n                }\n            ]\n        }\n        auth_file.close()\n        yield doc\n\n\ndef init_client(args):\n    return document_client.DocumentClient(args.endpoint, {'masterKey': args.key})\n\n\ndef get_db(client, db_name):\n    query = client.QueryDatabases('SELECT * FROM root r WHERE r.id=\\'' + db_name + '\\'')\n    return next(iter(query), None)\n\n\ndef get_or_create_db(client, db_name):\n    db = get_db(client, db_name)\n    if db is None:\n        db = client.CreateDatabase({'id': db_name})\n        log('Created database \"%s\"' % db_name)\n    return db\n\n\ndef init_coll(client, db, coll_name):\n    query = client.QueryCollections(db['_self'], 'SELECT * FROM root r WHERE r.id=\\'' + coll_name + '\\'')\n    it = iter(query)\n    coll = next(it, None)\n    if coll is None:\n        collection_definition = {'id': coll_name,\n                                 'partitionKey':\n                                     {\n                                         'paths': ['/id'],\n                                         'kind': documents.PartitionKind.Hash\n                                     }\n                                 }\n        collection_options = {}  # {'offerThroughput': 10100}\n        coll = client.CreateCollection(db['_self'], collection_definition, collection_options)\n        log('Created collection \"%s\"' % coll_name)\n    return coll\n\n\ndef log(msg):\n    if verbose:\n        print(msg)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "tools/db/deleteLogsFromActivations.py",
    "content": "#!/usr/bin/env python\n\"\"\"Python script to delete logs from old Activations.\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\nimport argparse\nimport time\nimport couchdb.client\n\ntry:\n    long        # Python 2\nexcept NameError:\n    long = int  # Python 3\n\nDAY = 1000 * 60 * 60 * 24\n\ndef removeLogFromActivation(viewResult):\n    doc = viewResult.doc\n    doc[\"logs\"] = []\n    return doc\n\n#\n# Delete activations\n#\ndef deleteLogsFromOldActivations(args):\n    db = couchdb.client.Server(args.dbUrl)[args.dbName]\n    endkey = long(time.time() * 1000) - args.days * DAY\n    while True:\n        activations = db.view(\"logCleanup/byDateWithLogs\", limit=args.docsPerRequest, start_key=0, end_key=endkey, include_docs=True)\n        if activations:\n            activationsWithoutLogs = [removeLogFromActivation(activation) for activation in activations]\n            db.update(activationsWithoutLogs)\n        else:\n            return\n\nparser = argparse.ArgumentParser(description=\"Utility to delete logs from activations that are older than x days in given database.\")\nparser.add_argument(\"--dbUrl\", required=True, help=\"Server URL of the database, that has to be cleaned of old activations. E.g. 'https://xxx:yyy@domain.couch.com:443'\")\nparser.add_argument(\"--dbName\", required=True, help=\"Name of the Database of the activations to be truncated.\")\nparser.add_argument(\"--days\", required=True, type=int, help=\"How many days of the logs in activations to be kept.\")\nparser.add_argument(\"--docsPerRequest\", type=int, default=20, help=\"Number of documents handled on each CouchDb Request. Default is 20.\")\nargs = parser.parse_args()\n\ndeleteLogsFromOldActivations(args)\n"
  },
  {
    "path": "tools/db/moveCodeToAttachment.py",
    "content": "#!/usr/bin/env python\n'''Python script update actions.\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'''\n\nimport argparse\nimport couchdb.client\nimport time\nfrom couchdb import ResourceNotFound\n\ndef updateNonJavaAction(db, doc, id):\n    updated = False\n    code = doc['exec']['code']\n\n    if not isinstance(code, dict):\n        db.put_attachment(doc, code, 'codefile', 'text/plain')\n        doc = db.get(id)\n        doc['exec']['code'] = {\n            'attachmentName': 'codefile',\n            'attachmentType': 'text/plain'\n        }\n        db.save(doc)\n        updated = True\n\n    return updated\n\ndef createNonMigratedDoc(db):\n    try:\n        db['_design/nonMigrated']\n    except ResourceNotFound:\n        db.save({\n            '_id': '_design/nonMigrated',\n            'language': 'javascript',\n            'views': {\n                'actions': {\n                    'map': 'function (doc) {   var isAction = function (doc) {     return (doc.exec !== undefined)   };   var isMigrated = function (doc) {     return (doc._attachments !== undefined && doc._attachments.codefile !== undefined && typeof doc.code != \\'string\\')   };   if (isAction(doc) && !isMigrated(doc)) try {     emit([doc.name]);   } catch (e) {} }'\n                }\n            }\n        })\n\ndef deleteNonMigratedDoc(db):\n    del db['_design/nonMigrated']\n\ndef main(args):\n    db = couchdb.client.Server(args.dbUrl)[args.dbName]\n    createNonMigratedDoc(db)\n    docs = db.view('_design/nonMigrated/_view/actions')\n    docCount = len(docs)\n    docIndex = 1\n\n    print('Number of actions to update: {}'.format(docCount))\n\n    for row in docs:\n        id = row.id\n        doc = db.get(id)\n\n        print('Updating action {0}/{1}: \"{2}\"'.format(docIndex, docCount, id))\n\n        if 'exec' in doc and 'code' in doc['exec']:\n            if doc['exec']['kind'] != 'java':\n                updated = updateNonJavaAction(db, doc, id)\n            else:\n                updated = False\n\n            if updated:\n                print('Updated action: \"{0}\"'.format(id))\n                time.sleep(.500)\n            else:\n                print('Action already updated: \"{0}\"'.format(id))\n\n        docIndex = docIndex + 1\n\n    deleteNonMigratedDoc(db)\n\nparser = argparse.ArgumentParser(description='Utility to update database action schema.')\nparser.add_argument('--dbUrl', required=True, help='Server URL of the database. E.g. \\\"https://xxx:yyy@domain.couch.com:443\\\"')\nparser.add_argument('--dbName', required=True, help='Name of the Database to update.')\nargs = parser.parse_args()\n\nmain(args)\n"
  },
  {
    "path": "tools/db/replicateDbs.py",
    "content": "#!/usr/bin/env python\n\"\"\"Python script to replicate and replay databases.\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\n\nimport argparse\nimport time\nimport re\nimport couchdb.client\nimport functools\n\ndef retry(fn, retries):\n    try:\n        return fn()\n    except:\n        if (retries > 0):\n            time.sleep(1)\n            return retry(fn, retries - 1)\n        else:\n            raise\n\n\ndef replicateDatabases(args):\n    \"\"\"Replicate databases.\"\"\"\n    sourceDb = couchdb.client.Server(args.sourceDbUrl)\n    targetDb = couchdb.client.Server(args.targetDbUrl)\n\n    excludedDatabases = args.exclude.split(\",\")\n    excludedBaseNames = [x for x in args.excludeBaseName.split(\",\") if x != \"\"]\n\n    # Create _replicator DB if it does not exist yet.\n    if \"_replicator\" not in sourceDb:\n        sourceDb.create(\"_replicator\")\n\n    replicator = sourceDb[\"_replicator\"]\n\n    now = int(time.time())\n    backupPrefix = \"backup_%d_\" % now\n\n    def isExcluded(dbName):\n        dbNameWithoutPrefix = dbName.replace(args.dbPrefix, \"\", 1)\n        # is the databaseName is in the list of excluded database\n        isNameExcluded = dbNameWithoutPrefix in excludedDatabases\n        # if one of the basenames matches, the database is excluded\n        isBaseNameExcluded = functools.reduce(lambda x, y: x or y, [dbNameWithoutPrefix.startswith(en) for en in excludedBaseNames], False)\n        return isNameExcluded or isBaseNameExcluded\n\n    # Create backup of all databases with given prefix\n    print(\"----- Create backups -----\")\n    for db in [dbName for dbName in sourceDb if dbName.startswith(args.dbPrefix) and not isExcluded(dbName)]:\n        backupDb = backupPrefix + db if not args.continuous else 'continuous_' + db\n        replicateDesignDocument = {\n            \"_id\": backupDb,\n            \"source\": args.sourceDbUrl + \"/\" + db,\n            \"target\": args.targetDbUrl + \"/\" + backupDb,\n            \"create_target\": True,\n            \"continuous\": args.continuous,\n        }\n        print(\"create backup: %s\" % backupDb)\n\n        filterName = \"snapshotFilters\"\n        filterDesignDocument = sourceDb[db].get(\"_design/%s\" % filterName)\n        if not args.continuous and filterDesignDocument:\n            replicateDesignDocument[\"filter\"] = \"%s/withoutDeletedAndDesignDocuments\" % filterName\n        replicator.save(replicateDesignDocument)\n\n    def isBackupDb(dbName):\n        return re.match(\"^backup_\\d+_\" + args.dbPrefix, dbName)\n\n    def extractTimestamp(dbName):\n        return int(dbName.split(\"_\")[1])\n\n    def isExpired(timestamp):\n        return now - args.expires > timestamp\n\n    # Delete all documents in the _replicator-database of old backups to avoid that they continue after they are deprecated\n    print(\"----- Delete backup-documents older than %d seconds -----\" % args.expires)\n    for doc in [doc for doc in replicator.view('_all_docs', include_docs=True) if isBackupDb(doc.id) and isExpired(extractTimestamp(doc.id))]:\n        print(\"deleting backup document: %s\" % doc.id)\n        # Get again the latest version of the document to delete the right revision and avoid Conflicts\n        retry(lambda: replicator.delete(replicator[doc.id]), 5)\n\n    # Delete all backup-databases, that are older than specified\n    print(\"----- Delete backups older than %d seconds -----\" % args.expires)\n    for db in [db for db in targetDb if isBackupDb(db) and isExpired(extractTimestamp(db))]:\n        print(\"deleting backup: %s\" % db)\n        targetDb.delete(db)\n\n\ndef replayDatabases(args):\n    \"\"\"Replays databases.\"\"\"\n    sourceDb = couchdb.client.Server(args.sourceDbUrl)\n\n    # Create _replicator DB if it does not exist yet.\n    if \"_replicator\" not in sourceDb:\n        sourceDb.create(\"_replicator\")\n\n    for db in [dbName for dbName in sourceDb if dbName.startswith(args.dbPrefix)]:\n        plainDbName = db.replace(args.dbPrefix, \"\")\n        (identifier, _) = sourceDb[\"_replicator\"].save({\n            \"source\": args.sourceDbUrl + \"/\" + db,\n            \"target\": args.targetDbUrl + \"/\" + plainDbName,\n            \"create_target\": True\n        })\n        print(\"replaying backup: %s -> %s (%s)\" % (db, plainDbName, identifier))\n\nparser = argparse.ArgumentParser(description=\"Utility to create a backup of all databases with the defined prefix.\")\nparser.add_argument(\"--sourceDbUrl\", required=True, help=\"Server URL of the source database, that has to be backed up. E.g. 'https://xxx:yyy@domain.couch.com:443'\")\nparser.add_argument(\"--targetDbUrl\", required=True, help=\"Server URL of the target database, where the backup is stored. Like sourceDbUrl.\")\nsubparsers = parser.add_subparsers(help='sub-command help')\n\n# Replicate\nreplicateParser = subparsers.add_parser(\"replicate\", help=\"Replicates source databases to the target database.\")\nreplicateParser.add_argument(\"--dbPrefix\", required=True, help=\"Prefix of the databases, that should be backed up.\")\nreplicateParser.add_argument(\"--expires\", required=True, type=int, help=\"Deletes all backups, that are older than the given value in seconds.\")\nreplicateParser.add_argument(\"--continuous\", action=\"store_true\", help=\"Wether or not the backup should be continuous\")\nreplicateParser.add_argument(\"--exclude\", default=\"\", help=\"Comma separated list of database names, that should not be backed up. (Without prefix).\")\nreplicateParser.add_argument(\"--excludeBaseName\", default=\"\", help=\"Comma separated list of database base names. All databases, that have this basename in their name will not be backed up. (Without prefix).\")\nreplicateParser.set_defaults(func=replicateDatabases)\n\n# Replay\nreplicateParser = subparsers.add_parser(\"replay\", help=\"Replays source databases to the target database.\")\nreplicateParser.add_argument(\"--dbPrefix\", required=True, help=\"Prefix of the databases, that should be replayed. Usually 'backup_{TIMESTAMP}_'\")\nreplicateParser.set_defaults(func=replayDatabases)\n\narguments = parser.parse_args()\narguments.func(arguments)\n"
  },
  {
    "path": "tools/dev/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Utility Scripts\n\nThis module is a collection of few utility scripts for OpenWhisk development. The scripts\ncan be invoked as gradle tasks. Depending on your current directory the gradle command would\nchange\n\nWith current directory set to OpenWhisk home\n\n    ./gradlew :tools:dev:<taskName>\n\nWith this module being base directory\n\n    ../../gradlew <taskName>\n\n## couchdbViews\n\nExtracts and dump the design docs js in readable format. It reads all the design docs from\n_<OPENWHISH_HOME>/ansibles/files_ and dumps them in _build/views_ directory\n\nSample output\n\n    $./gradlew :tools:dev:couchdbViews\n    Processing whisks_design_document_for_entities_db_v2.1.0.json\n            - whisks.v2.1.0-rules.js\n            - whisks.v2.1.0-packages-public.js\n            - whisks.v2.1.0-packages.js\n            - whisks.v2.1.0-actions.js\n            - whisks.v2.1.0-triggers.js\n    Processing activations_design_document_for_activations_db.json\n            - activations-byDate.js\n    Processing auth_design_document_for_subjects_db_v2.0.0.json\n            - subjects.v2.0.0-identities.js\n    Processing filter_design_document.json\n    Processing whisks_design_document_for_activations_db_v2.1.0.json\n            - whisks.v2.1.0-activations.js\n    Skipping runtimes.json\n    Processing logCleanup_design_document_for_activations_db.json\n            - logCleanup-byDateWithLogs.js\n    Processing whisks_design_document_for_all_entities_db_v2.1.0.json\n            - all-whisks.v2.1.0-all.js\n    Processing whisks_design_document_for_activations_db_filters_v2.1.1.json\n            - whisks-filters.v2.1.1-activations.js\n    Generated view json files in /path/too/tools/build/views\n\n## IntelliJ Run Config Generator\n\nThis script enables creation of [Intellij Launch Configuration][1] in _<openwhisk home>/.idea/runConfigurations_\nwith name controller0 and invoker0. For this to work your Intellij project should be [directory based][3]. If your\nproject is file based (uses ipr files) then you can convert it to directory based via _File -> Save as Directory-Based Format_. These run configurations can then be invoked from _Run -> Edit Configurations -> Application_\n\n### Usage\n\nFirst setup OpenWhisk so that Controller and Invoker containers are up and running. Then run the script:\n\n    ./gradlew :tools:dev:intellij\n\nIt would inspect the running docker containers and then generate the launch configs with name 'controller0'\nand 'invoker0'.\n\nNow the docker container(s) (controller and/or invoker) can be stopped and they can be launched instead from within the IDE.\n\nKey points to note:\n\n1. Controller uses port `10001` and Invoker uses port `12001`.\n2. Action activation logs are [disabled][2].\n3. SSL is disabled for Controller and Invoker.\n4. Make sure you have the loopback interface configured:\n   ```bash\n   sudo ifconfig lo0 alias 172.17.0.1/24\n   ```\n5. `~/.wskprops` must be updated with `APIHOST=http://localhost:10001` so that the `wsk` CLI communicates directly with the controller.\n6. On a MAC\n   * With Docker For Mac the invoker is configured to use a Container Factory that exposes ports for actions on the host,\n     as otherwise the invoker can't make HTTP requests to the actions.\n     You can read more at [docker/for-mac#171][7].\n\n   * When using [docker-compose][8] locally you have to update `/etc/hosts` with the line bellow:\n      ```\n      127.0.0.1       kafka zookeeper kafka.docker zookeeper.docker db.docker controller whisk.controller\n      ```\n\n\n### Configuration\n\nThe script allows some local customization of the launch configuration. This can be done by creating a [config][4] file\n`intellij-run-config.groovy` in project root directory. Below is an example of _<openwhisk home>/intellij-run-config.groovy_\nfile to customize the logging and db port used for CouchDB.\n\n```groovy\n//Configures the settings for controller application\ncontroller {\n    //Base directory used for controller process\n    workingDir = \"/path/to/controller\"\n    //System properties to be set\n    props = [\n            'logback.configurationFile':'/path/to/custom/logback.xml'\n    ]\n    //Environment variables to be set\n    env = [\n            'DB_PORT' : '5989',\n            'CONFIG_whisk_controller_protocol' : 'http'\n    ]\n}\n\ninvoker {\n    workingDir = \"/path/to/invoker\"\n    props = [\n            'logback.configurationFile':'/path/to/custom/logback.xml'\n    ]\n    env = [\n            'DB_PORT' : '5989'\n    ]\n}\n\n```\n\nThe config allows following properties:\n\n* `workingDir` - Base directory used for controller or invoker process.\n* `props` - Map of system properties which should be passed to the application.\n* `env` - Map of environment variables which should be set for application process.\n\n## Github Repository Lister\n\nLists all Apache OpenWhisk related repositories by using [Github Search API][5] with pagination. Its preferable that prior\nto using this you specify a [Github Access Token][6] as otherwise requests will quickly become rate limited. The token\ncan be specified by setting environment variable `GITHUB_ACCESS_TOKEN`\n\n```bash\n$ ./gradlew :tools:dev:listRepos\nFound 44 repositories\nopenwhisk\nopenwhisk-GitHubSlackBot\nopenwhisk-apigateway\nopenwhisk-catalog\n...\nStored the list in /openwhisk_home/build/repos/repos.txt\nStored the JSON details in /openwhisk_home/build/repos/repos.json\n\n```\n\nIt generates 2 files\n\n* `repos.txt` - List repository names one per line.\n* `repos.json` - Stores an array of repository details JSON containing various repository related details.\n\n## OpenWhisk Module Status Generator\n\nIt renders a markdown file which lists the status of various OpenWhisk modules by using the output generated by `listRepos`\ntask. The rendered markdown file is stored in `docs/dev/modules.md`. This rendered file should be later checked in.\n\n```bash\n$ ./gradlew :tools:dev:renderModuleDetails\n\n  > Task :tools:dev:renderModuleDetails\n  Generated modules details at /openwhisk_home/docs/dev/modules.md\n\n```\n\n[1]: https://www.jetbrains.com/help/idea/run-debug-configurations-dialog.html#run_config_common_options\n[2]: https://github.com/apache/openwhisk/issues/3195\n[3]: https://www.jetbrains.com/help/idea/configuring-projects.html#project-formats\n[4]: http://docs.groovy-lang.org/2.4.2/html/gapi/groovy/util/ConfigSlurper.html\n[5]: https://developer.github.com/v3/search/\n[6]: https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/\n[7]: https://github.com/docker/for-mac/issues/171\n[8]: https://github.com/apache/openwhisk-devtools/tree/master/docker-compose\n"
  },
  {
    "path": "tools/dev/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nplugins {\n    id 'groovy'\n}\n\nrepositories {\n    mavenCentral()\n}\n\ndef owHome = project.projectDir.parentFile.parentFile\n\ndependencies {\n    implementation \"org.codehaus.groovy:groovy-all:3.0.17\"\n    implementation \"commons-io:commons-io:2.11.0\"\n    implementation \"org.apache.commons:commons-lang3:3.8.1\"\n}\n\ntask couchdbViews(type: JavaExec) {\n    description 'Dumps CouchDB views as js files'\n    mainClass = 'couchdbViews'\n    args owHome.absolutePath\n    classpath = sourceSets.main.runtimeClasspath\n}\n\ntask intellij(type: JavaExec) {\n    description 'Generates Intellij run config for Controller and Invoker'\n    mainClass = 'intellijRunConfig'\n    args owHome.absolutePath\n    classpath = sourceSets.main.runtimeClasspath\n}\n\ntask listRepos(type: JavaExec) {\n    description 'Generates a list of all OpenWhisk related Git repos'\n    mainClass = 'listRepos'\n    args owHome.absolutePath\n    classpath = sourceSets.main.runtimeClasspath\n}\n\ntask renderModuleDetails(type: JavaExec) {\n    description 'Renders modules details'\n    mainClass = 'renderModuleDetails'\n    args owHome.absolutePath\n    classpath = sourceSets.main.runtimeClasspath\n}\n"
  },
  {
    "path": "tools/dev/src/main/groovy/CategoryManager.groovy",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport java.util.function.Predicate\n\nclass CategoryManager {\n    def categories = process([\n        [name: \"Main\", travis: true, archived: false, suffixes: ['openwhisk', 'apigateway', 'catalog', 'cli', 'wskdeploy', 'composer', 'composer-python']],\n        [name: \"Clients\", travis: true, archived: false, contains: ['-client-']],\n        [name: \"Runtimes\", travis: true, archived: false, contains: ['-runtime-']],\n        [name: \"Deployments\", travis: true, archived: false, contains: ['-deploy-']],\n        [name: \"Packages\", travis: true, archived: false, contains: ['-package-', '-provider']],\n        [name: \"Samples and Examples\", travis: false, archived: false, suffixes: ['slackinvite']],\n        [name: \"Development Tools\", travis: false, archived: false, suffixes: ['devtools', 'wskdebug', '-plugin', '-extension']],\n        [name: \"Utilities\", travis: false, archived: false, suffixes: ['utilities', 'release']],\n        [name: \"Others\", travis: false, archived: false],\n        [name: \"Archived\", travis: false, archived: true]\n    ])\n\n    private def suffixMatcher(List<String> suffixes) {\n        return {name -> suffixes.any {name.endsWith(it)}} as Predicate<String>\n    }\n\n    private def containsMatcher(List<String> marker) {\n        return {name -> marker.any {name.contains(it)}} as Predicate<String>\n    }\n\n    private def createMatcher(Map m){\n        if (m.containsKey('suffixes')) return suffixMatcher(m.suffixes)\n        else if (m.containsKey('contains')) return containsMatcher(m['contains'])\n        else return {true} as Predicate\n    }\n\n    private def process(List<Map> repos) {\n        repos.collect {m -> new Category(m.name, m.travis, m.archived, createMatcher(m))}\n    }\n\n    def addToCategory(repo) {\n        categories.find {c -> c.matches(repo.name) && c.archived == repo.archived}.addRepo(repo)\n    }\n\n    def sort(){\n        categories.each {it.sort()}\n    }\n}\n\nclass Category {\n    String name\n    boolean travisEnabled\n    boolean archived\n    List repos = []\n    Predicate<String> matcher\n\n    Category(name, travisEnabled, archived, matcher) {\n        this.name = name\n        this.travisEnabled = travisEnabled\n        this.archived = archived\n        this.matcher = matcher\n    }\n\n    def matches(String repoName) {\n        matcher.test(repoName)\n    }\n\n    def addRepo(repo){\n        repos << repo\n    }\n\n    def sort() {\n        repos.sort {a, b -> a.name <=> b.name}\n    }\n}\n"
  },
  {
    "path": "tools/dev/src/main/groovy/couchdbViews.groovy",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport groovy.json.JsonSlurper\n\nassert args : \"Expecting the OpenWhisk home directory to passed\"\ndef owHomePath = args[0]\n\nFile designDocDir = new File(\"$owHomePath/ansible/files\")\nFile buildDir = createFreshBuildDir()\n\ndesignDocDir.listFiles({it.name.endsWith(\".json\")} as FileFilter).each {File file ->\n    def json = new JsonSlurper().parse(file)\n\n    //Design docs json have first field as _id. So use that to determine if json\n    //is a design doc or not\n    String id = json._id\n    if (id && id.startsWith(\"_design/\")){\n        println \"Processing ${file.name}\"\n        String baseName = id.substring(\"_design/\".length())\n        json.views?.each{String viewName, def view ->\n            String viewJs = parseViewJs(view.map)\n            File viewFile = new File(buildDir, \"$baseName-${viewName}.js\")\n            viewFile.text = viewJs\n            println \"\\t- ${viewFile.name}\"\n        }\n    } else {\n        println \"Skipping ${file.name}\"\n    }\n}\nprintln \"Generated view json files in ${buildDir.absolutePath}\"\n\nprivate static File createFreshBuildDir() {\n    File dir = new File(\"build/views\")\n    if (dir.exists()) {\n        dir.deleteDir()\n    }\n    dir.mkdirs()\n    dir\n}\n\nprivate static String parseViewJs(String jsonText) {\n    jsonText.replace(\"\\\\n\", \"\")\n            .replace('\\\"','\"')\n}\n"
  },
  {
    "path": "tools/dev/src/main/groovy/intellijRunConfig.groovy",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport groovy.json.JsonSlurper\nimport groovy.text.SimpleTemplateEngine\nimport org.apache.commons.lang3.SystemUtils\n\nassert args : \"Expecting the OpenWhisk home directory to passed\"\nowHome = args[0]\n\n//Launch config template\ndef configTemplate = '''<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"${name}\" type=\"Application\" factoryName=\"Application\">\n    <extension name=\"coverage\" enabled=\"false\" merge=\"false\" sample_coverage=\"true\" runner=\"idea\" />\n    <option name=\"MAIN_CLASS_NAME\" value=\"$main\" />\n    <option name=\"VM_PARAMETERS\" value=\"$sysProps\" />\n    <option name=\"PROGRAM_PARAMETERS\" value=\"$programParams\" />\n    <option name=\"WORKING_DIRECTORY\" value=\"$workingDir\" />\n    <option name=\"ALTERNATIVE_JRE_PATH_ENABLED\" value=\"false\" />\n    <option name=\"ALTERNATIVE_JRE_PATH\" />\n    <option name=\"ENABLE_SWING_INSPECTOR\" value=\"false\" />\n    <option name=\"ENV_VARIABLES\" />\n    <option name=\"PASS_PARENT_ENVS\" value=\"true\" />\n    <module name=\"openwhisk.core.${type}.main\" />\n    <envs>\n      <% env.each { k,v -> %><env name=\"$k\" value=\"$v\" />\n      <% } %>\n    </envs>\n    <method />\n  </configuration>\n</component>\n'''\n\ndef meta = [\n        controller : [main:\"org.apache.openwhisk.core.controller.Controller\"],\n        invoker : [main:\"org.apache.openwhisk.core.invoker.Invoker\"]\n]\n\n//Get names of all running containers\ndef containerNames = 'docker ps --format {{.Names}}'.execute().text.split(\"\\n\")\n\nconfig = getConfig()\n\nMap controllerEnv = null\nMap invokerEnv = null\n\ncontainerNames.each{cn ->\n\n    //Inspect the specific container\n    def inspectResult = \"docker inspect $cn\".execute().text\n    def json = new JsonSlurper().parseText(inspectResult)\n\n    def imageName = json[0].'Config'.'Image'\n    if (imageName.contains(\"controller\") || imageName.contains(\"invoker\")){\n        // pre-configure the local ports for controller and invoker\n        def mappedPort = imageName.contains(\"controller\") ? '10001' : '12001'\n\n        def envBaseMap = getEnvMap(json[0].'Config'.'Env')\n        String type\n        if (imageName.contains(\"controller\")){\n            type = \"controller\"\n            controllerEnv = envBaseMap\n        } else {\n            type = \"invoker\"\n            invokerEnv = envBaseMap\n        }\n\n        def overrides = [\n                'PORT' : mappedPort,\n                'WHISK_LOGS_DIR' : \"$owHome/core/$type/build/tmp\"\n        ]\n\n        def envMap = getEnv(envBaseMap, type, overrides)\n\n        //Prepare system properties\n        def sysProps = getSysProps(envMap,type)\n        // disable log collection. See more at: https://github.com/apache/openwhisk/issues/3195\n        sysProps += \" -Dwhisk.log-limit.max=0 -Dwhisk.log-limit.std=0\"\n        // disable https protocol for controller and invoker\n        sysProps = sysProps.replaceAll(\"protocol=https\", \"protocol=http\")\n        if (SystemUtils.IS_OS_MAC){\n            sysProps = sysProps.replaceAll(\"use-runc=True\", \"use-runc=False\")\n            sysProps += \" -Dwhisk.spi.ContainerFactoryProvider=org.apache.openwhisk.core.containerpool.docker.DockerForMacContainerFactoryProvider\"\n        }\n\n        def templateBinding = [\n                main: meta[type].main,\n                type:type,\n                name:cn,\n                env: encodeForXML(envMap),\n                sysProps : sysProps,\n                USER_HOME : '$USER_HOME$',\n                workingDir : getWorkDir(type),\n                programParams: imageName.contains(\"controller\") ? '0' : '--id  0'\n        ]\n\n        def engine = new SimpleTemplateEngine()\n        def template = engine.createTemplate(configTemplate).make(templateBinding)\n\n        def launchFile = new File(\"$owHome/.idea/runConfigurations/${cn}.xml\")\n        launchFile.parentFile.mkdirs()\n        launchFile.text = template\n        println \"Created ${launchFile.absolutePath}\"\n    }\n}\n\n/**\n * Computes the env values which are common and then specific to controller and invoker\n * and dumps them to a file. This can be used for docker-compose\n */\nif (controllerEnv != null && invokerEnv != null){\n    Set<String> commonKeys = controllerEnv.keySet().intersect(invokerEnv.keySet())\n\n    SortedMap commonEnv = new TreeMap()\n    SortedMap controllerSpecificEnv = new TreeMap(controllerEnv)\n    SortedMap invokerSpecificEnv = new TreeMap(invokerEnv)\n    commonKeys.each{ key ->\n        if (controllerEnv[key] == invokerEnv[key]){\n            commonEnv[key] = controllerEnv[key]\n            controllerSpecificEnv.remove(key)\n            invokerSpecificEnv.remove(key)\n        }\n    }\n\n    copyEnvToFile(commonEnv,\"whisk-common.env\")\n    copyEnvToFile(controllerSpecificEnv,\"whisk-controller.env\")\n    copyEnvToFile(invokerSpecificEnv,\"whisk-invoker.env\")\n}\n\ndef copyEnvToFile(SortedMap envMap,String envFileName){\n    File envFile = new File(getEnvFileDir(), envFileName)\n    envFile.withPrintWriter {pw ->\n        envMap.each{k,v ->\n            pw.println(\"$k=$v\")\n\n        }\n    }\n    println \"Wrote env to ${envFile.absolutePath}\"\n}\n\nprivate File getEnvFileDir() {\n    File dir = new File(new File(\"build\"), \"env\")\n    dir.mkdirs()\n    return dir\n}\n\n/**\n * Reads config from intellij-run-config.groovy file\n */\ndef getConfig(){\n    def configFile = new File(owHome, 'intellij-run-config.groovy')\n    def config = configFile.exists() ? new ConfigSlurper().parse(configFile.text) : new ConfigObject()\n    if (configFile.exists()) {\n        println \"Reading config from ${configFile.absolutePath}\"\n    }\n    config\n}\n\ndef getWorkDir(String type){\n    def dir = config[type].workingDir\n    if (dir){\n        File f = new File(dir)\n        if (!f.exists()) {\n            f.mkdirs()\n        }\n        dir\n    }else {\n        'file://$MODULE_DIR$'\n    }\n}\n\ndef getSysProps(def envMap, String type){\n    def props = config[type].props\n    def sysProps = transformEnv(envMap)\n    sysProps.putAll(props)\n    sysProps.collect{k,v -> \"-D$k='$v'\"}.join(' ').replace('\"','').replace('\\'','')\n}\n\n//Implements the logic from transformEnvironment.sh\n//to ensure comparability as sed -r is not supported on Mac\ndef transformEnv(Map<String, String> envMap){\n    def transformedMap = [:]\n    envMap.each{String k,String v ->\n        if (!k.startsWith(\"CONFIG_\") || v.isEmpty()) return\n        k = k.substring(\"CONFIG_\".length())\n        def parts = k.split(\"\\\\_\")\n        def transformedKey = parts.collect {p ->\n            if (Character.isUpperCase(p[0] as char)){\n                // if the current part starts with an uppercase letter (is PascalCased)\n                // leave it alone\n                return p\n            } else {\n                // rewrite camelCased to kebab-cased\n                return p.replaceAll(/([a-z0-9])([A-Z])/,/$1-$2/).toLowerCase()\n            }\n        }\n\n        //Resolve values which again refer to env variables\n        if (v.startsWith('$')) {\n            def valueAsKey = v.substring(1)\n            if (envMap.containsKey(valueAsKey)){\n                v = envMap.get(valueAsKey)\n            }\n        }\n        transformedMap[transformedKey.join('.')] = v\n    }\n    return transformedMap\n}\n\n/**\n * Inspect command from docker returns the environment variables as list of string of form key=value\n * This method converts it to map and add provided overrides with overrides from config\n */\ndef getEnv(Map envMap, String type, Map overrides){\n    def ignoredKeys = ['PATH','JAVA_HOME','JAVA_VERSION','JAVA_TOOL_OPTIONS']\n    def overridesFromConfig = config[type].env\n    Map sortedMap = new TreeMap(envMap)\n    sortedMap.putAll(overrides)\n\n    //Config override come last\n    sortedMap.putAll(overridesFromConfig)\n\n    //Remove ignored keys like PATH which should be inherited\n    ignoredKeys.each {sortedMap.remove(it)}\n    sortedMap\n}\n\ndef getEnvMap(def env){\n    def envMap = env.collectEntries {String e ->\n        def eqIndex = e.indexOf('=')\n        def k = e.substring(0, eqIndex)\n        def v = e.substring(eqIndex + 1)\n        [(k):v]\n    }\n    def sortedMap = new TreeMap()\n    sortedMap.putAll(envMap)\n    Collections.unmodifiableSortedMap(sortedMap)\n}\n\ndef getEnvAsList(Map envMap){\n    envMap.collect{k,v -> \"$k=$v\"}\n}\n\ndef encodeForXML(Map map){\n    map.collectEntries {k,v -> [(k): v.replace('\"', '&quot;')]}\n}\n"
  },
  {
    "path": "tools/dev/src/main/groovy/listRepos.groovy",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport groovy.json.JsonOutput\nimport groovy.json.JsonSlurper\nimport org.apache.commons.io.FilenameUtils\n\n// script to read the Apache OpenWhisk repositories using the Github API\n// it is recommended to use authentication by setting the 'GITHUB_ACCESS_TOKEN' env\n// as otherwise requests will quickly become rate limited\n\nassert args : \"Expecting the OpenWhisk home directory to passed\"\nowHomePath = args[0]\n\ndef parser = new JsonSlurper()\n\n// get as many repos per page as possible to cut down on the number of calls\ndef link = \"https://api.github.com/orgs/apache/repos?per_page=100\"\ndef creds = getCredentials()\ndef owRepos = []\n\nif (!creds) {\n    println \"It is recommended to pass access token via env variable 'GITHUB_ACCESS_TOKEN' as otherwise requests will quickly become rate limited\"\n}\n\nwhile ( link ) {\n    def url = new URL(link)\n    def conn = url.openConnection()\n    if ( creds ) {\n        conn.setRequestProperty(\"Authorization\", \"Basic \" + creds.bytes.encodeBase64())\n    }\n\n    // add all projects matching naming conventions\n    def result = parser.parse(conn.inputStream)\n    owRepos += result\n            .findAll { it.name.startsWith('openwhisk') }\n\n    // find link to next page, if applicable\n    link = null\n\n    def links = conn.headerFields['Link']\n    if ( links ) {\n        def next = links[0].split(',').find{ it.contains('rel=\"next\"') }\n        link = next != null ? next.find('<(.*)>').replaceAll('<|>',''): null\n    }\n}\n\n\n// ensure a consistent order\nowRepos.sort {\n    a,b -> a.name <=> b.name\n}\n\ndef owReoNames = owRepos.collect {it.name}\n\ndef nameListFile = createNameListFile()\ndef jsonFile = createRepoJsonFile()\ndef list = owReoNames.join(\"\\n\")\n\nnameListFile.text = list\njsonFile.text = JsonOutput.prettyPrint(JsonOutput.toJson(owRepos))\n\nprintln(\"Found ${owRepos.size()} repositories\")\nprintln(list)\nprintln(\"Stored the list in ${nameListFile.getAbsolutePath()}\")\nprintln(\"Stored the json details in ${jsonFile.getAbsolutePath()}\")\n\ndef getCredentials(){\n    String creds = System.getenv(\"GITHUB_ACCESS_TOKEN\")\n    creds\n}\n\ndef createNameListFile(){\n    new File(getOutDir(), \"repos.txt\")\n}\n\ndef createRepoJsonFile(){\n    new File(getOutDir(), \"repos.json\")\n}\n\ndef getOutDir(){\n    def dir = new File(FilenameUtils.concat(owHomePath, \"build/repos\"))\n    dir.mkdirs()\n    dir\n}\n"
  },
  {
    "path": "tools/dev/src/main/groovy/renderModuleDetails.groovy",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport groovy.json.JsonSlurper\nimport groovy.text.SimpleTemplateEngine\nimport org.apache.commons.io.FilenameUtils\n\nassert args : \"Expecting the OpenWhisk home directory to passed\"\nowHomePath = args[0]\n\ndef repos = loadRepoJson()\n\ndef template = getClass().getResource(\"./modules.md\").text\ndef engine = new SimpleTemplateEngine()\n\ndef categoryManager = new CategoryManager()\n\nrepos.each{ repo ->categoryManager.addToCategory(repo)}\ncategoryManager.sort()\n\ndef binding = [\"categories\":categoryManager.categories]\ndef result = engine.createTemplate(template).make(binding)\n\ndef file = getModuleOutputFile()\nfile.setText(result.toString(), 'UTF-8')\nprintln \"Generated modules details at ${file.getAbsolutePath()}\"\n\ndef loadRepoJson(){\n    File file = new File(FilenameUtils.concat(owHomePath, \"build/repos/repos.json\"))\n    assert file.exists() : \"Did not found ${file.absolutePath}. Run './gradlew :tools:dev:listRepos' prior to this script\"\n    def parser = new JsonSlurper()\n    parser.parseText(file.text)\n}\n\ndef getModuleOutputFile(){\n    new File(FilenameUtils.concat(owHomePath, \"docs/dev/modules.md\"))\n}\n"
  },
  {
    "path": "tools/dev/src/main/resources/modules.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n<!--\nDO NOT EDIT.\nThis page is generated via script `./gradlew :tools:dev:renderModuleDetails`. See tools/dev/README.md for details.\n-->\n\n# Modules\n\n<% categories.each { c -> %>\n## ${c.name}\n<% if (c.travisEnabled) {%>\n| Module | Description | Module Status |\n|---\t|---\t|---    |\n<% c.repos.each { repo -> %>| [${repo.name}](https://github.com/apache/${repo.name}) | ${repo.description} | [![Build Status](https://travis-ci.com/apache/${repo.name}.svg?branch=master)](https://travis-ci.com/apache/${repo.name}) |\n<% }  %><% } else { %>\n| Module | Description |\n|---\t|---\t|\n<% c.repos.each { repo -> %>| [${repo.name}](https://github.com/apache/${repo.name}) | ${repo.description} |\n<% } %><% } %>\n<% } %>\n"
  },
  {
    "path": "tools/eclipse/java.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<profiles version=\"12\">\n<profile kind=\"CodeFormatterProfile\" name=\"whisk-java\" version=\"12\">\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_ellipsis\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_after_imports\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_javadoc_comments\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indentation.size\" value=\"4\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.disabling_tag\" value=\"@formatter:off\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.continuation_indentation\" value=\"2\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_enum_constants\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_imports\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_after_package\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_binary_operator\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.indent_root_tags\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.enabling_tag\" value=\"@formatter:on\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.compiler.problem.enumIdentifier\" value=\"error\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_statements_compare_to_block\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.line_length\" value=\"80\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.use_on_off_tags\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_method_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_binary_expression\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_block\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_lambda_body\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.compact_else_if\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.compiler.problem.assertIdentifier\" value=\"error\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_binary_operator\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_unary_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_ellipsis\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_line_comments\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.align_type_members_on_columns\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_assignment\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_conditional_expression\" value=\"80\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_block_in_case\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_header\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode\" value=\"enabled\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_method_declaration\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.join_wrapped_lines\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_resources_in_try\" value=\"80\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.compiler.source\" value=\"1.8\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.tabulation.size\" value=\"4\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_source_code\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_field\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer\" value=\"2\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_method\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.compiler.codegen.targetPlatform\" value=\"1.8\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_switch\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_html\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_compact_if\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_empty_lines\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_unary_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_label\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_member_type\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_semicolon\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.format_block_comments\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line\" value=\"false\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_statements_compare_to_body\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.alignment_for_multiple_fields\" value=\"16\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_array_initializer\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.wrap_before_binary_operator\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.compiler.compliance\" value=\"1.8\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_enum_constant\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.brace_position_for_type_declaration\" value=\"end_of_line\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_before_package\" value=\"0\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.join_lines_in_comments\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional\" value=\"insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.comment.indent_parameter_description\" value=\"true\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.tabulation.char\" value=\"space\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.blank_lines_between_import_groups\" value=\"1\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.lineSplit\" value=\"250\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation\" value=\"do not insert\"/>\n<setting id=\"org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch\" value=\"insert\"/>\n</profile>\n</profiles>\n"
  },
  {
    "path": "tools/eclipse/scala.properties",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#Scalariform formatter preferences\n#Wed Apr 20 09:26:25 EDT 2016\nalignParameters=true\nformatXml=true\npreserveDanglingCloseParenthesis=false\nspaceInsideBrackets=false\nindentWithTabs=false\nspaceInsideParentheses=false\nmultilineScaladocCommentsStartOnFirstLine=false\nalignSingleLineCaseStatements=true\ncompactStringConcatenation=false\nplaceScaladocAsterisksBeneathSecondAsterisk=false\nindentPackageBlocks=true\ncompactControlReadability=false\nspacesWithinPatternBinders=true\nalignSingleLineCaseStatements.maxArrowIndent=40\ndoubleIndentClassDeclaration=false\npreserveSpaceBeforeArguments=false\nspaceBeforeColon=false\nrewriteArrowSymbols=false\nindentLocalDefs=false\nindentSpaces=2\n"
  },
  {
    "path": "tools/git/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Purpose\n\nThis directory contains shell scripts to be used in conjunction with `git` operations.\n\n## Pre-commit hooks\n\nThis directory contains following `pre-commit` hooks. Read `man githooks` for details\nabout `pre-commit` hooks and how to install / use them.\n\n### Scala source formatting\n\nAny of the following `pre-commit` hooks can be used to format all staged Scala source files (`*.scala`)\naccording to project standards. Said files are changed in-place and re-staged so that committed\nfiles are properly formatted.\n\n* `pre-commit-scalafmt-gradlew.sh`: Use Gradle wrapper for formatting.\n* `pre-commit-scalafmt-native.sh`: Use `scalafmt` command for formatting. Less overhead and thus,\n  faster than Gradle wrapper approach. You have to install `scalafmt` command - see http://scalameta.org/scalafmt/.\n"
  },
  {
    "path": "tools/git/pre-commit-scalafmt-gradlew.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# Purpose: Run this script as a git pre-commit hook to apply project-specific\n#          Scala formatting rules to all staged Scala source files (*.scala).\n#          Uses Gradle wrapper to perform Scala formatting with `scalafmt`.\n#          The script will re-stage the formatted Scala source files.\n\n# Uncomment the following line to obtain Shell script execution tracing.\n# set -x\n\n# -u: fail if variable is undefined\n# -f: disable globbing = file name expansion with regular expressions\n# -e: fail on non-zero exit code\nset -u -f -e\n\n# Determine OpenWhisk base directory\nROOT_DIR=\"$(git rev-parse --show-toplevel)\"\n\n# Run `scalafmt` iff there are staged .scala source files\nset +e\nSTAGED_SCALA_FILES=$(git diff --cached --name-only --no-color --diff-filter=d --exit-code -- \"${ROOT_DIR}/*.scala\")\nSTAGED_SCALA_FILES_DETECTED=$?\nset -e\n\nif [ \"${STAGED_SCALA_FILES_DETECTED}\" -eq 1 ]; then\n    # Re-format scala code iff a scala file is staged\n    \"${ROOT_DIR}/gradlew\" --project-dir \"${ROOT_DIR}\" scalafmtAll\n\n    # Re-add all staged .scala files\n    for SCALA_FILE in ${STAGED_SCALA_FILES}\n    do\n      git add -- \"${ROOT_DIR}/${SCALA_FILE}\"\n    done\nfi\n\nexit 0\n"
  },
  {
    "path": "tools/git/pre-commit-scalafmt-native.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n# Purpose: Run this script as a git pre-commit hook to apply project-specific\n#          Scala formatting rules to all staged Scala source files (*.scala).\n#          Uses native command to perform Scala formatting with `scalafmt`.\n#          The script will re-stage the formatted Scala source files.\n#\n# Prerequisites: `scalafmt` command needs to be installed on the system.\n#                See http://scalameta.org/scalafmt/\n\n# Uncomment the following line to obtain Shell script execution tracing.\n# set -x\n\n# -u: fail if variable is undefined\n# -f: disable globbing = file name expansion with regular expressions\n# -e: fail on non-zero exit code\nset -u -f -e\n\n# Determine if `scalafmt` command is available and exit if not.\nset +e\nhash scalafmt\nSCALAFMT_CHECK=$?\nset -e\n\nif [ \"${SCALAFMT_CHECK}\" -ne 0 ]; then\n  echo \"Required command 'scalafmt' not found. Please install.\"\n  echo \"See http://scalameta.org/scalafmt/\"\n  exit 1\nfi\n\n# Determine OpenWhisk base directory\nROOT_DIR=\"$(git rev-parse --show-toplevel)\"\n\n# Run `scalafmt` iff there are staged .scala source files\nset +e\nSTAGED_SCALA_FILES=$(git diff --cached --name-only --no-color --diff-filter=d --exit-code -- \"${ROOT_DIR}/*.scala\")\nSTAGED_SCALA_FILES_DETECTED=$?\nset -e\n\nif [ \"${STAGED_SCALA_FILES_DETECTED}\" -eq 1 ]; then\n    # Re-format and re-add all staged .scala files\n    for SCALA_FILE in ${STAGED_SCALA_FILES}\n    do\n      scalafmt --config \"${ROOT_DIR}/.scalafmt.conf\" \"${ROOT_DIR}/${SCALA_FILE}\"\n      git add -- \"${ROOT_DIR}/${SCALA_FILE}\"\n    done\nfi\n\nexit 0\n"
  },
  {
    "path": "tools/github/checkAndUploadLogs.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# showing test results on the CI log\nINDEX=\"tests/build/reports/tests/testCoverageLean/index.html\"\ntest -f \"$INDEX\" && lynx -dump file://$PWD/$INDEX | grep .\n\n# check variables\nfor i in LOG_BUCKET LOG_ACCESS_KEY_ID LOG_SECRET_ACCESS_KEY LOG_REGION\ndo\n  if test -z \"${!i}\"\n  then echo \"Required Environment Variable Missing: $i\" ; exit 0\n  fi\ndone\n\n# Disable abort script at first error as we require the logs to be uploaded\n# even if check and log collection fails\n# set -e\n\n\nSECONDS=0\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR\n\nLOG_NAME=\"$1\"\n\n# tags is db only when the test is unit\nTAGS=\"\"\n[[ \"$2\" == \"Unit\" ]] && TAGS=\"db\"\n\nLOG_DIR=\"$(date +%Y-%m-%d)/${LOG_NAME}-${GH_BUILD}-${GH_BRANCH}\"\nBUCKET_URL=\"https://$LOG_BUCKET.s3.$LOG_REGION.amazonaws.com\"\n\necho \"Logs: ${BUCKET_URL}/index.html#${LOG_DIR}/\"\necho \"Reports: ${BUCKET_URL}/${LOG_DIR}/test-reports/reports/tests/testCoverageLean/index.html\"\n\necho \"logs=${BUCKET_URL}/index.html#${LOG_DIR}/\" >>${GITHUB_OUTPUT:-/dev/stdin}\necho \"report=${BUCKET_URL}/${LOG_DIR}/test-reports/reports/tests/testCoverageLean/index.html\" >>${GITHUB_OUTPUT:-/dev/stdin}\n\nansible-playbook -i ansible/environments/local ansible/logs.yml\n\n./tools/build/checkLogs.py logs \"$TAGS\"\n\n./tools/github/s3-upload.sh \"$PWD/logs\" \"$LOG_DIR\"\n\necho \"Time taken for ${0##*/} is $SECONDS secs\"\n"
  },
  {
    "path": "tools/github/debugAction.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nif [[ -z \"$NGROK_DEBUG\" ]] || [[ \"$NGROK_DEBUG\" == \"false\" ]]\nthen exit 0\nfi\n\nif [[ -z \"$NGROK_TOKEN\" ]]\nthen echo \"Please set 'NGROK_TOKEN'\"\n     exit 1\nfi\n\nif [[ -z \"$NGROK_PASSWORD\" ]]\nthen echo \"Please set 'NGROK_PASSWORD'\"\n     exit 1\nfi\n\necho \"### Install ngrok ###\"\nif ! test -e ./ngrok\nthen\n  wget -q https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-386.tgz\n  tar xvf ngrok-v3-stable-linux-386.tgz\n  chmod +x ./ngrok\nfi\n\necho \"### Update user: $USER password ###\"\necho -e \"$NGROK_PASSWORD\\n$NGROK_PASSWORD\" | sudo passwd \"$USER\"\n\necho \"### Start ngrok proxy for 22 port ###\"\n\nrm -f .ngrok.log\n./ngrok authtoken \"$NGROK_TOKEN\"\n./ngrok tcp 22 --log \".ngrok.log\" &\n\nsleep 10\nHAS_ERRORS=$(grep \"command failed\" < .ngrok.log)\n\nif [[ -z \"$HAS_ERRORS\" ]]; then\n  MSG=\"To connect: $(grep -o -E \"tcp://(.+)\" < .ngrok.log | sed \"s/tcp:\\/\\//ssh $USER@/\" | sed \"s/:/ -p /\")\"\n  echo \"\"\n  echo \"==========================================\"\n  echo \"$MSG\"\n  echo \"==========================================\"\n  if test -n \"$SLACK_WEBHOOK\"\n  then\n      echo -n '{\"text\":' >/tmp/msg$$\n      echo -n \"$MSG\" | jq -Rsa . >>/tmp/msg$$\n      echo -n '}' >>/tmp/msg$$\n      curl -X POST -H 'Content-type: application/json' --data \"@/tmp/msg$$\" \"$SLACK_WEBHOOK\"\n  fi\nelse\n  echo \"$HAS_ERRORS\"\n  exit 1\nfi\n"
  },
  {
    "path": "tools/github/flake8.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\npip3 install --user --upgrade flake8\n\n# These files do not have a .py extension so flake8 will not scan them\ndeclare -a PYTHON_FILES=(\".\"\n                         \"./tools/admin/wskadmin\"\n                         \"./tools/build/citool\"\n                         \"./tools/build/redo\")\n\necho 'Flake8: first round (fast fail) stops the build if there are any Python 3 syntax errors...'\nfor i in \"${PYTHON_FILES[@]}\"\ndo\n    flake8 \"$i\" --select=E999,F821 --statistics\n    RETURN_CODE=$?\n    if [ $RETURN_CODE != 0 ]; then\n        echo 'Flake8 found Python 3 syntax errors above. See: https://docs.python.org/3/howto/pyporting.html'\n        exit $RETURN_CODE\n    fi\ndone\n\necho 'Flake8: second round to find any other stylistic issues...'\nfor i in \"${PYTHON_FILES[@]}\"\ndo\n    flake8 \"$i\" --ignore=E,W503,W504,W605 --max-line-length=127 --statistics\ndone\n"
  },
  {
    "path": "tools/github/runDummyTests.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\nmkdir $ROOTDIR/logs\necho \"<h1>$(date)</h1>\" >$ROOTDIR/logs/now.html\nexit 0\n"
  },
  {
    "path": "tools/github/runLeanSystemTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\n\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_LEAN_SYSTEM\"\nexport GRADLE_COVERAGE=true\n\n./setupPrereq.sh /ansible/files/runtimes-nodeonly.json\n\n./distDocker-lean.sh\n\n./setupLeanSystem.sh /ansible/files/runtimes-nodeonly.json\n\n./runTests.sh\n"
  },
  {
    "path": "tools/github/runMultiRuntimeTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\n\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_MULTI_RUNTIME\"\nexport GRADLE_COVERAGE=true\n\n./setupPrereq.sh\n\n./distDocker.sh\n\n./setupSystem.sh\n\n./runTests.sh\n"
  },
  {
    "path": "tools/github/runSchedulerTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_SCHEDULER\"\nexport GRADLE_COVERAGE=true\n\n./setupPrereq.sh /ansible/files/runtimes-nodeonly.json\n\n./distDocker.sh\n\n./setupSystem.sh /ansible/files/runtimes-nodeonly.json\n\n./runTests.sh\n"
  },
  {
    "path": "tools/github/runStandaloneTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_STANDALONE\"\nexport GRADLE_COVERAGE=true\n\ncd $ROOTDIR/ansible\n$ANSIBLE_CMD setup.yml\n$ANSIBLE_CMD properties.yml -e manifest_file=\"/ansible/files/runtimes-nodeonly.json\"\n$ANSIBLE_CMD downloadcli-github.yml\n\n# Install kubectl\ncurl -Lo ./kubectl https://storage.googleapis.com/kubernetes-release/release/v1.25.3/bin/linux/amd64/kubectl\nchmod +x kubectl\nsudo cp kubectl /usr/local/bin/kubectl\n\n# Install kind\ncurl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.17.0/kind-linux-amd64\nchmod +x kind\nsudo cp kind /usr/local/bin/kind\n\nkind create cluster --wait 5m\nexport KUBECONFIG=\"$(kind get kubeconfig-path)\"\nkubectl config set-context --current --namespace=default\n\n# This is required because it is timed out to pull the image during the test.\ndocker pull openwhisk/action-nodejs-v14:nightly\ndocker pull openwhisk/dockerskeleton:nightly\ndocker pull openwhisk/example:nightly\ndocker pull openwhisk/apigateway:0.11.0\n\ncd $ROOTDIR\nTERM=dumb ./gradlew :core:standalone:build \\\n  :core:monitoring:user-events:distDocker\n\ncd $ROOTDIR\nTERM=dumb ./gradlew :core:standalone:cleanTest \\\n  :core:standalone:test \\\n  :core:monitoring:user-events:reportTestScoverage\n\n# Run test in end as it publishes the coverage also\ncd $ROOTDIR/tools/travis\n./runTests.sh\n"
  },
  {
    "path": "tools/github/runSystemTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\n\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_SYSTEM\"\nexport GRADLE_COVERAGE=true\n\n./setupPrereq.sh /ansible/files/runtimes-nodeonly.json\n\n./distDocker.sh\n\n./setupSystem.sh /ansible/files/runtimes-nodeonly.json\n\n./runTests.sh\n"
  },
  {
    "path": "tools/github/runUnitTests.sh",
    "content": "#!/usr/bin/env bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\nexport TESTCONTAINERS_RYUK_DISABLED=\"true\"\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_ONLY_DB\"\n\n./setupPrereq.sh\n\ncat \"$ROOTDIR/tests/src/test/resources/application.conf\"\n\n./distDocker.sh\n\n./runTests.sh\n"
  },
  {
    "path": "tools/github/s3-upload.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# check variables\nfor i in LOG_BUCKET LOG_ACCESS_KEY_ID LOG_SECRET_ACCESS_KEY\ndo\n  if test -z \"${!i}\"\n  then echo \"Please set $i\" ; exit 1\n  fi\ndone\n\nif [[ -z \"$1\" ]] || [[ -z \"$2\" ]]\nthen echo \"usage: <source-dir> <target-path>\" ; exit 1\nfi\n\nFROM=\"$1\"\nTO=\"$2\"\n\nBROWSER=\"https://raw.githubusercontent.com/qoomon/aws-s3-bucket-browser/master/index.html\"\nBUCKET_URL=\"https://$LOG_BUCKET.s3.$LOG_REGION.amazonaws.com/\"\n\n# install rclone\nif ! which rclone\nthen curl https://rclone.org/install.sh | sudo bash\nfi\n\nRCLONE=\"rclone --config /dev/null \\\n  --s3-provider AWS \\\n  --s3-region $LOG_REGION \\\n  --s3-acl public-read \\\n  --s3-access-key-id  $LOG_ACCESS_KEY_ID \\\n  --s3-secret-access-key $LOG_SECRET_ACCESS_KEY\"\n\ncurl -s \"$BROWSER\" |\\\n  sed -e 's!bucketUrl: undefined!bucketUrl: \"'$BUCKET_URL'\"!' |\\\n  $RCLONE rcat \":s3:$LOG_BUCKET/index.html\"\n\n$RCLONE copyto \"$FROM\" \":s3:$LOG_BUCKET/$TO/\"\n"
  },
  {
    "path": "tools/github/scan.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\n# Build script for Travis-CI.\nSECONDS=0\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\nHOMEDIR=\"$SCRIPTDIR/../../../\"\nUTILDIR=\"$HOMEDIR/openwhisk-utilities/\"\n\ncd $ROOTDIR\n./tools/github/flake8.sh  # Check Python files for style and stop the build on syntax errors\n\n# clone the openwhisk utilities repo.\ncd $HOMEDIR\ngit clone https://github.com/apache/openwhisk-utilities.git\n\n# run the scancode util. against project source code starting at its root\ncd $UTILDIR\nscancode/scanCode.py --config scancode/ASF-Release.cfg $ROOTDIR\n\n# run scalafmt checks\ncd $ROOTDIR\nTERM=dumb ./gradlew checkScalafmtAll\n\n# lint tests to all be actually runnable\nMISSING_TESTS=$(grep -rL \"RunWith\" --include=\"*Tests.scala\" tests || true)\nif [ -n \"$MISSING_TESTS\" ]\nthen\n  echo \"The following tests are missing the 'RunWith' annotation\"\n  echo $MISSING_TESTS\n  exit 1\nfi\n\necho \"Time taken for ${0##*/} is $SECONDS secs\"\n"
  },
  {
    "path": "tools/github/setup.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#if [[ $TEST_SUITE =~ Dummy ]]\n#then echo skipping setup ; exit 0\n#fi\n\nset -e\n\n# retries a command for five times and exits with the non-zero exit if even after\n# the retries the command did not succeed.\nfunction retry() {\n  local exitcode=0\n  for i in {1..5};\n  do\n    exitcode=0\n    \"$@\" && break || exitcode=$? && echo \"$i. attempt failed. Will retry $((5-i)) more times!\" && sleep 1;\n  done\n  if [ $exitcode -ne 0 ]; then\n    exit $exitcode\n  fi\n}\n\n# lynx utility to show test results on the job run\nsudo apt-get -y install lynx\n\n# setup docker to listen in port 4243\nsudo systemctl stop docker\nsudo sed -i -e 's!/usr/bin/dockerd -H fd://!/usr/bin/dockerd -H tcp://0.0.0.0:4243 -H fd://!' /lib/systemd/system/docker.service\nsudo systemctl daemon-reload\nsudo systemctl start docker\n\n# compiles with eclipse temurin jdk 17\nJDK=https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.9%2B9/OpenJDK17U-jdk_x64_linux_hotspot_17.0.9_9.tar.gz\ncurl -sL $JDK | sudo tar xzvf - -C /usr/local\nJAVA=\"$(which java)\"\nsudo mv \"$JAVA\" \"$JAVA\".\"$(date +%s)\"\nsudo ln -sf /usr/local/jdk*/bin/java $JAVA\njava -version\n\n# Python\npython -m pip install --user couchdb\n\n# Ansible (warning you need jinja < 3.1 with this version)\npython -m pip install --user 'jinja2<3.1' ansible==2.8.18\n\n# Azure CosmosDB\npython -m pip install --user pydocumentdb\n\n# Support the revises log upload script\npython -m pip install --user humanize requests\n\n# Scan code before compiling the code\ntools/github/scan.sh\n\n# Preload alpine 3.5 to avoid issues with depending images\nretry docker pull alpine:3.5\n\n# exit if dummy test suite skipping the long compilation when debugging\nif [[ $TEST_SUITE =~ Dummy ]]\nthen echo skipping setup ; exit 0\nfi\n\n# Basic check that all code compiles and dependencies are downloaded correctly.\n# Compiling the tests will compile all components as well.\n#\n# Downloads the gradle wrapper, dependencies and tries to compile the code.\n# Retried 5 times in case there are network hiccups.\nTERM=dumb retry ./gradlew :tests:compileTestScala\n"
  },
  {
    "path": "tools/github/waitIfDebug.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nif ! test -e .ngrok.log\nthen exit 0\nfi\necho \"You have an hour to debug this build.\"\necho \"Do touch /tmp/continue to continue.\"\necho \"Do touch /tmp/abort to abort.\"\n\nEXIT=0\nfor i in $(seq 1 60)\ndo\n   if test -e /tmp/continue ; then EXIT=0 ; break ; fi\n   if test -e /tmp/abort ; then EXIT=1 ; break ; fi\n   echo \"$i/60 still waiting...\"\n   sleep 60\ndone\n\nkillall ngrok\nrm -f .ngrok.log /tmp/continue /tmp/abort\nexit $EXIT\n"
  },
  {
    "path": "tools/github/writeOnSlack.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nif test -z \"$SLACK_WEBHOOK\"\nthen echo \"Please create an incoming webhook for slack and set SLACK_WEBHOOK\"\n     exit 0\nfi\n\necho -n '{\"text\":' >/tmp/msg$$\necho -n \"$@\" | jq -Rsa . >>/tmp/msg$$\necho -n '}' >>/tmp/msg$$\n\ncurl -X POST -H 'Content-type: application/json' --data \"@/tmp/msg$$\" \"$SLACK_WEBHOOK\"\n"
  },
  {
    "path": "tools/jenkins/apache/dockerhub.groovy",
    "content": "#!groovy\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nnode('ubuntu') {\n  sh \"env\"\n  sh \"docker version\"\n  sh \"docker info\"\n\n  checkout scm\n\n  stage(\"Build and Deploy to DockerHub\") {\n    def JAVA_JDK_8=tool name: 'jdk_1.8_latest', type: 'hudson.model.JDK'\n    withEnv([\"Path+JDK=$JAVA_JDK_8/bin\",\"JAVA_HOME=$JAVA_JDK_8\"]) {\n      sh \"mkdir $WORKSPACE/local-docker-cfg\"\n      withCredentials([usernamePassword(credentialsId: 'openwhisk_dockerhub', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USER')]) {\n          sh 'HOME=\"$WORKSPACE/local-docker-cfg\" docker login -u ${DOCKER_USER} -p ${DOCKER_PASSWORD}'\n      }\n      sh \"docker run -it --rm --userns=host --privileged tonistiigi/binfmt --install all\"\n      sh \"docker buildx create --name owbuilder\"\n      sh \"docker buildx use owbuilder\"\n      def PUSH_CMD = \"./gradlew :core:controller:distDocker :core:scheduler:distDocker :core:invoker:distDocker :core:standalone:distDocker :core:monitoring:user-events:distDocker :tools:ow-utils:distDocker :core:cosmos:cache-invalidator:distDocker -PdockerRegistry=docker.io -PdockerImagePrefix=openwhisk -PdockerMultiArchBuild=true\"\n      def gitCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim()\n      def shortCommit = gitCommit.take(7)\n      sh \"./gradlew clean\"\n      sh \"HOME=\\\"$WORKSPACE/local-docker-cfg\\\" ${PUSH_CMD} -PdockerImageTag=nightly\"\n      sh \"HOME=\\\"$WORKSPACE/local-docker-cfg\\\" ${PUSH_CMD} -PdockerImageTag=${shortCommit}\"\n    }\n  }\n\n  stage(\"Clean\") {\n    sh \"docker buildx rm owbuilder\"\n    sh \"docker images\"\n    sh 'docker rmi -f $(docker images -f \"reference=openwhisk/*\" -q) || true'\n    sh \"docker images\"\n    sh \"docker logout\"\n    sh \"rm -rf $WORKSPACE/local-docker-cfg\"\n  }\n\n  stage(\"Notify\") {\n    withCredentials([string(credentialsId: 'openwhisk_slack_token', variable: 'OPENWHISK_SLACK_TOKEN')]) {\n      sh \"curl -X POST --data-urlencode 'payload={\\\"channel\\\": \\\"#dev\\\", \\\"username\\\": \\\"whiskbot\\\", \\\"text\\\": \\\"OpenWhisk Docker Images build and posted to https://hub.docker.com/u/openwhisk by Jenkins job ${BUILD_URL}\\\", \\\"icon_emoji\\\": \\\":openwhisk:\\\"}' https://hooks.slack.com/services/${OPENWHISK_SLACK_TOKEN}\"\n    }\n\n  }\n}\n"
  },
  {
    "path": "tools/macos/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Setting up OpenWhisk with Docker for Mac\n\nOpenWhisk can run on a Mac host with [Docker for Mac](https://docs.docker.com/docker-for-mac/).\nIf you prefer to use Docker-machine, you can follow instructions in [docker-machine/README.md](docker-machine/README.md)\n\n# Prerequisites\n\nThe following are required to build and deploy OpenWhisk from a Mac host:\n\n- [Docker 18.06.3+](https://docs.docker.com/docker-for-mac/install/)\n- [Open JDK 11](https://adoptopenjdk.net/releases.html#x64_mac)\n- [Scala 2.12](http://scala-lang.org/download/)\n- [Ansible 4.1.0](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html)\n\n**Tips:**\n 1. Versions of Docker and Ansible are lower than the latest released versions, the versions used in OpenWhisk are pinned to have stability during continuous integration and deployment.<br>\n 2. It is required to install Docker >= 18.06.2 because of this [CVE](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5736)\n\n\n[Homebrew](http://brew.sh/) is an easy way to install all of these and prepare your Mac to build and deploy OpenWhisk. The following shell command is provided for your convenience to install `brew` with [Cask](https://github.com/caskroom/homebrew-cask) and bootstraps these to complete the setup. Copy the entire section below and paste it into your terminal to run it.\n\n```bash\necho '\n# install homebrew\n/usr/bin/ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"\n# install cask\nbrew tap homebrew/cask\n# install for AdoptOpenJDK (java11)\nbrew tap AdoptOpenJDK/openjdk\n# install java 11\nbrew install --cask adoptopenjdk11\n# install scala\nbrew install scala\n# install gnu tar\nbrew install gnu-tar\n# install pip\nsudo easy_install pip\n# install script prerequisites\npip install docker==5.0.0 ansible==4.1.0 jinja2==3.0.1 couchdb==1.2 httplib2==0.19.1 requests==2.25.1 six==1.16.0\n```\n\nMake sure you correctly configure the environment variable $JAVA_HOME.\n\n# Build\n```bash\ncd /your/path/to/openwhisk\n./gradlew distDocker\n```\n**Tip** Using `gradlew` handles the installation of the correct version of Gradle to use.\n\n# Deploy\nFollow instructions in [ansible/README.md](../../ansible/README.md)\n\n### Configure the CLI\n\n#### Using brew\n\n```bash\nbrew install wsk\nwsk property set --apihost https://localhost\nwsk property set --auth `cat ansible/files/auth.guest`\n```\n#### Other methods\nFor more instructions see [Configure CLI doc](../../docs/cli.md).\n\n### Use the wsk CLI\n```bash\nwsk action invoke /whisk.system/utils/echo -p message hello --result\n{\n    \"message\": \"hello\"\n}\n```\n\n# Develop\n\n## Running unit tests\n\n> Unit tests require [Ansible setup](../../ansible/README.md) at the moment.\n\nBellow are the ansible commands required to prepare your machine:\n\n```bash\ncd ./ansible\n\nansible-playbook setup.yml -e mode=HA\nansible-playbook couchdb.yml\nansible-playbook initdb.yml\nansible-playbook wipe.yml\nansible-playbook downloadcli-github.yml\n\nansible-playbook properties.yml\n```\n\nTo run the unit tests execute the command bellow from the project's root folder:\n```bash\n# go back to project's root folder\ncd ../\n./gradlew -PtestSetName=\"REQUIRE_ONLY_DB\" :tests:testCoverageLean\n```\n"
  },
  {
    "path": "tools/macos/docker-machine/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Setting up OpenWhisk with Docker-machine\n\nOpenWhisk can run on a Mac using a virtual machine in which Docker daemon is running.\nYou will make provision of a virtual machine with Docker-machine and communicate with them via Docker remote API.\n\n# Prerequisites\n\nThe following are required to build and deploy OpenWhisk from a Mac host:\n\n- [Oracle VM VirtualBox](https://www.virtualbox.org/wiki/Downloads)\n- [Docker 18.06.3](https://docs.docker.com/machine/install-machine/) (including `docker-machine`)\n- [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/index.html)\n- [Scala 2.11](http://scala-lang.org/download/)\n- [Ansible 2.5.2](http://docs.ansible.com/ansible/intro_installation.html)\n\n**Tips:**\n1. Versions of Docker and Ansible are lower than the latest released versions, the versions used in OpenWhisk are pinned to have stability during continues integration and deployment.\n2. It is required to install Docker >= 18.06.2 because of this [CVE](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5736)\n\n[Homebrew](http://brew.sh/) is an easy way to install all of these and prepare your Mac to build and deploy OpenWhisk. The following shell command is provided for your convenience to install `brew` with [Cask](https://github.com/caskroom/homebrew-cask) and bootstraps these to complete the setup. Copy the entire section below and paste it into your terminal to run it.\n\n```bash\necho '\n# install homebrew\n/usr/bin/ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"\n# install cask\nbrew tap homebrew/cask\n# install virtualbox\nbrew install --cask virtualbox\n# install docker 1.12.0\nbrew install https://raw.githubusercontent.com/Homebrew/homebrew-core/33301827c3d770bfd49f0e50d84e0b125b06b0b7/Formula/docker.rb\n# install docker-machine\nbrew install docker-machine\n# install java 8\nbrew install --cask java\n# install scala\nbrew install scala\n# install gnu tar\nbrew install gnu-tar\n# install pip\nsudo easy_install pip\n# install script prerequisites\nsudo -H pip install docker==2.2.1 ansible==2.5.2 jinja2==2.9.6 couchdb==1.1 httplib2==0.9.2 requests==2.10.0' | bash\n```\n\n# Create and configure Docker machine\n\nIt is recommended that you create a virtual machine `whisk` with at least 4GB of RAM.\n\n```bash\ndocker-machine create -d virtualbox \\\n   --virtualbox-memory 4096 \\\n   --virtualbox-boot2docker-url=https://github.com/boot2docker/boot2docker/releases/download/v1.12.0/boot2docker.iso \\\n    whisk # the name of your docker machine\n```\nNote that by default the third octet chosen by docker-machine will be 99. If you've multiple docker machines\nand want to ensure that the ip of the created whisk vm isn't dependent of the machine start order then provide `--virtualbox-hostonly-cidr \"192.168.<third_octet>.1/24\"` in order to create a dedicated virtual network interface.\n\nThe Docker virtual machine requires some tweaking to work from the Mac host with OpenWhisk.\nThe following [script](./tweak-dockermachine.sh) will disable TLS, add port forwarding\nwithin the VM and routes `172.17.x.x` from the Mac host to the Docker virtual machine.\nEnter your sudo Mac password when prompted.\n\n```bash\ncd /your/path/to/openwhisk\n./tools/macos/docker-machine/tweak-dockermachine.sh\n```\n\nThe final output of the script should resemble the following two lines.\n```bash\nRun the following:\nexport DOCKER_HOST=\"tcp://192.168.99.100:4243\" # your Docker virtual machine IP may vary\n```\n\nThe Docker host reported by `docker-machine ip whisk` will give you the IP address.\nCurrently, the system requires that you use port `4243` to communicate with the Docker host\nfrom OpenWhisk.\n\nIgnore errors messages from `docker-machine ls` for the `whisk` virtual machine, this is due\nto the configuration of the port `4243` vs. `2376`\n```bash\nNAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER    ERRORS\nwhisk     -        virtualbox   Running   tcp://192.168.99.100:2376           Unknown   Unable to query docker version: Cannot connect to the docker engine endpoint\n```\n\nTo verify that docker is configure properly with `docker-machine` run `docker ps`, you should not see any errors. Here is an example output:\n```bash\nCONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES\n\n```\n\nYou may find it convenient to set these environment variables in your bash profile (e.g., `~/.bash_profile` or `~/.profile`).\n```bash\nexport OPENWHISK_HOME=/your/path/to/openwhisk\nexport DOCKER_HOST=tcp://$(docker-machine ip whisk):4243\n```\n\nThe tweaks to the Docker machine persist across reboots.\nHowever one of the tweaks is applied on the Mac host and must be applied\nagain if you reboot your Mac. Without it, some tests which require direct\ncommunication with Docker containers will fail. To run just the Mac host tweaks,\nrun the following [script](./tweak-dockerhost.sh). Enter your sudo Mac password when prompted.\n```bash\ncd /your/path/to/openwhisk\n./tools/macos/docker-machine/tweak-dockerhost.sh\n```\n\n# Build\n```bash\ncd /your/path/to/openwhisk\n./gradlew distDocker\n```\n**Tip** Using `gradlew` handles the installation of the correct version of Gradle to use.\n\n# Deploy\n\n```bash\nbrew install python\npip install ansible==2.5.0\npip install jinja2==2.9.6\n\ncd ansible\nansible-playbook -i environments/docker-machine setup.yml [-e docker_machine_name=whisk]\n```\n\n**Hint:** If you omit the optional `-e docker_machine_name` parameter, it will default to \"whisk\".\nIf your docker-machine VM has a different name you may pass it via the `-e docker_machine_name` parameter.\n\nAfter this there should be a `hosts` file in the `ansible/environments/docker-machine` directory.\n\nTo verify the hosts file you can do a quick ping to the docker machine:\n\n```bash\ncd ansible\nansible all -i environments/docker-machine -m ping\n```\n\nShould result in something like:\n\n```bash\nansible | SUCCESS => {\n    \"changed\": false,\n    \"ping\": \"pong\"\n}\n192.168.99.100 | SUCCESS => {\n    \"changed\": false,\n    \"ping\": \"pong\"\n}\n```\n\nFollow remaining instructions from [Using Ansible](../../../ansible/README.md#using-ansible) section in [ansible/README.md](../../../ansible/README.md)\n\n### Configure the CLI\nFollow instructions in [Configure CLI](../../../docs/cli.md)\n\n### Use the wsk CLI\n```bash\nbin/wsk action invoke /whisk.system/utils/echo -p message hello --result\n{\n    \"message\": \"hello\"\n}\n```\n"
  },
  {
    "path": "tools/macos/docker-machine/tweak-dockerhost.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# this should run to permit some unit tests to run which directly communicate\n# with containers; if you notice the route forwarding is immediately deleted\n# after this script runs (you can check by running 'route monitor') then\n# shut down your wireless and disconnect all networking cables, wait a few secs\n# and try again; you should see the route stick and now you can re-enable wifi etc.\n\n# Set this to the name of the docker-machine VM.\nMACHINE_NAME=whisk\nMACHINE_VM_IP=$(docker-machine ip $MACHINE_NAME)\n\nsudo route -n delete 172.17.0.0/16\nsudo route -n -q add 172.17.0.0/16 $MACHINE_VM_IP\nnetstat -nr |grep 172\\.17\n"
  },
  {
    "path": "tools/macos/docker-machine/tweak-dockermachine.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Setup docker-machine VM and OSX host.\n# Assumes there exists a docker-machine called whisk; to create one:\n# > docker-machine create whisk --driver virtualbox\n\n# Set this to the name of the docker-machine VM.\nMACHINE_NAME=${1:-whisk}\n\n\n# Disable TLS.\ndocker-machine ssh $MACHINE_NAME \"echo DOCKER_TLS=no |sudo tee -a /var/lib/boot2docker/profile > /dev/null\"\ndocker-machine ssh $MACHINE_NAME \"echo DOCKER_HOST=\\'-H tcp://0.0.0.0:4243\\' |sudo tee -a /var/lib/boot2docker/profile > /dev/null\"\ndocker-machine ssh $MACHINE_NAME \"echo EXTRA_ARGS=\\'--userns-remap=default\\' |sudo tee -a /var/lib/boot2docker/profile > /dev/null\"\ndocker-machine ssh $MACHINE_NAME \"echo '#!/bin/sh\n/sbin/syslogd\nSTATUS=\\$(curl -s -o /dev/null -w '%{http_code}' repo.tinycorelinux.net)\nif [ \\$STATUS -ne 200 ]; then\n    sudo echo \\\"http://ftp.nluug.nl/os/Linux/distr/tinycorelinux/\\\" > /opt/tcemirror\nfi\nsu - docker -c \\\"tce-load -wi python\\\"\nif ! [ -x /usr/local/bin/pip ]; then\n    curl -k https://bootstrap.pypa.io/get-pip.py | sudo python\n    sudo pip install \\\"docker==2.2.1\\\"\n    sudo pip install \\\"httplib2==0.9.2\\\"\nfi\n' | sudo tee /var/lib/boot2docker/bootsync.sh > /dev/null\"\ndocker-machine ssh $MACHINE_NAME \"sudo chmod +x /var/lib/boot2docker/bootsync.sh\"\n\n\n# Install prereqs\ndocker-machine ssh $MACHINE_NAME \"sudo /var/lib/boot2docker/bootsync.sh > /dev/null\"\n\n# Restart docker daemon.\ndocker-machine ssh $MACHINE_NAME \"sudo /etc/init.d/docker restart\"\n\n# Set routes on host.\n# If you notice the route forwarding is immediately deleted after this script\n# runs (you can check by running 'route monitor') then shut down your networking\n# (turn off wifi and disconnect all networking cables), wait a few secs and try\n# again; you should see the route stick and now you can reenable networking.\necho \"Adding route forwarding on your host machine, enter sudo password if/when prompted\"\nMACHINE_VM_IP=$(docker-machine ip $MACHINE_NAME)\nsudo route -n -q delete 172.17.0.0/16\nsudo route -n -q add 172.17.0.0/16 $MACHINE_VM_IP\nnetstat -nr |grep 172\\.17\n\n# Env variables to set.\n# Note, the user CAN NOT do eval $(docker-machine env whisk) because of a bug in docker-machine that assumes TLS even if not enabled\necho Save the following to your shell profile:\necho \"  \" export DOCKER_HOST=\"tcp://$MACHINE_VM_IP:4243\"\n"
  },
  {
    "path": "tools/ow-utils/Dockerfile",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM adoptopenjdk/openjdk8:jdk8u262-b10\n\nENV DOCKER_VERSION=1.12.0\nENV KUBECTL_VERSION=v1.16.3\nENV WHISK_CLI_VERSION=latest\nENV WHISKDEPLOY_CLI_VERSION=latest\n\nRUN apt-get update && apt-get install -y \\\n  git \\\n  jq \\\n  libffi-dev \\\n  nodejs \\\n  npm \\\n  python \\\n  python-pip \\\n  wget \\\n  zip \\\n  locales \\\n&& rm -rf /var/lib/apt/lists/*\n\n# update npm\nRUN npm install -g n && n stable && hash -r\n\nRUN locale-gen en_US.UTF-8\nENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8'\n\n# Python packages\nRUN pip install --upgrade pip\nRUN pip install --upgrade setuptools\nRUN pip install cryptography==2.5 && \\\n    pip install ansible==2.5.2 && \\\n    pip install jinja2==2.9.6 && \\\n    pip install docker\n\n# Install docker client\nRUN wget --no-verbose https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION}.tgz && \\\n  tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/docker && \\\n  rm -f docker-${DOCKER_VERSION}.tgz && \\\n  chmod +x /usr/bin/docker\n\n# Install kubectl in /usr/local/bin\nRUN curl -Lo ./kubectl https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl && chmod +x kubectl && mv kubectl /usr/local/bin/kubectl\n\n# Install `wsk` cli in /usr/local/bin\nRUN wget -q https://github.com/apache/openwhisk-cli/releases/download/$WHISK_CLI_VERSION/OpenWhisk_CLI-$WHISK_CLI_VERSION-linux-amd64.tgz && \\\n  tar xzf OpenWhisk_CLI-$WHISK_CLI_VERSION-linux-amd64.tgz -C /usr/local/bin wsk && \\\n  rm OpenWhisk_CLI-$WHISK_CLI_VERSION-linux-amd64.tgz\n\n# Install wskadmin in /bin\nCOPY wskutil.py /bin\nCOPY wskprop.py /bin\nCOPY wskadmin /bin\n\n# Setup tools/data for certificate generation (used by openwhisk-deploy-kube)\nRUN mkdir /cert-gen\nCOPY openwhisk-server-key.pem /cert-gen\nCOPY genssl.sh /usr/local/bin/\n"
  },
  {
    "path": "tools/ow-utils/Dockerfile.arm",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nFROM arm64v8/eclipse-temurin:8u422-b05-jdk-noble\n\nENV DOCKER_VERSION=1.12.0\nENV KUBECTL_VERSION=v1.16.3\nENV WHISK_CLI_VERSION=latest\nENV WHISKDEPLOY_CLI_VERSION=latest\n\nRUN apt-get update && apt-get install -y \\\n  git \\\n  jq \\\n  libffi-dev \\\n  nodejs \\\n  npm \\\n  python-is-python3 \\\n  python3-pip \\\n  python3-venv \\\n  wget \\\n  zip \\\n  locales \\\n&& rm -rf /var/lib/apt/lists/*\n\n# update npm\nRUN npm install -g n && n stable && hash -r\n\nRUN locale-gen en_US.UTF-8\nENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8'\n\nWORKDIR /root\n\nRUN python -m venv .venv\nENV PATH=\"/root/.venv/bin:$PATH\"\n\n# Python packages\nRUN pip3 install --upgrade pip\nRUN pip3 install --upgrade setuptools\nRUN pip3 install cryptography && \\\n    pip3 install ansible==2.5.2 && \\\n    pip3 install jinja2==2.9.6 && \\\n    pip3 install docker\n\n# Install docker client\nRUN wget --no-verbose https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION}.tgz && \\\n  tar --strip-components 1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/bin docker/docker && \\\n  rm -f docker-${DOCKER_VERSION}.tgz && \\\n  chmod +x /usr/bin/docker\n\n# Install kubectl in /usr/local/bin\nRUN curl -Lo ./kubectl https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl && chmod +x kubectl && mv kubectl /usr/local/bin/kubectl\n\n# Install `wsk` cli in /usr/local/bin\nRUN wget -q https://github.com/apache/openwhisk-cli/releases/download/$WHISK_CLI_VERSION/OpenWhisk_CLI-$WHISK_CLI_VERSION-linux-amd64.tgz && \\\n  tar xzf OpenWhisk_CLI-$WHISK_CLI_VERSION-linux-amd64.tgz -C /usr/local/bin wsk && \\\n  rm OpenWhisk_CLI-$WHISK_CLI_VERSION-linux-amd64.tgz\n\n# Install wskadmin in /bin\nCOPY wskutil.py /bin\nCOPY wskprop.py /bin\nCOPY wskadmin /bin\n\n# Setup tools/data for certificate generation (used by openwhisk-deploy-kube)\nRUN mkdir /cert-gen\nCOPY openwhisk-server-key.pem /cert-gen\nCOPY genssl.sh /usr/local/bin/\n"
  },
  {
    "path": "tools/ow-utils/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\nUtility image for executing tasks\n================\n\nThis `ow-utils` image can be used to execute various utility tasks\nfor OpenWhisk using most of the tools that are used in the project.\nIt includes a JDK8, python/ansible, nodejs, and standard packages\nsuch as bash, git, wget, curl, and docker.\n\nIt also includes the `wsk` and `wskadmin` CLIs.\n"
  },
  {
    "path": "tools/ow-utils/build.gradle",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\next.dockerImageName = 'ow-utils'\nif(System.getProperty(\"os.arch\").toLowerCase(Locale.ENGLISH).startsWith(\"aarch64\")) {\n    ext.dockerDockerfileSuffix = \".arm\"\n}\napply from: '../../gradle/docker.gradle'\n\ndistDocker.dependsOn 'copyWskadmin', 'copyGenssl'\ndistDocker.finalizedBy('cleanup')\n\ntask copyWskadmin(type: Copy) {\n    from '../admin/wskadmin', '../admin/wskutil.py', '../admin/wskprop.py'\n    into '.'\n}\n\ntask copyGenssl(type: Copy) {\n    from '../../ansible/files/genssl.sh', '../../ansible/roles/nginx/files/openwhisk-server-key.pem'\n    into '.'\n}\n\ntask cleanup(type: Delete) {\n    delete 'wskadmin'\n    delete 'wskprop.py'\n    delete 'wskutil.py'\n    delete 'genssl.sh'\n    delete 'openwhisk-server-key.pem'\n}\n\n"
  },
  {
    "path": "tools/owperf/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n# :electric_plug: owperf - a performance test tool for Apache OpenWhisk\n\n## General Info\nThis test tool benchmarks an OpenWhisk deployment for (warm) latency and throughput, with several new capabilities:\n1. Measure performance of rules (trigger-to-action) in addition to actions\n1. Deeper profiling without instrumentation (e.g., Kamino) by leveraging the activation records in addition to the client's timing data. This avoids special setups, and can help gain performance insights on third-party deployments of OpenWhisk.\n1. New tunables that can affect performance:\n   1. Parameter size - controls the size of the parameter passed to the action or event\n   1. Actions per iteration (a.k.a. _ratio_) - controls how many rules are associated with a trigger [for rules] or how many actions are asynchronously invoked (burst size) at each iteration of a test worker [for actions].\n1. \"Master apart\" mode - Allow the master client to perform latency measurements while the worker clients stress OpenWhisk using a specific invocation pattern in the background. Useful for measuring latency under load, and for comparing latencies of rules and actions under load.\nThe tool is written in node.js, using mainly the modules of OpenWhisk client, cluster for concurrency, and commander for CLI processing.\n\n### Operation\nThe general operation of a test is simple:\n1. **Setup**: the tool creates the test action, test trigger, and a number of rules that matches the ratio tunable above.\n1. **Test**: the tool fires up a specified number of concurrent clients - a master and workers.\n   1. Each client wakes up once every _delta_ msec (iteration) and invokes the specified activity: either the trigger (for rule testing) or multiple concurrent actions - matching the ratio tunable. Action invocations can be blocking.\n   1. After each client has completed a number of initial iterations (warmup), measurement begins, controlled by the master client, for either a specified number of iterations or specified time.\n   1. At the end of the measurement, each client retrieves the activation records of its triggers and/or actions, and generates summary data that is sent to the master, which generates and prints the final results.\n1. **Teardown**: clean up the OpenWhisk assets created during setup\n\nFinal results are written to the standard output stream (so can be redirected to a file) as a single highly-detailed CSV record containing all the input settings and the output measurements (see below). There is additional control information that is written to the standard error stream and can be silenced in CLI. The control information also contains the CSV header, so it can be copied into a spreadsheet if needed.\n\nIt is possible to invoke the tool in \"Master apart\" mode, where the master client is invoking a different activity than the workers, and at possibly a different (very likely, much slower) rate. In this mode, latency statistics are computed based solely on the master's data, since the worker's activity is used only as background to stress the OpenWhisk deployment. So one experiment can have the master client invoke rules and another one can have the master client invoke actions, while in both experiments the worker clients perform the same background activity.\n\nThe tool is highly customizable via CLI options. All the independent test variables are controlled via CLI. This includes number of workers, invocation pattern, OW client configuration, test action sleep time, etc.\n\nTest setup and teardown can be independently skipped via CLI, and/or directly invoked from the external setup script (```setup.sh```), so that setup can be shared between multiple tests. More advanced users can replace the test action with a custom action in the setup script to benchmark action invocation or event-response throughput and latency of specific applications.\n\n**Clock skew**: OpenWhisk is a distributed system, which means that clock skew is expected between the client machine computing invocation timestamps and the controllers or invokers that generate the timestamps in the activation records. However, this tool assumes that clock skew is bound at few msec range, due to having all machines clocks synchronized, typically using NTP. At such a scale, clock skew is quite small compared to the measured time periods. Some of the time periods are measured using the same clock (see below) and are therefore oblivious to clock skew issues.\n\n## Initial Setup\nThe tool requires very little setup. You need to have node.js (v8+) and the wsk CLI client installed (on $PATH). Before the first run, execute ```npm install``` in the tool folder to install the dependencies.\n**Throttling**: By default, OW performance is throttled according to some [limits](https://github.com/apache/openwhisk/blob/master/docs/reference.md#system-limits), such as maximum number of concurrent requests, or maximum invocations per minute. If your benchmark stresses OpenWhisk beyond the limit value, you might want to relax those limits. If it's an OpenWhisk deployment that you control, you can set the limits to 999999, thereby effectively cancelling the limits. If it's a third-party service, you may want to consult the service documentation and/or support to see what limits can be relaxed and by how much.\n\n## Usage\nTo use the tool, run ```./owperf.sh <options>``` to perform a test. To see all the available options and defaults run ```./owperf.sh -h```.\n\nThe default for ratio is 1. If using a different ratio, be sure to specify the same ratio value for all steps.\n\nFor example, let's perform a test of rule performance with 3 clients, using the default delta of 200 msec, for 100 iterations (counted at the master client, excluding the warmup), ratio of 4. Each client performs 5 iterations per second, each iteration firing a trigger that invokes 4 rules, yielding a total of 3x5x4=60 rule invocations per second. The command to run this test: ```./owperf.sh -a rule -w 3 -i 100 -r 4```\n\n## Measurements\nAs explained above, the owperf tool collects both latency and throughput data at each experiment.\n\n### Latency\nThe following time-stamps are collected for each invocation, of either action, or rule (containing an action):\n* **BI** (Before Invocation) - taken by a client immediately before invoking - either the trigger fire (for rules), or an action invocation.\n* **TS** (Trigger Start) - taken from the activation record of the trigger linked to the rules, so applies only to rule tests. All actions invoked by the rules of the same trigger have the same TS value.\n* **AS** (Action Start) - taken from the activation record of the action.\n* **AE** (Action End) - taken from the activation record of the action.\n* **AI** (After Invocation) - taken by the client immediately after the invocation, for blocking action invocation tests only.\n\nBased on these timestamps, the following measurements are taken:\n* **OEA** (Overhead of Entering Action) - OpenWhisk processing overhead from sending the action invocation or trigger fire to the beginning of the action execution. OEA = AS-BI\n* **D** - the duration of the test action - as reported by the action itself in the return value.\n* **AD** - Action Duration - as measured by OpenWhisk invoker. AD = AE - AS. Always expect that AD >= D.\n* **OER** (Overhead of Executing Request) - OpenWhisk processing overhead from sending the action invocation or trigger fire to the completion of the action execution in the OpenWhisk Invoker. OER = AE-BI-D\n* **TA** (Trigger to Answer) - the processing time from the start of the trigger process to the start of the action (rule tests only). TA = AS-TS\n* **ORA** (Overhead of Returning from Action) - time from action end till being received by the client (blocking action tests only). ORA = AI - AE\n* **RTT** (Round Trip Time) - time at the client from action invocation till reply received (blocking action tests only). RTT = AI - BI\n* **ORTT** (Overhead of RTT) - RTT at the client excluding the net action computation time. ORTT = RTT - D\n\nFor each measurement, the tool computes average (_avg_), standard deviation (_std_), and extremes (_min_ and _max_).\n\nThe following chart depicts the relationship between the various measurements and the action invocation and rule invocation flows.\n\n![](owperf_data.png)\n\n### Throughput\nThroughput is measured w.r.t. several different counters. During post-processing of an experiment, each counter value is divided by the measurement time period to compute a respective throughput.\n* **Attempts** - number of invocation attempts performed inside the time frame (according to their BI). This is the \"arrival rate\" of invocations, should be close to _clients * ratio / delta_ .\n* **Requests** - number of requests sent to OpenWhisk inside the time frame. Each action invocation is one request, and each trigger fire is also one request (so a client invoking rules at ratio _k_ generates _k+1_ requests).\n* **Activations** - number of completed activations inside the time frame, counting both trigger activations (based on TS), and action activations (based on AS and AE).\n* **Invocations** - number of successful invocations of complete rules or actions (depending on the activity). This is the \"service rate\" of invocations (assuming errors happen only because OW is overloaded).\n\nFor each counter, the tool reports the total counter value (_abs_), total throughput per second (_tp_), throughput of the worker clients without the master (_tpw_) and the master's percentage of throughput relative to workers (_tpd_). The last two values are important mostly for master apart mode.\n\nAside from that, the tool also counts **errors**. Failed invocations - of actions, of triggers, or of actions from triggers (via rules) are counted each as an error. The tool reports both absolute error count (_abs_) and percent out of requests (_percent_).\n\n## Acknowledgements\nThe owperf tool has been developed by IBM Research as part of the [CLASS](https://class-project.eu/) EU project. CLASS aims to integrate OpenWhisk as a foundation for latency-sensitive polyglot event-driven big-data analytics platform running on a compute continuum from the cloud to the edge. CLASS is funded by the European Union's Horizon 2020 Programme grant agreement No. 780622.\n\n"
  },
  {
    "path": "tools/owperf/owperf.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\n/**\n * This is a test tool for measuring the performance of OpenWhisk actions and rules.\n * The full documentation of the tool is available in README.md .\n */\n\nconst fs = require('fs');\nconst ini = require('ini');\nconst cluster = require('cluster');\nconst openwhisk = require('openwhisk');\nconst program = require('commander');\nconst exec = require('node-exec-promise').exec;\n\nconst ACTION = \"action\";\nconst RULE = \"rule\";\nconst RESULT = \"result\";\nconst ACTIVATION = \"activation\";\nconst NONE = \"none\";\n\nfunction parseIntDef(strval, defval) {\n    return parseInt(strval);\n}\n\nprogram\n    .description('Latency and throughput measurement of OpenWhisk actions and rules')\n    .version('0.0.1')\n    .option('-a, --activity <action/rule>', \"Activity to measure\", /^(action|rule)$/i, \"action\")\n    .option('-b, --blocking <result/activation/none>', \"For actions, wait until result or activation, or don't wait\", /^(result|activation|none)$/i, \"none\")\n    .option('-d, --delta <msec>', \"Time diff between consequent invocations of the same worker, in msec\", parseIntDef, 200)\n    .option('-i, --iterations <count>', \"Number of measurement iterations\", parseInt)\n    .option('-p, --period <msec>', \"Period of measurement in msec\", parseInt)\n    .option('-r, --ratio <count>', \"How many actions per iteration (or rules per trigger)\", parseIntDef, 1)\n    .option('-s, --parameter_size <size>', \"Size of string parameter passed to trigger or actions\", parseIntDef, 1000)\n    .option('-w, --workers <count>', \"Total number of concurrent workers incl. master\", parseIntDef, 1)\n    .option('-A, --master_activity <action/rule>', \"Set master activity apart from other workerss\", /^(action|rule)$/i)\n    .option('-B, --master_blocking <result/activation/none>', \"Set master blocking apart from other workers\", /^(result|activation|none)$/i)\n    .option('-D, --master_delta <msec>', \"Set master delta apart from other workers\", parseInt)\n    .option('-u, --warmup <count>', \"How many invocations to perform at each worker as warmup\", parseIntDef, 5)\n    .option('-l, --delay <msec>', \"How many msec to delay at each action\", parseIntDef, 50)\n    .option('-P --pp_delay <msec>', \"Wait for remaining activations to finalize before post-processing\", parseIntDef, 60000)\n    .option('-G --burst_timing', \"For actions, use the same invocation timing (BI) for all actions in a burst\")\n    .option('-S --no-setup', \"Skip test setup (so use previous setup)\")\n    .option('-T --no-teardown', \"Skip test teardown (to allow setup reuse)\")\n    .option('-f --config_file <filepath>', \"Specify a wskprops configuration file to use\", `${process.env.HOME}/.wskprops`)\n    .option('-q, --quiet', \"Suppress progress information on stderr\");\n\nprogram.parse(process.argv);\n\nvar testRecord = {input: {}, output: {}};    // holds the final test data\n\nfor (var opt in program.opts())\n    if (typeof program[opt] != 'function')\n        testRecord.input[opt] = program[opt];\n\n// If neither period nor iterations are set, then period is set by default to 1000 msec\nif (!testRecord.input.iterations && !testRecord.input.period)\n    testRecord.input.period = 1000;\n\n// If either master_activity, master_blocking or master_delta are set, then test is in 'master apart' mode\ntestRecord.input.master_apart = ((testRecord.input.master_activity || testRecord.input.master_blocking || testRecord.input.master_delta) && true);\n\nmLog(\"Parameter Configuration:\");\nfor (var opt in testRecord.input)\n    mLog(`${opt} = ${testRecord.input[opt]}`);\nmLog(\"-----\\n\");\n\nmLog(\"Generating invocation parameters\");\nvar inputMessage = \"A\".repeat(testRecord.input.parameter_size);\nvar params = {sleep: testRecord.input.delay, message: inputMessage};\n\nmLog(\"Loading wskprops\");\nconst config = ini.parse(fs.readFileSync(testRecord.input.config_file, \"utf-8\"));\nmLog(\"APIHOST = \" + config.APIHOST);\nmLog(\"AUTH = \" + config.AUTH);\nmLog(\"-----\\n\");\nconst wskParams = `--apihost ${config.APIHOST} --auth ${config.AUTH} -i`;    // to be used when invoking setup and teardown via external wsk\n\n// openwhisk client used for invocations\nconst ow = openwhisk({apihost: config.APIHOST, api_key: config.AUTH, ignore_certs: true});\n\n// counters for throughput computation (all)\nconst tpCounters = {attempts: 0, invocations: 0, activations: 0, requests: 0, errors: 0};\n\n// counters for latency computation\nconst latCounters = {\n                    ta: {sum: undefined, sumSqr: undefined, min: undefined, max: undefined},\n                    oea: {sum: undefined, sumSqr: undefined, min: undefined, max: undefined},\n                    oer: {sum: undefined, sumSqr: undefined, min: undefined, max: undefined},\n                    d: {sum: undefined, sumSqr: undefined, min: undefined, max: undefined},\n                    ad: {sum: undefined, sumSqr: undefined, min: undefined, max: undefined},\n                    ora: {sum: undefined, sumSqr: undefined, min: undefined, max: undefined},\n                    rtt: {sum: undefined, sumSqr: undefined, min: undefined, max: undefined},\n                    ortt: {sum: undefined, sumSqr: undefined, min: undefined, max: undefined}\n};\n\nconst measurementTime = {start: -1, stop: -1};\n\nconst sampleData = [];    // array of samples (tuples of collected invocation data, for rule or for action, depending on the activity)\n\nvar loopSleeper;    // used to abort sleep in mainLoop()\nvar abort = false;    // used to abort the loop in mainLoop()\n\n// Used only at the master\nvar workerData = [];    // holds data for each worker, at [1..#workers]. Master's entry is 0.\n\nconst activity = ((cluster.isWorker || !testRecord.input.master_activity) ? testRecord.input.activity : testRecord.input.master_activity);\n\nif (cluster.isMaster)\n    runMaster();\nelse\n    runWorker();\n\n// -------- END OF MAIN -------------\n\n/**\n * Master operation\n */\nfunction runMaster() {\n\n    // Setup OpenWhisk assets for the test\n    testSetup().then(() => {\n\n        // Start workers, configure interaction\n        for(var i = 0; i < testRecord.input.workers; i++) {\n            if (i > 0)        // fork only (workers - 1) times\n                cluster.fork();\n        }\n\n        for (const id in cluster.workers) {\n\n            // Exit handler for each worker\n            cluster.workers[id].on('exit', (code, signal) => {\n                if (signal)\n                    mLog(`Worker ${id} was killed by signal: ${signal}`);\n                else\n                    if (code !== 0)\n                        mLog(`Worker ${id} exited with error code: ${code}`);\n                checkExit();\n            });\n\n            // Message handler for each worker\n            cluster.workers[id].on('message', (msg) => {\n                if (msg.init)\n                    // Initialization barrier for workers. Makes sure they are all fully engaged when the measurement start\n                    checkInit();\n\n                if (msg.summary) {\n                    workerData[id] = msg.summary;\n                    checkSummary();\n                }\n            });\n        }\n\n        mainLoop().then(() => {\n\n            // set finish of measurement and notify all other workers\n            measurementTime.stop = new Date().getTime();\n            testRecord.output.measure_time = (measurementTime.stop - measurementTime.start) / 1000.0;    // measurement duration converted to seconds\n            mLog(`Stop measurement. Start post-processing after ${testRecord.input.pp_delay} msec`);\n            mLogSampleHeader();\n            for (const j in cluster.workers)\n                cluster.workers[j].send({abort: measurementTime});\n\n            // The master's post-processing to generate its workerData\n            sleep(testRecord.input.pp_delay)\n                .then(() => {\n                    postProcess()\n                    .then(() => {\n                        // The master's workerData\n                        workerData[0] = {lat: latCounters, tp: tpCounters};\n                        checkSummary();\n                    })\n                    .catch(err => {    // FATAL - shouldn't happen unless BUG\n                        mLog(`Post-process ERROR in MASTER: ${err}`);\n                        throw err;\n                    });\n                });\n        });\n\n    });\n\n}\n\n\n/**\n * Setup assets before the test depending on configuration\n */\nasync function testSetup() {\n\n    if (!testRecord.input.setup)\n        return;\n\n    const cmd = `./setup.sh s ${testRecord.input.ratio} ${wskParams}`;\n    mLog(`SETUP: ${cmd}`);\n\n    try {\n        await exec(cmd);\n    }\n    catch (error) {\n        mLog(`FATAL: setup failure - ${error}`);\n        process.exit(-2);\n    }\n}\n\n\n/**\n * Teardown assets after the test depending on configuration\n */\nasync function testTeardown() {\n\n    if (!testRecord.input.teardown)\n        return;\n\n    const cmd = `./setup.sh t ${testRecord.input.ratio} ${wskParams}`;\n    mLog(`TEARDOWN: ${cmd}`);\n\n    try {\n        await exec(cmd);\n    }\n    catch (error) {\n        mLog(`WARNING: teardown error - ${error}`);\n        process.exit(-3);\n    }\n}\n\n\n/**\n * Print table header for samples to the runtime log on stderr\n */\nfunction mLogSampleHeader() {\n    mLog(\"bi,\\tas,\\tae,\\tts,\\tta,\\toea,\\toer,\\td,\\tad,\\tai,\\tora,\\trtt,\\tortt\");\n}\n\n/**\n * Worker operation\n */\nfunction runWorker() {\n\n    // abort message from master will set the measurement time frame and abort the loop\n    process.on('message', (msg) => {\n        if (msg.abort) {\n            // Set the measurement time frame at the worker - required for post-processing\n            measurementTime.start = msg.abort.start;\n            measurementTime.stop = msg.abort.stop;\n            abortLoop();\n        }\n    });\n\n    mainLoop().then(() => {\n        sleep(testRecord.input.pp_delay)\n            .then(() => {\n                postProcess()\n                    .then(() => {\n                        process.send({summary:{lat: latCounters, tp:tpCounters}});\n                        process.exit();\n                    })\n                    .catch(err => {    // shouldn't happen unless BUG\n                        mLog(`Post-process ERROR in WORKER: ${err}`);\n                        throw err;\n                    });\n                });\n    });\n}\n\n\n// Barrier for checking all workers have initialized and then start measurement\n\nvar remainingInits = testRecord.input.workers;\nvar remainingIterations = -1;\n\nfunction checkInit() {\n    remainingInits--;\n    if (remainingInits == 0) {    // all workers are engaged (incl. master) - can start measurement\n        mLog(\"All clients finished warmup. Start measurement.\");\n        measurementTime.start = new Date().getTime();\n\n        if (testRecord.input.period)\n            setTimeout(abortLoop, testRecord.input.period);\n\n        if (testRecord.input.iterations)\n            remainingIterations = testRecord.input.iterations;\n    }\n}\n\n// Barrier for checking all workers have finished, generate output and exit\n\nvar remainingExits = testRecord.input.workers;\n\nfunction checkExit() {\n    remainingExits--;\n    if (remainingExits == 0) {\n        mLog(\"All workers finished - generating output and exiting.\");\n        generateOutput();\n        // Cleanup test assets from OW and then exit\n        testTeardown().then(() => {\n            mLog(\"Done\");\n            process.exit();\n        });\n    }\n}\n\n// Barrier for receiving post-processing results from all workers before computing final results\n\nvar remainingSummaries = testRecord.input.workers;\n\nfunction checkSummary() {\n    remainingSummaries--;\n    if (remainingSummaries == 0) {\n        mLogSampleHeader();\n        mLog(\"All clients post-processing completed - computing output.\")\n        computeOutputRecord();\n        checkExit();\n    }\n}\n\n\n/**\n * Main loop for invocations - invoke activity asynchronously once every (delta) msec until aborted\n */\nasync function mainLoop() {\n\n    var warmupCounter = testRecord.input.warmup;\n    const delta = ((cluster.isWorker || !testRecord.input.master_delta) ? testRecord.input.delta : testRecord.input.master_delta);\n    const blocking = ((cluster.isWorker || !testRecord.input.master_blocking) ? testRecord.input.blocking : testRecord.input.master_blocking);\n    const doBlocking = (blocking != NONE);\n    const getResult = (blocking == RESULT);\n\n    while (!abort) {\n\n        // ----\n        // Pass init (worker - send message) after <warmup> iterations\n        if (warmupCounter == 0) {\n            if (cluster.isMaster)\n                checkInit();\n            else     // worker - send init\n                process.send({init: 1});\n        }\n\n        if (warmupCounter >= 0)        // take 0 down to -1 to make sure it does not trigger another init message\n            warmupCounter--;\n        // ----\n\n        // If iterations limit set, abort loop when finished iterations\n        if (remainingIterations == 0) {\n            abortLoop();\n            continue;\n        }\n\n        if (remainingIterations > 0)\n            remainingIterations--;\n\n        const si = new Date().getTime();    // SI = Start of Iteration timestamp\n\n        var samples;\n\n        if (activity == ACTION)\n            samples = await invokeActions(testRecord.input.ratio, doBlocking, getResult, si);\n        else\n            samples = await invokeRules(si);\n\n        samples.forEach(sample => {\n            sampleData.push(sample);\n        });\n\n        const ei = new Date().getTime();    // EI = End of Iteration timestamp\n        const duration = ei - si;\n        if (delta > duration) {\n            loopSleeper = sleep(delta - duration);\n            if (!abort)        // check again to avoid race condition on loopSleeper\n                await loopSleeper;\n        }\n    }\n}\n\n\n/**\n * Used to abort the mainLoop() function\n */\nfunction abortLoop() {\n    abort = true;\n    if (loopSleeper)\n        loopSleeper.resolve();\n}\n\n\n/**\n * Invoke the predefined OW action a specified number of times without waiting using Promises (burst).\n * Returns a promise that resolves to an array of {id, isError}.\n */\nfunction invokeActions(count, doBlocking, getResult, burst_bi) {\n    return new Promise( function (resolve, reject) {\n        var ipa = [];    // array of invocation promises;\n        for(var i = 0; i< count; i++) {\n            ipa[i] = new Promise((resolve, reject) => {\n                const bi = (testRecord.input.burst_timing ? burst_bi : new Date().getTime());  // default is BI per invocation\n                ow.actions.invoke({name: 'testAction', blocking: doBlocking, result: getResult, params: params})\n                    // If returnedJSON is full activation or just activation ID then activation ID should be in \"activationId\" field\n                    // If returnedJSON is the result of the test action, then \"activationId\" is part of the returned result of the test action\n                    .then(returnedJSON => {\n                        var ai;    // after invocation\n                        if (doBlocking)\n                            ai = new Date().getTime();     // only for blocking invocations, AI is meaningful\n                        resolve({aaid: returnedJSON.activationId, bi: bi, ai: ai});\n                    })\n                    .catch(err => {\n                        resolve({aaidError: err});\n                    });\n            });\n        }\n\n        Promise.all(ipa).then(ipArray => {\n            resolve(ipArray);\n        }).catch(err => {    // Impossible to reach since no contained promise rejects\n            reject(err);\n        });\n\n    });\n}\n\n\n/**\n * Invoke the predefined OW rules asynchronously and return a promise of an array with a single element of {id, isError}\n */\nfunction invokeRules(bi) {\n    return new Promise( function (resolve, reject) {\n        const triggerSamples = [];\n        // Fire trigger to invoke the rule\n        ow.triggers.invoke({name: 'testTrigger', params: params})\n            .then(triggerActivationIdJSON => {\n                const triggerActivationId = triggerActivationIdJSON.activationId;\n                triggerSamples.push({taid: triggerActivationId, bi: bi});\n                resolve(triggerSamples);\n            })\n            .catch (err => {\n                triggerSamples.push({taidError: err});\n                resolve(triggerSamples);\n            });\n    });\n}\n\n\n/**\n * This function processes the sampleData. Each sample is processed as following:\n * 1. A sample with error (TAID or AAID) is processed directly (not much to do beyond counting errors)\n * 2. An action sample - first attempt to retrieve activation, then process with it\n * 3. A rule sample - first convert to set of bound action samples (by processing the trigger activation), then process each action sample in step 2 above\n  */\nasync function postProcess() {\n    for(var i in sampleData) {\n        const sample = sampleData[i];\n        if (activity == ACTION) {\n            await processSampleWithAction(sample);\n        }\n        else {        // activity == RULE\n            if (sample.taidError)    // TAID error - no need to retrieve bound actions - move to process the sample directly\n                processSample(sample);\n            else {    // have valid TAID - retrieve bound action ids and then process\n                const actionSamples = await getActionSamplesOfRules(sample);\n                for(var j in actionSamples)\n                    await processSampleWithAction(actionSamples[j]);\n            }\n        }\n    }\n}\n\n\n/**\n * Retrieve the activation ids of the actions bound to the trigger activation provided by id.\n * Failure to retrieve trigger activation for a valid activation id is considered a fatal error, since the activation must exist.\n * @param {*} triggerActivation\n */\nfunction getActionSamplesOfRules(triggerSample) {\n    return new Promise((resolve, reject) => {\n        ow.activations.get({name: triggerSample.taid})\n            .then(triggerActivation => {\n                triggerSample.ts = triggerActivation.start;\n                var actionSamples = [];\n                for(var i = 0; i < triggerActivation.logs.length; i++) {\n                    const boundActionRecord = JSON.parse(triggerActivation.logs[i]);\n                    const actionSample = Object.assign({}, triggerSample);\n                    if (boundActionRecord.success)\n                        actionSample.aaid = boundActionRecord.activationId;\n                    else\n                        actionSample.aaidError = boundActionRecord.error;\n                    actionSamples.push(actionSample);\n                }\n                resolve(actionSamples);\n            })\n            .catch (err =>    {    // FATAL: failed to retrieve trigger activation for a valid id\n                mLog(`getActionSamplesOfRules returned ERROR: ${err}`);\n                reject(err);\n            });\n    });\n}\n\n\n/**\n * Processing each action sample sequentially, i.e., wait until activation is retrieved before retrieving the next one.\n * Otherwise, concurrent retrieval of possibly thousands of activations and more, may cause issues.\n * Failure to retrieve activation record for a valid id is ok, assuming the action may have not completed yet.\n * @param {*} actionSample\n */\nasync function processSampleWithAction(actionSample) {\n    if (actionSample.aaidError)    // no activation, move on to processing sample with error\n        processSample(actionSample);\n    else {    // have activation, try to get record\n        var activation;\n        try {\n            activation = await ow.activations.get({name: actionSample.aaid});\n        }\n        catch (err) {\n            mLog(`Failed to retrieve activation for id: ${actionSample.aaid} for reason: ${err}`);\n        }\n        processSample(actionSample, activation);\n    }\n}\n\n\n/**\n * Process a single sample + optional related action activation, updating latency and throughput counters\n * @param {*} sample\n */\nfunction processSample(sample, activation) {\n\n    const bi = sample.bi;\n\n    if (bi < measurementTime.start || bi > measurementTime.stop)    {    // BI outside time frame. No further processing.\n        mLog(`Sample discarded. BI exceeds measurement time frame`);\n        return;\n    }\n\n    tpCounters.attempts++;    // each sample invoked in the time frame counts as one invocation attempt\n\n    if (sample.taidError) {    // trigger activation failed - count one request, one error. No further processing.\n        tpCounters.requests++;\n        tpCounters.errors++;\n        mLog(`Sample discarded. Trigger activation error: ${sample.taidError}`);\n        return;\n    }\n\n    var ts;\n    if (sample.ts) {\n        ts = parseInt(sample.ts);\n\n        if (ts >= measurementTime.start && ts <= measurementTime.stop) {    // trigger activation in time frame - count one activation, one request\n            tpCounters.activations++;\n            tpCounters.requests++;\n        }\n    }\n    else\n        ts = undefined;\n\n    if (sample.aaidError) {    // action activation failed - count one request, one error. No further processing.\n        tpCounters.requests++;\n        tpCounters.errors++;\n        mLog(`Sample discarded. Action activation error: ${sample.aaidError}`);\n        return;\n    }\n\n    if (!activation) {    // no activation, so assumed incomplete. No further processing.\n        mLog(`Sample discarded. Activation was not retrieved.`)\n        return;\n    }\n\n    const as = parseInt(activation.start);\n    const ae = parseInt(activation.end);\n    const d = parseInt(activation.response.result.duration);\n\n    if (as < measurementTime.start || ae > measurementTime.stop) {    // got activation, but it exceeds the time frame. No further processing.\n        mLog(`Sample discarded. Action activation exceeded measurement time frame.`)\n        return;\n    }\n\n    // Activation is in time frame, so count one activation, one request and one full invocation\n    tpCounters.activations++;\n    tpCounters.requests++;\n    tpCounters.invocations++;\n\n    // For full invocations, update latency counters\n\n    const ta = (ts ? as - ts : undefined);\n    const ad = ae - as;\n    const oea = as - bi;\n    const oer = ae - bi - d;\n\n    updateLatSample(\"d\", d);\n    updateLatSample(\"ta\", ta);\n    updateLatSample(\"ad\", ad);\n    updateLatSample(\"oea\", oea);\n    updateLatSample(\"oer\", oer);\n\n    // for blocking action invocations - will be \"undefined\" otherwise\n    const ai = sample.ai;\n\n    const ora = (ai ? ai - ae : undefined);\n    const rtt = (ai ? ai - bi : undefined);\n    const ortt = (rtt ? rtt - d : undefined);\n\n    updateLatSample(\"ora\", ora);\n    updateLatSample(\"rtt\", rtt);\n    updateLatSample(\"ortt\", ortt);\n\n    mLog(`${bi},\\t${as},\\t${ae},\\t${ts},\\t${ta},\\t${oea},\\t${oer},\\t${d},\\t${ad},\\t${ai},\\t${ora},\\t${rtt},\\t${ortt}`);\n}\n\n/**\n * Update counters of one latency statistic of a worker with value data from one sample\n */\nfunction updateLatSample(statName, value) {\n\n    if (!value)     // value == undefined => skip it\n        return;\n\n    // Update sum for avg\n    if (!latCounters[statName].sum)\n        latCounters[statName].sum = 0;\n    latCounters[statName].sum += value;\n\n    // Update sumSqr for std\n    if (!latCounters[statName].sumSqr)\n        latCounters[statName].sumSqr = 0;\n    latCounters[statName].sumSqr += value * value;\n\n    // Update min value\n    if (!latCounters[statName].min || latCounters[statName].min > value)\n        latCounters[statName].min = value;\n\n    // Update max value\n    if (!latCounters[statName].max || latCounters[statName].max < value)\n        latCounters[statName].max = value;\n}\n\n\n/**\n * Compute the final output record based on the workerData records.\n * The output of the program is a single CSV row of data consisting of the input parameters,\n * then latencies computed above - avg (average) and std (std. dev.), then throughput.\n */\nfunction computeOutputRecord() {\n\n    // Latency stats: avg, std, min, max\n    [\"ta\", \"oea\", \"oer\", \"d\", \"ad\", \"ora\", \"rtt\", \"ortt\"].forEach(statName => {\n        testRecord.output[statName] = computeLatStats(statName);\n    });\n\n    // Tp stats: abs, tp, tpw, tpd\n    [\"attempts\", \"invocations\", \"activations\", \"requests\"].forEach(statName => {\n        testRecord.output[statName] = computeTpStats(statName);\n    });\n\n    // Error stats: abs, percent\n    testRecord.output.errors = computErrorStats();\n}\n\n\n/**\n * Based on workerData, compute average and standard deviation of a given latency statistic.\n * @param {*} statName\n */\nfunction computeLatStats(statName) {\n    var totalSum = 0;\n    var totalSumSqr = 0;\n    var totalInvocations = 0;\n    var hasSamples = undefined;    // does the current stat have any samples. If not => undefined, not NaN\n    var min = undefined;\n    var max = undefined;\n    if (testRecord.input.master_apart) {    // in master_apart mode, only master performs latency measurements\n        totalSum = workerData[0].lat[statName].sum;\n        totalSumSqr = workerData[0].lat[statName].sumSqr;\n        min = workerData[0].lat[statName].min;\n        max = workerData[0].lat[statName].max;\n        totalInvocations = workerData[0].tp.invocations;\n    }\n    else // in regular mode, all workers participate in latency measurements\n        workerData.forEach(wd => {\n            if (wd.lat[statName].sum) {    // If this worker has valid latency samples (not undefined)\n                hasSamples = 1;\n                totalSum += wd.lat[statName].sum;\n                totalSumSqr += wd.lat[statName].sumSqr;\n                if (!min || min > wd.lat[statName].min)\n                    min = wd.lat[statName].min;\n                if (!max || max < wd.lat[statName].max)\n                    max = wd.lat[statName].max;\n            }\n            totalInvocations += wd.tp.invocations;\n        });\n\n    const avg = (hasSamples ? totalSum / totalInvocations : undefined);\n    const std = (hasSamples ? Math.sqrt(totalSumSqr / totalInvocations - avg * avg) : undefined);\n\n    return ({avg: avg, std: std, min: min, max: max});\n}\n\n\n/**\n * Based on workerData, compute throughput of a given counter, with (tp) and without (tpw) the master, and the percent difference (tpd)\n * @param {*} statName\n */\nfunction computeTpStats(statName) {\n    var masterCount = workerData[0].tp[statName];\n    var totalCount = 0;\n    workerData.forEach(wd => {totalCount += wd.tp[statName];});\n    const tp = totalCount / testRecord.output.measure_time;            // throughput\n    const tpw = (totalCount - masterCount) / testRecord.output.measure_time;        // throughput without master\n    const tpd = (tp - tpw) * 100.0 / tp;        // percent difference relative to TP\n\n    return ({abs: totalCount, tp: tp, tpw: tpw, tpd: tpd});\n}\n\n\n/**\n * Based on workerData, compute the relative portion of total errors out of total requests\n */\nfunction computErrorStats() {\n    var totalErrors = 0;\n    var totalRequests = 0;\n\n    workerData.forEach(wd => {\n        totalErrors += wd.tp.errors;\n        totalRequests += wd.tp.requests;\n    });\n\n    const errAbs = totalErrors;\n    const errPer = totalErrors * 100.0 / totalRequests;\n    return ({abs: errAbs, percent: errPer});\n}\n\n\n/**\n * Generate a properly formatted output record to stdout. The header is also printed, but via mDump to stderr and can be\n * silenced.\n */\nfunction generateOutput() {\n    var first = true;\n\n    // First, print header to stderr\n    dfsObject(testRecord, (name, data, isRoot, isObj) => {\n        if (!isObj) {        // print leaf nodes\n            if (!first)\n                mWrite(\",\\t\");\n            first = false;\n            mWrite(`${name}`);\n        }\n    });\n    mWrite(\"\\n\");\n\n    first = true;\n\n    // Now, print data to stdout\n    dfsObject(testRecord, (name, data, isRoot, isObj) => {\n        if (!isObj) {        // print leaf nodes\n            if (!first)\n                process.stdout.write(\",\\t\");\n            first = false;\n            if (typeof data == 'number')    // round each number to 3 decimal digits\n                data = round(data, 3);\n            process.stdout.write(`${data}`);\n        }\n    });\n    process.stdout.write(\"\\n\");\n}\n\n\n/**\n * Sleep for a given time. Useful mostly with await from an async function\n * resolve and reject are externalized as properties to allow early abortion\n * @param {*} ms\n */\nfunction sleep(ms) {\n    var res, rej;\n    var p = new Promise((resolve, reject) => {\n        setTimeout(resolve, ms);\n        res = resolve;\n        rej = reject;\n    });\n    p.resolve = res;\n    p.reject = rej;\n\n    return p;\n  }\n\n\n/**\n * Generate a random integer in the range of [1..max]\n * @param {*} max\n */\nfunction getRandomInt(max) {\n    return Math.floor(Math.random() * Math.floor(max) + 1);\n  }\n\n\n/**\n * Round a number after specified decimal digits\n * @param {*} num\n * @param {*} digits\n */\n  function round(num, digits = 0) {\n    const factor = Math.pow(10, digits);\n    return Math.round(num * factor) / factor;\n}\n\n\n// If not quiet, emit control messages on stderr (with newline)\nfunction mLog(text) {\n    if (!testRecord.input.quiet)\n        console.error(`${clientId()}:\\t${text}`);\n}\n\n\n/**\n * Return the id of the client - MASTER-0 or WORKER-k (k=1..w-1)\n */\nfunction clientId() {\n    return (cluster.isMaster ? \"MASTER-0\" : `WORKER-${cluster.worker.id}`);\n}\n\n\n// If not quiet, write strings on stderr (w/o newline)\nfunction mWrite(text) {\n    if (!testRecord.input.quiet)\n        process.stderr.write(text);\n}\n\n/**\n * Traverse a (potentially deep) object in DFS, visiting each non-function node with function f\n * @param {*} data\n * @param {*} func\n */\nfunction dfsObject(data, func, allowInherited = false) {\n    var isRoot = true;\n    var rootObj = data;\n    crawlObj(\"\", data, func, allowInherited);\n\n    function crawlObj(name, data, f, allowInherited) {\n        var isObj = (typeof data == 'object');\n        var isFunc = (typeof data == 'function');\n        if (!isFunc)\n            f(name, data, isRoot, isObj);    // visit the current node\n        isRoot = false;\n        if (isObj)\n            for (var child in data) {\n                if (allowInherited || data.hasOwnProperty(child)) {\n                    const childName = (name == \"\" ? child : name + \".\" + child);\n                    crawlObj(childName, data[child], f, true);    // After root level no need to check inheritance\n                }\n            }\n    }\n}\n"
  },
  {
    "path": "tools/owperf/owperf.sh",
    "content": "#!/bin/bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# This is a simple launch script for owperf\n\nnode owperf.js $@\n"
  },
  {
    "path": "tools/owperf/package.json",
    "content": "{\n  \"name\": \"owperf\",\n  \"version\": \"1.0.0\",\n  \"description\": \"owperf - a performance evaluation tool for Apache OpenWhisk\",\n  \"main\": \"owperf.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": {\n    \"name\": \"Erez Hadad\",\n    \"email\": \"erezh@il.ibm.com\"\n  },\n  \"license\": \"Apache-2.0\",\n  \"dependencies\": {\n    \"btoa\": \"^1.2.1\",\n    \"child-process-promise\": \"^2.2.1\",\n    \"cluster\": \"^0.7.7\",\n    \"commander\": \"^2.19.0\",\n    \"ini\": \"^1.3.5\",\n    \"node-exec-promise\": \"^1.0.2\",\n    \"openwhisk\": \"^3.21.7\",\n    \"xmlhttprequest\": \"^1.8.0\"\n  }\n}\n"
  },
  {
    "path": "tools/owperf/setup.sh",
    "content": "#!/bin/bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -x\n\n# Setup for overhead test: create action, create trigger, and create a number of rules as required.\n# This script performs both setup and teardown\n# Designed to be an idempotent operation (can be applied repeatedly for the same result)\n# Usage: setup.sh [op] [ratio] <wsk global flags>\n# op - MANDATORY. \"s\" for setup, \"t\" for teardown\n# ratio - MANDATORY. ratio as defined in README.md\n# wsk global flags - OPTIONAL. Global flags for the wsk command (e.g. for specifying non-default wsk API host, auth, etc)\n\nMAXRULES=30\t# assume no more than 30 rules per trigger max\nop=$1\t\t# s for setup, t for teardown\ncount=$2\t# ratio for rules\ndelcount=$count # For teardown, delete ratio rules. For setup, delete MAXRULES\nif [ \"$op\" = \"s\" ]; then\n\tdelcount=$MAXRULES\nfi\n\nshift 2\nwskparams=\"$@\"\t# All other parameters are assumed to be OW-specific\n\n\nfunction remove_assets() {\n\n\t# Delete rules\n\tfor i in $(seq 1 $delcount); do\n    \t\twsk rule delete testRule_$i $@;\n\tdone\n\n\t# Delete trigger\n\twsk trigger delete testTrigger $@\n\n\t# Delete action\n\twsk action delete testAction $@\n\n}\n\n\nfunction deploy_assets() {\n\n\t# Create action\n\twsk action create testAction testAction.js --kind nodejs:default $@\n\n\t# Create trigger after deleting it\n\twsk trigger create testTrigger $@\n\n\t# Create rules\n\tfor i in $(seq 1 $count); do\n    \t\twsk rule create testRule_$i testTrigger testAction $@;\n\tdone\n\n}\n\n\n# Always start with removal of existing assets\nremove_assets $wskparams\n\n# If setup requested, deploy new assets\nif [ \"$op\" = \"s\" ]; then\n\tdeploy_assets $wskparams\nfi\n\n"
  },
  {
    "path": "tools/owperf/testAction.js",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\n/**\n * Default test action for owperf. Sleeps specified time.\n * All test actions should return the invocation parameters (to stress the return path), but augmented with the execution duration and the activation id.\n * Use this code as reference if you want to create a custom test action.\n */\n\nfunction sleep(ms) {\n    return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nasync function main(params) {\n    var start = new Date().getTime();\n    params.activationId = process.env.__OW_ACTIVATION_ID;\n    await sleep(parseInt(params.sleep));\n    var end = new Date().getTime();\n    params.duration = end - start;\n    return params;\n}\n\n// Invoke main when runnig - only when setting TEST in the env\nif (process.env.TEST)\n    main({sleep:50}).then(params => console.log(params.duration));\n\n"
  },
  {
    "path": "tools/travis/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Travis Setup\n\nTravis build is configured to perform build of this repo in multiple parallel jobs as listed below.\n\n1. Unit Tests - Runs the test which only need database service.\n2. System Tests - Runs those tests which need complete OpenWhisk system up and running.\n3. Performance test suite - Run basic performance tests with the objective to check if tests are working or not.\n\nThese jobs make use of following scripts\n\n1. `scan.sh` - Performs various code scan task like python flake scan, scala formatting etc.\n2. `setupPrereq.sh` - Performs setup if basis prerequisites like database setup and property file generation.\n3. `distDocker.sh` - Builds the various docker containers.\n4. `setupSystem.sh` - Runs the various containers which are part of an OpenWhisk setup like Controller, Invoker etc.\n5. `runTests.sh` - Runs the tests. It make use of `ORG_GRADLE_PROJECT_testSetName` env setting to determine which test\n   suite to run.\n6. `checkAndUploadLogs.sh` -  Collects the logs, checks them and uploads them to https://openwhisk.box.com/v/travis-logs.\n"
  },
  {
    "path": "tools/travis/box-upload.py",
    "content": "#!/usr/bin/env python\n\"\"\"Executable Python script for compressing folders to Box.\n\nCompresses the contents of a folder and upload the result to Box.\n\n  Run this script as:\n  $ upload-logs.py LOG_DIR DEST_NAME\n\n  e.g.: $ upload-logs.py /tmp/wsklogs logs-5512.tar.gz\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\nfrom __future__ import print_function\n\nimport os\nimport subprocess\nimport sys\nimport tempfile\nimport urllib\nimport humanize\nimport requests\nimport hashlib\n\n\ndef upload_file(local_file, remote_file):\n    \"\"\"Upload file.\"\"\"\n    if remote_file[0] == '/':\n        remote_file = remote_file[1:]\n\n    url = \"http://DamCYhF8.mybluemix.net/upload?%s\" % \\\n        urllib.parse.urlencode({\"name\": remote_file})\n\n    r = requests.post(url,\n            headers={\"Content-Type\": \"application/gzip\"},\n            data=open(local_file, 'rb'))\n\n    print(\"Posting result\", r)\n    print(r.text)\n\n\ndef tar_gz_dir(dir_path):\n    \"\"\"Create TAR (ZIP) of path and its contents.\"\"\"\n    _, dst = tempfile.mkstemp(suffix=\".tar.gz\")\n    subprocess.call([\"tar\", \"-cvzf\", dst, dir_path])\n    return dst\n\n\ndef print_tarball_size(tarball):\n    \"\"\"Get and print the size of the tarball\"\"\"\n    tarballsize = os.path.getsize(tarball)\n    print(\"Size of tarball\", tarball, \"is\", humanize.naturalsize(tarballsize))\n\n    sha256_hash = hashlib.sha256()\n    with open(tarball, \"rb\") as f:\n        for byte_block in iter(lambda: f.read(4096), b\"\"):\n            sha256_hash.update(byte_block)\n    print(\"SHA256 hash of tarball is\", sha256_hash.hexdigest())\n\n\nif __name__ == \"__main__\":\n    dir_path = sys.argv[1]\n    dst_path = sys.argv[2]\n\n    if not os.path.isdir(dir_path):\n        print(\"Directory doesn't exist: %s.\" % dir_path)\n        sys.exit(0)\n\n    print(\"Compressing logs dir...\")\n    tar = tar_gz_dir(dir_path)\n    print_tarball_size(tar)\n\n    print(\"Uploading to Box...\")\n    upload_file(tar, dst_path)\n"
  },
  {
    "path": "tools/travis/checkAndUploadLogs.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Disable abort script at first error as we require the logs to be uploaded\n# even if check and log collection fails\n# set -e\n\nSECONDS=0\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR\n\nLOG_NAME=$1\nTAGS=${2-\"\"}\nLOG_TAR_NAME=\"${LOG_NAME}_${TRAVIS_BUILD_ID}-$TRAVIS_BRANCH.tar.gz\"\n\n# Perf logs are typically about 20MB and thus rapidly fill our box account.\n# Disable upload to reduce the interval at which we need to manually clean logs from box.\nif [ \"$LOG_NAME\" == \"perf\" ]; then\n    echo \"Skipping upload of perf logs to conserve space\"\n    exit 0\nfi\n\nansible-playbook -i ansible/environments/local ansible/logs.yml\n\n./tools/build/checkLogs.py logs \"$TAGS\"\n\n./tools/travis/box-upload.py \"$TRAVIS_BUILD_DIR/logs\" \"$LOG_TAR_NAME\"\n\necho \"Uploaded Logs with name $LOG_TAR_NAME\"\necho \"Time taken for ${0##*/} is $SECONDS secs\"\n"
  },
  {
    "path": "tools/travis/distDocker.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\n# Build script for Travis-CI.\n\nSECONDS=0\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR\nTERM=dumb ./gradlew clean # Run a clean step before build\nTERM=dumb ./gradlew distDocker -PdockerImagePrefix=testing $GRADLE_PROJS_SKIP\n\nTERM=dumb ./gradlew :core:controller:distDockerCoverage -PdockerImagePrefix=testing\nTERM=dumb ./gradlew :core:scheduler:distDockerCoverage -PdockerImagePrefix=testing\nTERM=dumb ./gradlew :core:invoker:distDockerCoverage -PdockerImagePrefix=testing\nTERM=dumb ./gradlew :core:standalone:build\n\necho \"Time taken for ${0##*/} is $SECONDS secs\"\n"
  },
  {
    "path": "tools/travis/docker.conf",
    "content": "#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n[Service]\nExecStart=\nExecStart=/usr/bin/dockerd\n"
  },
  {
    "path": "tools/travis/docker.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nBASEDIR=$(dirname \"$0\")\necho \"$BASEDIR\"\n\nsudo gpasswd -a travis docker\nsudo usermod -aG docker travis\n#sudo -E bash -c 'echo '\\''DOCKER_OPTS=\"-H tcp://0.0.0.0:4243 -H unix:///var/run/docker.sock --storage-driver=overlay --userns-remap=default\"'\\'' > /etc/default/docker'\n\n# Docker\nsudo apt-get clean\nsudo apt-get update\n\n# Need to update dpkg due to known issue: https://bugs.launchpad.net/ubuntu/+source/dpkg/+bug/1730627\nsudo apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common dpkg\ncurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\nsudo apt-key fingerprint 0EBFCD88\n\n# This is required because libseccomp2 (>= 2.3.0) is not provided in trusty by default\nsudo add-apt-repository -y ppa:ubuntu-sdk-team/ppa\n\nsudo add-apt-repository \\\n    \"deb [arch=$(uname -m | sed -e 's/x86_64/amd64/g')] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\"\n\nsudo apt-get update\nsudo apt-get -o Dpkg::Options::=\"--force-confold\" --force-yes -y install docker-ce=18.06.3~ce~3-0~ubuntu containerd.io\n# daemon.json and flags does not work together. Overwritting the docker.service file\n# to remove the host flags. - https://docs.docker.com/config/daemon/#troubleshoot-conflicts-between-the-daemonjson-and-startup-scripts\nsudo mkdir -p /etc/systemd/system/docker.service.d\nsudo cp $BASEDIR/docker.conf /etc/systemd/system/docker.service.d/docker.conf\n# setup-docker will add configs to /etc/docker/daemon.json\nsudo python $BASEDIR/setup-docker.py\nsudo cat /etc/docker/daemon.json\nsudo systemctl daemon-reload\nsudo systemctl restart docker\nsudo systemctl status docker.service\necho \"Docker Version:\"\ndocker version\necho \"Docker Info:\"\ndocker info\n"
  },
  {
    "path": "tools/travis/flake8.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\npip3 install --user --upgrade flake8\n\n# These files do not have a .py extension so flake8 will not scan them\ndeclare -a PYTHON_FILES=(\".\"\n                         \"./tools/admin/wskadmin\"\n                         \"./tools/build/citool\"\n                         \"./tools/build/redo\")\n\necho 'Flake8: first round (fast fail) stops the build if there are any Python 3 syntax errors...'\nfor i in \"${PYTHON_FILES[@]}\"\ndo\n    flake8 \"$i\" --select=E999,F821 --statistics\n    RETURN_CODE=$?\n    if [ $RETURN_CODE != 0 ]; then\n        echo 'Flake8 found Python 3 syntax errors above. See: https://docs.python.org/3/howto/pyporting.html'\n        exit $RETURN_CODE\n    fi\ndone\n\necho 'Flake8: second round to find any other stylistic issues...'\nfor i in \"${PYTHON_FILES[@]}\"\ndo\n    flake8 \"$i\" --ignore=E,W503,W504,W605 --max-line-length=127 --statistics\ndone\n"
  },
  {
    "path": "tools/travis/runLeanSystemTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\n\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_LEAN_SYSTEM\"\nexport GRADLE_COVERAGE=true\n\n./setupPrereq.sh /ansible/files/runtimes-nodeonly.json\n\n./distDocker-lean.sh\n\n./setupLeanSystem.sh /ansible/files/runtimes-nodeonly.json\n\n./runTests.sh\n"
  },
  {
    "path": "tools/travis/runMultiRuntimeTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\n\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_MULTI_RUNTIME\"\nexport GRADLE_COVERAGE=true\n\n./setupPrereq.sh\n\n./distDocker.sh\n\n./setupSystem.sh\n\n./runTests.sh\n"
  },
  {
    "path": "tools/travis/runSchedulerTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_SCHEDULER\"\nexport GRADLE_COVERAGE=true\n\n./setupPrereq.sh /ansible/files/runtimes-nodeonly.json\n\n./distDocker.sh\n\n./setupSystem.sh /ansible/files/runtimes-nodeonly.json\n\n./runTests.sh\n"
  },
  {
    "path": "tools/travis/runStandaloneTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_STANDALONE\"\nexport GRADLE_COVERAGE=true\n\ncd $ROOTDIR/ansible\n$ANSIBLE_CMD setup.yml\n$ANSIBLE_CMD properties.yml -e manifest_file=\"/ansible/files/runtimes-nodeonly.json\"\n$ANSIBLE_CMD downloadcli-github.yml\n\n# Install kubectl\ncurl -Lo ./kubectl https://storage.googleapis.com/kubernetes-release/release/v1.16.1/bin/linux/amd64/kubectl\nchmod +x kubectl\nsudo cp kubectl /usr/local/bin/kubectl\n\n# Install kind\ncurl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.5.1/kind-linux-amd64\nchmod +x kind\nsudo cp kind /usr/local/bin/kind\n\nkind create cluster --wait 5m\nexport KUBECONFIG=\"$(kind get kubeconfig-path)\"\nkubectl config set-context --current --namespace=default\n\n# This is required because it is timed out to pull the image during the test.\ndocker pull openwhisk/action-nodejs-v14:nightly\ndocker pull openwhisk/dockerskeleton:nightly\ndocker pull openwhisk/example:nightly\ndocker pull openwhisk/apigateway:0.11.0\n\ncd $ROOTDIR\nTERM=dumb ./gradlew :core:standalone:build \\\n  :core:monitoring:user-events:distDocker\n\ncd $ROOTDIR\nTERM=dumb ./gradlew :core:standalone:cleanTest \\\n  :core:standalone:test \\\n  :core:monitoring:user-events:reportTestScoverage\n\n# Run test in end as it publishes the coverage also\ncd $ROOTDIR/tools/travis\n./runTests.sh\n"
  },
  {
    "path": "tools/travis/runSystemTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\n\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_SYSTEM\"\nexport GRADLE_COVERAGE=true\n\n./setupPrereq.sh /ansible/files/runtimes-nodeonly.json\n\n./distDocker.sh\n\n./setupSystem.sh /ansible/files/runtimes-nodeonly.json\n\n./runTests.sh\n"
  },
  {
    "path": "tools/travis/runTests.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\n# Build script for Travis-CI.\n\nSECONDS=0\nSCRIPTDIR=$(cd \"$(dirname \"$0\")\" && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR\ncat whisk.properties\nTERM=dumb ./gradlew :tests:testCoverageLean :tests:reportCoverage :tests:testSwaggerCodegen\n\nbash <(curl -s https://codecov.io/bash)\necho \"Time taken for ${0##*/} is $SECONDS secs\"\n"
  },
  {
    "path": "tools/travis/runUnitTests.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\n\ncd $ROOTDIR/tools/travis\nexport TESTCONTAINERS_RYUK_DISABLED=\"true\"\nexport ORG_GRADLE_PROJECT_testSetName=\"REQUIRE_ONLY_DB\"\n\n./setupPrereq.sh\n\ncat \"$ROOTDIR/tests/src/test/resources/application.conf\"\n\n./distDocker.sh\n\n# yet another hack to hit docker rate limits early...\ndocker pull alpine:3.5\n\n./runTests.sh\n"
  },
  {
    "path": "tools/travis/scan.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\n# Build script for Travis-CI.\nSECONDS=0\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\nHOMEDIR=\"$SCRIPTDIR/../../../\"\nUTILDIR=\"$HOMEDIR/openwhisk-utilities/\"\n\ncd $ROOTDIR\n./tools/travis/flake8.sh  # Check Python files for style and stop the build on syntax errors\n\n# clone the openwhisk utilities repo.\ncd $HOMEDIR\ngit clone https://github.com/apache/openwhisk-utilities.git\n\n# run the scancode util. against project source code starting at its root\ncd $UTILDIR\nscancode/scanCode.py --config scancode/ASF-Release.cfg $ROOTDIR\n\n# run scalafmt checks\ncd $ROOTDIR\nTERM=dumb ./gradlew checkScalafmtAll\n\n# lint tests to all be actually runnable\nMISSING_TESTS=$(grep -rL \"RunWith\" --include=\"*Tests.scala\" tests || true)\nif [ -n \"$MISSING_TESTS\" ]\nthen\n  echo \"The following tests are missing the 'RunWith' annotation\"\n  echo $MISSING_TESTS\n  exit 1\nfi\n\necho \"Time taken for ${0##*/} is $SECONDS secs\"\n"
  },
  {
    "path": "tools/travis/setup-docker.py",
    "content": "#!/usr/bin/env python\n\"\"\"Executable Python script for setting up docker daemon.\n\nAdd docker daemon configuration options in /etc/docker/daemon.json\n\n  Run this script as:\n  $python setup-docker.py\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\"\"\"\n\nfrom __future__ import print_function\n\nimport json\nimport traceback\n\nDOCKER_DAEMON_FILE = \"/etc/docker/daemon.json\"\n\n# Read the file.\n\nDOCKER_OPTS = {\n    \"hosts\": [\n        \"tcp://0.0.0.0:4243\",\n        \"unix:///var/run/docker.sock\"\n    ],\n    \"storage-driver\": \"overlay\",\n    \"userns-remap\": \"default\"\n}\n\n\ndef get_daemon_content():\n    data = {}\n    with open(DOCKER_DAEMON_FILE) as json_file:\n        data = json.load(json_file)\n    return data\n\n\ndef add_content(data):\n    for config in DOCKER_OPTS.items():\n        # config will be a tuple of key, value\n        # ('hosts', ['tcp://0.0.0.0:4243', 'unix:///var/run/docker.sock'])\n        key, value = config\n        data[key] = value\n    return data\n\n\ndef write_to_daemon_conf(data):\n    try:\n        with open(DOCKER_DAEMON_FILE, 'w') as fp:\n            json.dump(data, fp)\n    except Exception as e:\n        print(\"Failed to write to daemon file\")\n        print(e)\n        traceback.print_exc()\n\n\nif __name__ == \"__main__\":\n    current_data = get_daemon_content()\n    print(current_data)\n    updated_data = add_content(current_data)\n    print(updated_data)\n    write_to_daemon_conf(updated_data)\n    print(\"Successfully Configured Docker daemon.json\")\n"
  },
  {
    "path": "tools/travis/setup.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# retries a command for five times and exits with the non-zero exit if even after\n# the retries the command did not succeed.\nfunction retry() {\n  local exitcode=0\n  for i in {1..5};\n  do\n    exitcode=0\n    \"$@\" && break || exitcode=$? && echo \"$i. attempt failed. Will retry $((5-i)) more times!\" && sleep 1;\n  done\n  if [ $exitcode -ne 0 ]; then\n    exit $exitcode\n  fi\n}\n\n# Python\npython -m pip install --user couchdb\n\n# Ansible\npython -m pip install --user ansible==2.8.18\n\n# Azure CosmosDB\npython -m pip install --user pydocumentdb\n\n# Support the revises log upload script\npython -m pip install --user humanize requests\n\n# Scan code before compiling the code\n./scan.sh\n\n# Basic check that all code compiles and depdendencies are downloaded correctly.\n# Compiling the tests will compile all components as well.\n#\n# Downloads the gradle wrapper, dependencies and tries to compile the code.\n# Retried 5 times in case there are network hiccups.\nTERM=dumb retry ./gradlew :tests:compileTestScala\n"
  },
  {
    "path": "tools/travis/setupLeanSystem.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\n# Build script for Travis-CI.\nSECONDS=0\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\nRUNTIMES_MANIFEST=${1:-\"/ansible/files/runtimes.json\"}\n\ncd $ROOTDIR/ansible\n\n$ANSIBLE_CMD openwhisk.yml -e manifest_file=\"$RUNTIMES_MANIFEST\" -e lean=true\n$ANSIBLE_CMD apigateway.yml\n$ANSIBLE_CMD routemgmt.yml\n\necho \"Time taken for ${0##*/} is $SECONDS secs\"\n"
  },
  {
    "path": "tools/travis/setupPrereq.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\n# Build script for Travis-CI.\nSECONDS=0\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\nRUNTIMES_MANIFEST=${1:-\"/ansible/files/runtimes.json\"}\n\ncd $ROOTDIR/ansible\n\n$ANSIBLE_CMD setup.yml -e mode=HA\n$ANSIBLE_CMD prereq.yml\n$ANSIBLE_CMD couchdb.yml\n$ANSIBLE_CMD initdb.yml\n$ANSIBLE_CMD wipe.yml\n$ANSIBLE_CMD elasticsearch.yml\n$ANSIBLE_CMD etcd.yml\n\n$ANSIBLE_CMD properties.yml -e manifest_file=\"$RUNTIMES_MANIFEST\"\necho \"Time taken for ${0##*/} is $SECONDS secs\"\n"
  },
  {
    "path": "tools/travis/setupSystem.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\n\n# Build script for Travis-CI.\nSECONDS=0\nSCRIPTDIR=$(cd $(dirname \"$0\") && pwd)\nROOTDIR=\"$SCRIPTDIR/../..\"\nRUNTIMES_MANIFEST=${1:-\"/ansible/files/runtimes.json\"}\n\n# This is required because it is timed out to pull the image during the test.\ndocker pull openwhisk/example\n\ncd $ROOTDIR/ansible\n\n$ANSIBLE_CMD openwhisk.yml -e manifest_file=\"$RUNTIMES_MANIFEST\" -e db_activation_backend=ElasticSearch\n$ANSIBLE_CMD apigateway.yml\n$ANSIBLE_CMD routemgmt.yml\n\necho \"Time taken for ${0##*/} is $SECONDS secs\"\n"
  },
  {
    "path": "tools/ubuntu-setup/README.md",
    "content": "<!--\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n-->\n\n# Setting up OpenWhisk on Ubuntu server(s)\n\nThe following are verified to work on Ubuntu 18.04. You may need `sudo` or root access to install required software depending on your system setup.\n\nThe commands below should be executed on the host machine for single VM/server deployments of OpenWhisk.\nFor a distributed deployment spanning multiple VMs, the commands should be executed on a machine with network connectivity to all the VMs in the deployment - this is called the `bootstrapper` and it is ideally an Ubuntu 18.04 VM that is provisioned in an IaaS (infrastructure as a service platform).\nYour local machine can act as the bootstrapper as well if it can connect to the VMs deployed in your IaaS.\n\n  ```\n  # Install git if it is not installed\n  sudo apt-get install git -y\n\n  # Clone openwhisk\n  git clone https://github.com/apache/openwhisk.git openwhisk\n\n  # Change current directory to openwhisk\n  cd openwhisk\n  ```\n\nOpen JDK 8 is installed by running the following script as the default Java environment.\n\n  ```\n  # Install all required software\n  (cd tools/ubuntu-setup && ./all.sh)\n  ```\n\nIf you choose to install Oracle JDK 8 instead of Open JDK 8, please run the following script.\n\n  ```\n  # Install all required software\n  (cd tools/ubuntu-setup && ./all.sh oracle)\n  ```\n\n### Select a data store\nFollow instructions [tools/db/README.md](../db/README.md) on how to configure a data store for OpenWhisk.\n\n## Build\n\n  ```\n  cd <home_openwhisk>\n  ./gradlew distDocker\n  ```\nIf your build fails with 'Exception in thread \"main\" javax.net.ssl.SSLException: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty', you might need to run 'sudo update-ca-certificates -f'.\n\n## Deploy\n\nFollow the instructions in [ansible/README.md](../../ansible/README.md) to deploy and teardown OpenWhisk within a single machine or VM.\n\nOnce deployed, several Docker containers will be running in your machine.\nYou can check that containers are running by using the docker CLI with the command `docker ps`.\n\n### Configure the CLI\nFollow instructions in [Configure CLI](../../docs/cli.md). The API host\nshould be `172.17.0.1` or more formally, the IP of the `edge` host from the\n[ansible environment file](../../ansible/environments/local/hosts).\n\n### Use the wsk CLI\n```\nbin/wsk action invoke /whisk.system/utils/echo -p message hello --result\n{\n    \"message\": \"hello\"\n}\n```\n\n"
  },
  {
    "path": "tools/ubuntu-setup/all.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#  This script can be tested for validity by doing something like:\n#\n#  docker run -v \"${OPENWHISK_HOME}:/openwhisk\" ubuntu:trusty \\\n#    sh -c 'apt-get update && apt-get -y install sudo && /openwhisk/tools/ubuntu-setup/all.sh'\n#\n#  ...but see the WARNING at the bottom of the script before tinkering.\n\nset -e\nset -x\n\nJAVA_SOURCE=${1:-\"open\"}\n\nSOURCE=\"${BASH_SOURCE[0]}\"\nSCRIPTDIR=\"$( dirname \"$SOURCE\" )\"\n\necho \"*** installing basics\"\n/bin/bash \"$SCRIPTDIR/misc.sh\"\n\necho \"*** installing python dependences\"\n/bin/bash \"$SCRIPTDIR/pip.sh\"\n\necho \"*** installing java\"\n/bin/bash \"$SCRIPTDIR/java8.sh\" $JAVA_SOURCE\n\necho \"*** installing ansible\"\n/bin/bash \"$SCRIPTDIR/ansible.sh\"\n\n# WARNING:\n#\n# This step MUST be last when testing scripts for validity using\n# Docker (as recommended above).  The reason is because the scripted restart\n# of docker may actually communicates with a Docker for Mac controlling\n# instance and terminate the container.  It's the last step, so it's okay,\n# but nothing after this step will run in that validity test situation.\n\necho \"*** installing docker\"\nu_release=\"$(lsb_release -rs)\"\nif [ \"${u_release%%.*}\" -lt \"16\" ]; then\n    /bin/bash \"$SCRIPTDIR/docker.sh\"\nelse\n    echo \"--- WARNING -------------------------------------------------\"\n    echo \"Using EXPERIMENTAL Docker CE script on Xenial or later Ubuntu\"\n    echo \"--- WARNING -------------------------------------------------\"\n    /bin/bash \"$SCRIPTDIR/docker-xenial.sh\"\nfi\n"
  },
  {
    "path": "tools/ubuntu-setup/ansible.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\nset -x\n\nsudo pip install --upgrade setuptools pip\nsudo apt-get install -y software-properties-common\nsudo apt-add-repository -y ppa:ansible/ansible\nsudo apt-get update\nsudo apt-get install -y python-dev libffi-dev libssl-dev\nsudo pip install markupsafe\nsudo pip install ansible==2.5.2\nsudo pip install jinja2==2.9.6\nsudo pip install docker==2.2.1    --ignore-installed  --force-reinstall\nsudo pip install httplib2==0.9.2  --ignore-installed  --force-reinstall\nsudo pip install requests==2.10.0 --ignore-installed  --force-reinstall\n\nansible --version\nansible-playbook --version\n"
  },
  {
    "path": "tools/ubuntu-setup/bashprofile.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n# Adds openwhisk bin to bash profile\necho 'export PATH=$HOME/openwhisk/bin:$PATH' > \"$HOME/.bash_profile\"\n# Adds tab completion\necho 'eval \"$(register-python-argcomplete wskadmin)\"' >> \"$HOME/.bash_profile\"\n"
  },
  {
    "path": "tools/ubuntu-setup/docker-xenial.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n#\n#  WARNING:  This is EXPERIMENTAL support for running OpenWhisk on the latest\n#            stable version of Docker CE on Ubuntu Xenial or later.  Proceed\n#            at your own risk.\n#\n#  Currently, ./all.sh does not support running this shell script.ls\n#\n\nset -e\nset -x\n\nsudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common\ncurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\nsudo apt-key fingerprint 0EBFCD88\n\nsudo add-apt-repository \\\n    \"deb [arch=$(uname -m | sed -e 's/x86_64/amd64/g')] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\"\nsudo apt-get -y update\n\nsudo apt-get purge lxc-docker || /bin/true\nsudo apt-cache policy docker-engine\n\n# DOCKER\n\n# NOTE: For the moment, this script will use the latest stable version of\n#       Docker CE.  When OpenWhisk locks down on a version of Docker CE to use,\n#       it can then be locked in using the commented lines\n#sudo apt-get install -y docker-ce=$docker_ce_version\n#sudo apt-mark hold docker-engine\nsudo apt-get install -y docker-ce  # Replace with lines above to lock in version\n\n# enable (security - use 127.0.0.1)\nsudo -E bash -c 'echo '\\''DOCKER_OPTS=\"-H tcp://0.0.0.0:4243 -H unix:///var/run/docker.sock --storage-driver=aufs\"'\\'' >> /etc/default/docker'\nsudo gpasswd -a \"$(whoami)\" docker\n\nsudo service docker restart\n"
  },
  {
    "path": "tools/ubuntu-setup/docker.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\nset -x\n\nsudo sudo apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common\ncurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -\nsudo apt-key fingerprint 0EBFCD88\n\nsudo add-apt-repository \\\n   \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\"\n\nsudo add-apt-repository \\\n    \"deb [arch=$(uname -m | sed -e 's/x86_64/amd64/g')] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\"\nsudo apt-get -y update\n\n# AUFS\n# Use '-virtual' package to support docker tests of the script\nsudo apt-get --no-install-recommends -y install linux-image-extra-virtual\n\n# DOCKER\nsudo apt-get install -y docker-ce=18.06.3~ce~3-0~ubuntu containerd.io\nsudo apt-mark hold docker-ce\n\n# enable (security - use 127.0.0.1)\nsudo -E bash -c 'echo '\\''DOCKER_OPTS=\"-H unix:///var/run/docker.sock --storage-driver=aufs\"'\\'' >> /etc/default/docker'\nsudo gpasswd -a \"$(whoami)\" docker\n\nsudo service docker restart\n"
  },
  {
    "path": "tools/ubuntu-setup/java8.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\nset -x\n\nJAVA_SOURCE=${1:-\"open\"}\n\nif [ \"$JAVA_SOURCE\" != \"oracle\" ] ; then\n    if [ \"$(lsb_release -cs)\" == \"trusty\" ]; then\n        sudo apt-get install -y software-properties-common python-software-properties\n        sudo add-apt-repository ppa:jonathonf/openjdk -y\n        sudo apt-get update\n    fi\n\n    sudo apt-get install openjdk-8-jdk -y\nelse\n    sudo apt-get install -y software-properties-common python-software-properties\n    sudo add-apt-repository ppa:webupd8team/java -y\n    sudo apt-get update\n\n    echo 'oracle-java8-installer shared/accepted-oracle-license-v1-1 boolean true' | sudo debconf-set-selections\n    sudo apt-get install oracle-java8-installer -y\nfi\n"
  },
  {
    "path": "tools/ubuntu-setup/misc.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\nset -x\n\nexport DEBIAN_FRONTEND=noninteractive\n\nsudo apt-get update -y\nsudo apt-get install -y ntp git zip unzip tzdata lsb-release npm\n\necho \"Etc/UTC\" | sudo tee /etc/timezone\nsudo dpkg-reconfigure --frontend noninteractive tzdata\n\nsudo service ntp restart\nsudo ntpq -c lpeer\n"
  },
  {
    "path": "tools/ubuntu-setup/pip.sh",
    "content": "#!/bin/bash\n#\n# Licensed to the Apache Software Foundation (ASF) under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nset -e\nset -x\n\nsudo apt-get install -y python-pip\nsudo pip install argcomplete\nsudo pip install couchdb\n"
  }
]